A polyfill for HTML5 progress element, the obsessive perfectionist way

Yesterday, for some reason I don’t remember, I was looking once more at Paul Irish’s excellent list of polyfills on Github. I was really surprised to see that there are none for the <progress> element. It seemed really simple: Easy to fake with CSS and only 4 IDL attributes (value, max, position and labels). “Hey, it sounds fun and easy, I’ll do it!”, I thought. I have no idea how in only 1 day this turned into “OMG, my brain is going to explode”. I’ve documented below all the pitfalls I faced. And don’t worry, it has a happy ending: I did finish it. And published it. So, if you’re not interested in long geeky stories, just jump straight to its page.

First things first: Controlling the width of the value bar

Most progress bars out there use 2 elements: One for the container and one for the value bar. I was pretty stubborn about not using an extra element. I wanted to use pseudo-elements instead and keep the DOM tree as clean as I found it. And there it was, the first problem: How to set the width?

CSS3 attr() and calc() are hardly supported and attr() is not even allowed in calc(), so I quickly realized that a pure CSS solution was out of the question. However, if I used JavaScript, how would I set a different width for every progress::before? You can’t set that in an inline style, and assigning every <progress> element an ID and adding separate rules seems a bit too intrusive to me. Think about it for a second, what would you do?

I realized I had to control the width of the pseudo-element through CSS properties of the parent container somehow. And then it dawned on me: If the pseudoelement has display:block, it will automatically get the parent width, minus the padding and borders. There it was, this was my solution. I just had to set padding-right accordingly, so that the value bar gets the width it needs to be! And I had already given it box-sizing: border-box, as it was in Webkit’s UA stylesheet, so I didn’t have to worry about padding changing the width of the element. The first problem was solved.

Becoming dynamic

The static part was quite easy indeed. Selecting all <progress> elements and using their attributes to set an appropriate padding-right was pretty much run of the mill. But that wasn’t enough. What happens if you set the properties through script? What happens if you set the attributes? The progress bar should update accordingly, it had to be dynamic. A static progress bar is not much of a fallback. It might be acceptable for <meter>, since in most interfaces it’s used in a static way. But a progress bar needs to change in order to show um, progress.

First step was adding the properties that are in its DOM Interface. “Easy, I’ll add them to the prototype” thought my naïve self. So, I needed to find which prototype, I didn’t want to add them in every HTML element of course. So I eagerly typed Object.prototype.toString.call(document.createElement('progress')) in Firebug’s console and it slapped me in the face with an '[object HTMLUnknownElement]'. D’oh! I had forgotten that unknown elements share a common prototype named like that. So, I had to add them to each one individually. I hated that, but since it was the only way, I did it and moved on.

Of course, I didn’t just assign a static value to them, otherwise they wouldn’t solve much: The progress bar would still be static. I assigned getters and setters that used the value and max attributes to return what they should. Assigning getters and setters to a property is a whole new problem by itself, as some browsers use __defineGetter__/__defineSetter__ and some others the ES5 standard Object.defineProperty. But I had solved that one before, so it didn’t slow me down.

The getters and setters solved the issue one-way only: If you set the properties, the progress bar and its attributes would be updated. That would be enough for most authors using the polyfill, but no, I wanted it to be perfect. “If you change the attributes, the progress bar and its properties should too!” my annoyingly pedantic inner self insisted. “And what if you dynamically add more <progress> elements?”.

There are two ways to do stuff when attributes change and elements get added: Polling and mutation events. The advantage of polling is its perfect browser support, which comes at a big cost: It’s horrible performance-wise. Also, polling introduces a delay that could be unacceptable in some cases, especially considering how short the duration of some progress bar use cases is. So, I went with mutation events, even though they are deprecated (seriously W3C? deprecating something, without providing a solid alternative??) and don’t have perfect browser support. After all, it was the only way (I don’t consider polling a real option in this case).

Styling

After messing around a little, it seemed to work great in Opera 10.63 and Firefox 5, which I had open for my tests. It was time to write some unit tests and check it out in more browsers. Instead, I opted to style it, as a desperate attempt to delay my confrontation with IE8 a bit longer (and for good reason, as it turned out later). Given that CSS is kinda my specialization, I expected styling to be a piece of cake and even relaxing. Instead, it came with it’s fair share of trouble and hard dilemmas.

If you notice the native progress bars in OSX, you will see that they use gradients. I mocked up something similar with CSS gradients, which wasn’t easy, as I wanted to keep the hue/saturation information in the background-color only, for easy modifications and Webkit uses a regular gradient with color stops that have different hues and saturations. And then I realised that this was not going to show up at all in IE8-IE9, which were 2 major browsers that my polyfill would target. No gradient may be acceptable in determinate progress bars, but it’s not an option in indeterminate ones: Scrolling diagonal stripes is the convention and there’s no other way to communicate this status to the average user.

So I decided to go with the old way of using raster images for gradients (through a data URI). Another painful slap in the face was when I realized that those moving stripes need to be semi-transparent. To do that, my options were:

  • CSS3 animations – no good in my case, as it’s crucial to show up and their browser support isn’t that good
  • SVG with SMIL – Much better browser support than CSS3 animations, but still no go in IE
  • APNG – Only supported by Firefox and Opera, even after all these years

I happened to be chatting with Tab Atkins at the moment, and he suggested I go with plain ol’ GIFs. I was originally negative, but after thinking about it I realized that antialiasing is not that crucial in 45deg stripes, especially when they’re moving. I tried it, I liked the result, so I kept it. Phew, that one was easy.

The IE8 nightmare

After spending a few hours tweaking the gradients and the CSS (yes, hours. I said I’m an obsessive perfectionist, didn’t I?) I finally wrote some unit tests and fired up Virtualbox to test with IE8. I prepared myself for the worst, and secretly hoped I’d be pleasantly surprised. Instead, I faced a developer’s worst nightmare. Two words: Stack overflow.

The culprit was a classic IE bug with DOM properties and HTML attrtibutes that I had blissfully forgotten: IE thinks they’re the same. I had added getters and setters (or etters, as I like to call both) to the max and value properties which used the max and value attributes, resulting in infinite recursion in IE8.

This was the hardest of all problems, and I never completely solved it: A few unit tests still fail in IE8 because of it, although there’s no infinite recursion any more. Luckily, this bug was fixed in IE9, so the polyfill works flawlessly there.

My first idea was the obvious one: to duplicate the values somewhere. In a lookup table, in another property, somewhere. I didn’t quite like the idea, so I kept brainstorming. And then it dawned on me. They’re already duplicated somewhere, and not only it’s not redundant, but actually encouraged: in the WAI-ARIA attributes!

To clarify, when progress elements are natively supported, they already have built-in ARIA roles and attributes. However, when they’re not, you should add them yourself, if you want the control to be accessible. From my research, there was a progressbar role, and it required the attributes aria-valuemin, aria-valuemax, aria-valuenow and aria-labelledby. I implemented all but the latter, as it proved too much of a hassle for very few edge cases (how many people put IDs in their labels without using aria-labelledby themselves?). So, aria-valuemax was already duplicating max and aria-valuenow was duplicating value. I changed everything to use those instead.

After lots of head-scratching, IE-cursing and feeling that my brain was going to explode all over my laptop, I managed to kinda have it working. I knew in advance that some unit tests would fail, as it doesn’t support mutation events. I eventually gave up when I realized that the last unit test in the “static” category failed because getAttribute('max') returned null, since IE had completely removed the attribute from the DOM tree. It was the last straw and made me say “That’s it, I’m done with this piece of shit”.

Safari 5 craziness

After IE, it was Safari’s turn. I knew that I could only target Safari 5, as Safari 4 doesn’t support etters on DOM elements and Safari 5.1 will probably support progress elements natively, since they’ve been in Webkit for ages. I launched Safari without fear. “How can it possibly not work in Safari? It will probably be fine, maybe just need a one or two little tweaks in the worst case”, I reassured myself thinking.

The progress bars were not even showing. At all. My first guess was that it was a one time rendering error. When it persisted after a few reloads, I opened the dev tools to see what the hell happened. I saw a series of errors like this:

<progress> is not allowed inside <label>. Content ignored.
Unmatched </progress> encountered.  Ignoring tag.

At first, I thought the problem was the label. So I made all labels external. And then still got the same errors for the <li>s. And every other element I tried. Even when I put them directly into the <body>, Safari complained that they are not allowed to be inside it! It turned out that this was a bug in a build of Webkit, and coincidentally, this build was the one Safari 5 uses.

There wasn’t much to think about in this one: They’re not in the DOM, so I can’t do anything about them. It’s mission impossible.

Happy(?) end

After IE8’s and Safari5’s cruel rejection, I was quite dispirited. IE8 had already caused me to make my code uglier and more verbose, and now Safari 5 flat out refuses to accept any treatment. It worked flawlessly in Firefox 3.5, but that didn’t cheer me up much. I decided that this has already taken up too much of my time. It’s now the community’s turn. Have any ideas about how further improvement? Maybe some more unit tests? I’ll be waiting for your pull requests! :)
Github repo

Appendix: Why do some unit tests fail in browsers that natively support <progress>?

While developing this, I discovered 2 browser bugs: One in Webkit’s implementation and in for Opera’s. I plan to report these soon.

  • http://www.facebook.com/jbrumond James Brumond

    Just something I noticed, looking at your demo page (http://leaverou.me/polyfills/progress/), chrome (which has native support) failed one of your tests. I don’t know if this is a bug in chrome or possibly a logic error in the test itself, but maybe something to look into :)

    The failed test was “get value property for indeterminate (should be: 0, was: 1)” and it was in chrome 12 on windows.

    Anyway, great work! I have made my own fork of the repo and spent a couple hours hacking away at some of those IE bugs… this might keep me busy for a while :)

    • http://leaverou.me Lea Verou

      I think it’s a Webkit bug and plan to report it. Opera has another (different) one too.

  • http://twitter.com/4esn0k Ivanov

    Great job!
    Chrome natively support progress,
    but Is there DOMAttrModified support in Chrome?

    • http://leaverou.me Lea Verou

      Doesn’t look like it: http://jsfiddle.net/leaverou/PcKrF/

  • http://eligrey.com Eli Grey

    Since you don’t usually include progress elements in document right away, and more often insert it only when a long task is being done, it should still be possible to support Safari 5, as it does still allow inserting created progress elements.

    • http://leaverou.me Lea Verou

      Oh right, accessors!!! I had completely forgotten this word, I’ll use that from now on, thanks!!

      I had also never thought that Safari 5 might allow that. Will look into supporting it. Thanks for that too!

      • styfle

        Accessors typically refer to Getters and Mutators refer to Setters.

  • http://twitter.com/4esn0k Ivanov

    Seems, Safari support element.__defineSetter__ on DOM element… but not support Object.defineProperty on DOM…
    Is it right?

  • http://www.hdflvplayer.net/ Flv Player

    The error or bug………… when it will have an end for itself!

  • Pingback: Some links for light reading (6/7/11) | Max Design

  • Anonymous

    Great Post. I’m prob too much of a perfectionist when it comes to HTML validation. See my HTML5 WIP that passes validation and view-ability standards. http://www.ineffableweb.com/HTML_5/home/index.html

    • Bilal Abdul

      Nice spam. Your site is awful and an offense to the HTML5 logo.

      • Anonymous

        Thanks For Sharing Bilal. For the record though it’s not a site. It’s a WIP HTML document that validates. People like you inspire me :P Thanks again.

  • Pingback: Web Design Weekly #2 | Web Design Weekly

  • Łukasz Kufel
    • http://leaverou.me Lea Verou

      I’ve seen this. It’s still at the stage of not even having a proper specification though, and Mutation events were deprecated even before that.

  • http://twitter.com/RonnyOrbach Ronny Orbach

    Brilliant and thorough as always, Lea. Thanks!
    Just nitpicking here – It shows this plugin was designed on a Mac, the OSX style looks off on Windows 7 ;-)

    Could be nice to have OS-specific skins. The easiest way I can think of supporting that is classList.add(navigator.platform) and then target progress[class*=win]

    • http://leaverou.me Lea Verou

      That’s a great idea! But I don’t have the time to do that :(

      Well, it’s on Github… ;)

  • Pingback: Detecting CSS selectors support + my JSConf EU talk | Lea Verou

  • Pingback: SWL-Projekt » Detecting CSS selectors support + my JSConf EU talk

  • spencer zhang

    Firstly, thx for your great job~ 
    var r=document.createElement(‘progress’);
    var we=document.getElementById(‘we’);
    we.insertBefore(r,we.firstChild);
    r.max=60;
    r.value=10;
    &
    var r=document.createElement(‘progress’);
    var we=document.getElementById(‘we’);
    r.max=60;
    r.value=10;
    we.insertBefore(readingprogress,we.firstChild);
    the second one won’t get any “max”,”value” attribute in ff5.
    the first one works  well.
    Is this  a misuse or a bug?

    • http://leaverou.me Lea Verou

      Hi Spencer,

      Thanks a lot for your comment. This perfectly demonstrates a support gap that JavaScript can’t help us to perfectly solve (or at least I can’t think of a way). 

      The way this polyfill works for new elements, is that it acts upon the DOMNodeInserted mutation event. This event fires when the element is added to the document. So, any properties set before that, are just regular arbitrary properties and don’t get treated by the accessors, since the accessors have not been added yet.

      One way to get around this in the polyfill itself, would be to check if the property is already set before adding accessors and assign that value instead of the default one. It’s not perfect, but it would solve your use case. I’ll try to remember to do that when I find some time.

  • Zoltan Hawryluk

    Excellent stuff! It “just works” and it is easy to style too.  It’s also great to see what the development history behind it, and as a fellow polyfill writer, it’s nice to hear war stories like this (as the saying goes, “Misery Loves Company”.  :-)

    Thank you for sharing!

  • Pingback: Cross Browser HTML5 Progress Bars In Depth « Zirkan

  • http://christiancox.com/ Christian

    Love this. I’ve added  -webkit-box-sizing:border-box; and applied a width of 100% on progress[role] to allow the progress bar to fill up the available space of a parent container on my Android 2.3 device. Thanks!

  • Peter

    Sadly, no cigar in Android mobile browser. Progress just plain doesn’t show up. I’ll start debugging, if I have something tangible, I’ll send it in.

  • http://www.rackmountsales.com/ Rackmount LCD Monitor

    Most important thing is that the polyfill must be soft n don’t shrink.

     

  • http://twitter.com/loucapo loucapo

    having an issue with this. how would you use this for a case where a set of progress bars appear in a page, but then later you add more progress bars?

  • Biophantom

    The and tag is such a wonderful idea! I just can’t understand why customizing it is so difficult! Anywho, well done Lea. I am certain many people appreciate the work you’ve done here.

  • Pingback: Html5 Progress Polyfill | mintik

  • http://sassaman.com/ Eric Sassaman

    You saved my life! Thanks for your work on this, loving the progress element now.

  • Benjamin

    Thank you Lea really help me.

  • Louise Eggleton

    Thanks. This is coming in really handy for supporting a file upload project I am working on. One minor point. In the demo, under the “How To Use” section you should also mention that for this to work in IE6-IE8, document.createElement(‘progress’) must be added to document or the HTML5 shiv library or similar must be included.

  • Pingback: 译:深度跨浏览器的HTML5进度条 | Junyi's Blog

  • Pingback: Creating & Styling Progress Bar With HTML5 | DesignNews

  • Pingback: Creating & Styling Progress Bar With HTML5 | OGM Français - Une vue terrifiante de l'Asie

  • Pingback: Creación y Estilo barra de progreso con HTML5 -

  • Pingback: Creating & Styling Progress Bar With HTML5 | DigitalMofo

  • Pingback: Creating & Styling Progress Bar With HTML5 | Android News

  • Pingback: Creating & Styling Progress Bar With HTML5 | iPixel Creative | Singapore Web Design & CMS Development Company Blog

  • Pingback: Creating & Styling Progress Bar With HTML5

  • peteroshaughnessy

    Thanks very much for this!

    I have a couple of problems in iOS Safari (6.1.3) when I inject a into the page…

    1) offsetWidth returns 0 (it’s not, it’s about 140px), so the paddingRight gets set to 0. JQuery width() gives the right result at that stage, so a dirty fix would be replacing progress.offsetWidth with $(progress).width()…

    2) After that, the element jumps to being wider, so the padding is set incorrectly. This appears to be because I’m defining the width using calc(). I guess it gets calculated after the DOMNodeInserted event? A dirty fix is to call self.init(node) after a short delay using a setTimeout…

    Thought I’d share in case anyone runs into the same issues or can suggest nicer fixes! Cheers.

  • Pingback: Beta Sites Galore | The HTML5 progress Element

  • Pingback: The HTML5 progress Element | Web Development, Search Engine Optimization, Social Media Marketing Guru

  • Pingback: The HTML5 progress Element | Lunarium Design

  • Pingback: The HTML5 progress Element | justreliable.com

  • Pingback: The HTML5 progress Element | The Creative Web Project

  • Pingback: The HTML5 progress Element - Abstract PHP

  • Pingback: » The HTML5 progress ElementPower of HTML

  • Pingback: The HTML5 progress Element | logichacker

  • Pingback: Element progress HTML5

  • Pingback: HTML5 progress标记 | YeeBing Studio

  • Pingback: Html5 Progress Polyfill