Skip to main content
Ben Nadel at cf.Objective() 2011 (Minneapolis, MN) with: Angela Buraglia and Dan Short
Ben Nadel at cf.Objective() 2011 (Minneapolis, MN) with: Angela Buraglia ( @aburaglia ) Dan Short ( @danshort )

Exploring Plain-Text Data URIs And The Anchor Download Attribute In JavaScript

By on

The other day, I took a look at using the Flexmark library to parse markdown content into HTML output in ColdFusion. As part of that exploration, I used the Prism.js JavaScript library to add syntax-highlighting to the rendered fenced code-blocks. While on the Prism.js site, I noticed something cool - it was using the anchor download attribute in order to provide customized versions of the Prism JS and CSS files. While I've looked at using the anchor download attribute to download PNG meme files, I've never used it for plain-text. In fact, I've never even used a Data URI for plain text. As such, I wanted to do a little exploration of how to use the anchor download attribute to prompt plain-text file downloads in JavaScript.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

When I first started to put this demo together, I was operating under the assumption that all Data URIs had to be Base64-encoded. As such, I started to look at how one might convert plain text into a Base64 payload. It was then that I discovered that modern browsers provide btoa() and atob() methods specifically for Base64 encoding and decoding, respectively.

But then, when I went to double-check the Data URI syntax on MDN, I saw that they had examples that completely omitted the "base64" directive. As it turns out, you only need to Base64-encode non-text payloads, like images. If you need a Data URI that points to a plain text payload, all you have to do is encode the value the same way that you would encode any other URI component.

To pull both of these new findings together and cement them in my mental model, I created a small demo in which you can enter arbitrary text into a Textarea form field. The entered text value can then be downloaded as a .txt file using an anchor tag's href and download attributes. And, to make things more interesting, I added a checkbox that determines whether or not the Data URI in the href attribute is generated using a Base64 or a plain-text encoding.

NOTE: As a reminder, the "download" attribute of the Anchor instructs the browser to download the associated URL instead of navigating to it. It is supported in all modern browsers (including Edge).

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>
		Exploring Plain-Text Data URIs And The Anchor Download Attribute In JavaScript
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css" />
</head>
<body>

	<h1>
		Exploring Plain-Text Data URIs And The Anchor Download Attribute In JavaScript
	</h1>

	<form>

		<textarea name="input">I like to move it, move it.</textarea>

		<!-- NOTE: Download attribute not supported in IE (but is in Edge). -->
		<a href="javascript:void(0)" download="data.txt">
			Download Text
		</a>

		<label for="useBase64">
			<input type="checkbox" id="useBase64" name="useBase64" />
			Use Base64 encoding?
		</label>

	</form>

	<script type="text/javascript">

		// Gather our DOM references.
		var input = document.querySelector( "textarea[ name = 'input' ]" );
		var download = document.querySelector( "a[ download ]" );
		var useBase64 = document.querySelector( "input[ name = 'useBase64' ]" );

		// Listen for relevant form changes so that we can dynamically update the HREF
		// attribute of our download link to contain the proper Data URI.
		input.addEventListener( "input", updateDownloadHref, false );
		useBase64.addEventListener( "change", updateDownloadHref, false );

		// Initialize the download link.
		updateDownloadHref();

		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //

		// I update the HREF of the download link to point to the textarea payload.
		function updateDownloadHref() {

			var text = input.value;

			// When we create a DATA URI for our download link, we don't have to encode
			// the value as Base64. Base64 is only _required_ when we are representing a
			// non-textual payload, like an Image. And, if we're not using Base64, we
			// only have to worry about standard entity encoding for the URL.
			if ( useBase64.checked ) {

				download.setAttribute(
					"href",
					( "data:text/plain;charset=utf-8;base64," + base64Encode( text ) )
				);

			} else {

				download.setAttribute(
					"href",
					( "data:text/plain;charset=utf-8," + encodeURIComponent( text ) )
				);

			}

		}


		// I encode the given string as a Base64 value.
		function base64Encode( stringInput ) {

			// NOTE: This normalization technique for handling characters that require
			// more than an 8-bit representation was provided on the Mozilla Developer
			// Network website for Base64 encoding. They also provided a companion DECODE
			// method. But, we don't need to decode in this demo.
			// --
			// READ MORE: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
			var normalizedInput = encodeURIComponent( stringInput ).replace(
				/%([0-9A-F]{2})/g,
				function toSolidBytes( $0, hex ) {

					return( String.fromCharCode( "0x" + hex ) );

				}
			);

			return( btoa( normalizedInput ) );

		}

	</script>

</body>
</html>

As you can see, there's not too much going on in this demo. You enter some text, you click the anchor tag, you get prompted to download the text file (all on the client-side). It's definitely cool; but, the most interesting part of this demo is the updateDownloadHref() function that actually generates the Data URI for the download. Depending on the state of the checkbox, it either generates a Base64-encoded payload or a plain-text payload.

If we open the browser, enter some text in the textarea, and click the "Download Text" link, the browser will prompt us to save the file. And, if we open the file, we can see that it contains the text we entered in the browser:

Downloading plain text files on the client-side using the anchor href and download attributes with Data URIs.

As you can see, the "Download Text" link caused my Chrome browser to download the "data.txt" file. And, when I open the .txt file in Sublime Text, the contents of the file mirror what we had in the Textarea input field.

Anyway, I suppose this was more of a "note to self" than anything else. I didn't realize that browsers offer native Base64 encoding and decoding support. And, I didn't realize that you could include non-Base64-encoded payloads in a Data URI. So, putting all of this new information together makes for a rather easy way to prompt the user to download custom text files in JavaScript without any server-side interaction required.

Want to use code from this post? Check out the license.

Reader Comments

3 Comments

Thanks so much for this article! This technique immediately solved a major problem I was having with a tool at Lottery Post.

It is a lottery combinations generator that generates all possible lottery combinations given a set of user-defined parameters, using a JavaScript Web Worker. (Available here: https://www.lotterypost.com/combinations) Previously the tool could not generate huge combination sets (several million combinations) because it had to display them all on the page, and the browser would run out of memory.

Now, using this technique, for any set of combinations greater than 100,000 lines, it shows a download button instead.

Because of the size of the downloads, I had to use a Blob technique instead of using a data URL directly. (A data URL can not handle huge sizes.) So the data URL is converted into a Blob object, and then the Blob is converted into a URL.

I am so grateful that you documented this, I had no idea this technique existed!

15,663 Comments

@Speednet,

I'm glad you found this helpful :D And, I have to say, that I'm actually very interested in your "Blob" approach. Someone on my team just mentioned to me that the Data URI has a limitation (as you mentioned). And that they were using the Blob approach. It's something I'll have to take a look at as well.

3 Comments

@Ben,

I used the following function to convert the data URL into a Blob:

function dataUriToBlob( dataUri ) {

	var binStr = dataUri.split( ',' )[1],
		len = binStr.length,
		arr = new Uint8Array( len ),
		mimeString = dataUri.split( ',' )[0].split( ':' )[1].split( ';' )[0]

	for ( var i = 0; i < len; i++ ) {
		arr[i] = binStr.charCodeAt( i );
	}

	return new Blob( [arr], { type: mimeString } );
}

Then, I load up the link's href with some jQuery:

$downloadButton.attr( "href", URL.createObjectURL( dataUriToBlob( "data:text/plain;charset=utf-8," + evt.data.output + suffix ) ) );

(The "evt.data.output + suffix" is the output from the combinations generator.)

3 Comments

@Ben,

I forgot to mention that if your data URI is base64 encoded, you would need to change the first var line to:

var binStr = atob( dataUri.split( ',' )[1] ),

Also, the data URI should not be URLEncoded. (Or you would need to add URL decoding to the first var.)

15,663 Comments

@Speednet,

Gotcha. Though, I assume if I was planning to use a a Blob, I would bypass the original Data URI to begin with and just use the Blob stuff direclty.

2 Comments

Unfortunately, most major browsers have blocked top-frame navigation to data URLs. Apparently, they are commonly used in phishing attacks.

In Chrome's Dev Console, you'll see errors such as this:

Not allowed to navigate top frame to data URL: [...]

More info:

15,663 Comments

@Illya,

Very interesting. I am not sure that I've run into this issue. Perhaps there are certain circumstances under which this security is applied; and, times when it is not? Thank you for pointing it out, though - I will keep my eyes open for this limitation.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel