The IE8 “hover” Bug: The Most Awesome IE Bug Ever?

Numbered Lists

When you want to create a numbered list in HTML, you probably use the <ol> and <li> tags.  These work well most of the time, and are pretty flexible when paired up with CSS.  You can have the numbers be decimals, alphabetic characters (lower or upper), roman numerals (lower or upper), and more, or you can hide them altogether.

But sometimes you can’t (or just don’t want to) use <li> tags to structure your markup, but you still want automatic numbering.  So CSS 2.1 came up with this swanky idea to allow automatic numbering on any element:  http://www.w3.org/TR/CSS2/generate.html.  It also allows decimal, alpha, roman, and other numbering formats, which is great.  And it’s officially supported in Firefox 4+, Google Chrome 10+, Safari 5+, and IE8+, which is also great-ish!

Here’s an example of how to use it that someone else did:  http://timmychristensen.com/css-ordered-list-numbering-examples.html

You know what’s funny, though?  It doesn’t really work that well in IE8, and you have to add some hacks to get it to work in IE9, too:  http://stackoverflow.com/questions/5584500/ordered-list-showing-all-zeros-in-ie9.  Oh, and the hacks don’t really work reliably, as you can see from people’s responses in that post.

The Bug

image

I love this bug.  It’s just bafflingly awesome.

  • IF you are using IE8 (the native version, not IE9 in IE8 compatibility mode!),
  • AND you are using the CSS ordered list numbering approach described above,
  • AND the html that has the classes that use the “counter-reset” and “counter-increment” CSS attributes is HIDDEN when the page loads,
  • THEN whenever that hidden html is displayed,
  • ALL of the automatic numbers will be ZERO,
  • BUT ONLY IF THE CSS :hover PSEUDO-CLASS IS USED ON THAT PAGE!

Okay, why am I yelling?  Because it’s that weird.  If you have CSS like this:

a:hover { text-decoration: underline; }

The numbers will all be zero in IE8.  If you remove the :hover, or change it to any other pseudo-class (like :active), the numbers are correct.

Here is a self-contained code snippet that exhibits the problem.  Remember, this only happens in IE8 NATIVE, and not in compatibility mode.

<!DOCTYPE html>
<html>
<head>

<style type="text/css">
    .ol-1-ctr {
        counter-reset: ol1 0;
    }
    .ol-1 {
        margin-left: 24px;
    }
    .ol-1:before {
        display: inline-block;
        font-weight: bold;
        float: left;
        width: 24px;
        margin-left: -24px;
        content: counter(ol1) ". ";
        counter-increment: ol1;
    }

    a:hover { text-decoration:underline; }
</style>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(function() {
    $('#step-2').hide();
    $('#show-step-2').click(function() {
        $('#step-1').hide();
        $('#step-2').show();
    });
    $('#show-step-1').click(function() {
        $('#step-2').hide();
        $('#step-1').show();
    });
});
</script>
</head>
<body>
    <a id="show-step-1" href="#">Show Step 1</a>
    <a id="show-step-2" href="#">Show Step 2</a>

    <div id="step-1">
        <div class="ol-1-ctr">
            <div class="ol-1">Step 1 Item 1</div>
            <div class="ol-1">Step 1 Item 2</div>
            <div class="ol-1">Step 1 Item 3</div>
        </div>
    </div>

    <div id="step-2">
        <div class="ol-1-ctr">
            <div class="ol-1">Step 2 Item 1</div>
            <div class="ol-1">Step 2 Item 2</div>
            <div class="ol-1">Step 2 Item 3</div>
        </div>
    </div>
</body>
</html>

The Fix

So, the support is dodgy in IE overall.  And, IE7 doesn’t support the “counter-reset” and “counter-increment” at all.

I had to come up with another approach.

First, I thought I could just remove all instances of the :hover pseudo-class from my CSS, but that’s heavy-handed, and hover effects are pretty darn useful for buttons.  Plus, my project was using jQuery UI, which is littered with :hovers.

Next, I tried to get fancy with CSS, by not hiding anything when the page loads, so the numbering still works.  But that meant using CSS absolute positioning and top/left values of –10000px to “hide” the divs by putting them outside of the visible window.  Disgusting, and not really something you can do easily if you are using other JavaScript frameworks.

The answer was clearly JavaScript.

The concept was simple:

  • Use a div with a class that represents a container for a numbered list (much like the <ol> tag does).
  • Sequentially number any items in that container with the class “ol-x”, where “x” represents the nesting level.
  • Allow for alphabetic characters and roman numerals as numbering formats, like the default outline numbering in Microsoft Word.
  • Handle up to 9 levels of nesting, because more than that is ridiculous.

I use jQuery for my solution, because it made this really easy.

So here is a self-contained solution that solves this problem, and works in IE8 (and IE7 too!).  I’ve got some sample HTML to show nine levels of nesting, for fun:

<!DOCTYPE html>
<html>
<head>

<style type="text/css">
    .dn
    {
        display: inline-block;
        font-weight: bold;
        float: left;
        width: 24px;
        margin-left: -24px;
    }

    .ol-1, .ol-2, .ol-3, .ol-4, .ol-5, .ol-6, .ol-7, .ol-8, .ol-9,
    .ol-1-ctr .ol-2-ctr, .ol-2-ctr .ol-3-ctr, .ol-3-ctr .ol-4-ctr,
    .ol-4-ctr .ol-5-ctr, .ol-5-ctr .ol-6-ctr, .ol-6-ctr .ol-7-ctr,
    .ol-7-ctr .ol-8-ctr, .ol-8-ctr .ol-9-ctr {
        margin-left: 24px;
    }
</style>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(function() {
    var DynamicNumbering = (function() {
        // Private Variables
        var open = '<div class="dn">', close = '.</div>';
        var alpha = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
        var roman = ['i','ii','iii','iv','v','vi','vii','viii','ix','x','xi','xii','xiii','xiv','xv','xvi','xvii','xviii','xix','xx'];

        // Private Functions
        var getLevelDisplay = function(level, index) {
            var display;
            switch (level) {
                case 1:
                case 4:
                case 7:
                    display = index + 1;
                    break;
                case 2:
                case 5:
                case 8:
                    display = alpha[index];
                    break;
                case 3:
                case 6:
                case 9:
                    display = roman[index];
                    break;
                default:
                    display = 'X';
            }
            return open + display + close;
        };

        // Public Functions
        return {
            execute: function() {
                $('.ol-1-ctr').each(function() {
                    $('.ol-1', this).each(function(i) { $(this).prepend(getLevelDisplay(1, i)); });
                });
                $('.ol-2-ctr').each(function() {
                    $('.ol-2', this).each(function(i) { $(this).prepend(getLevelDisplay(2, i)); });
                });
                $('.ol-3-ctr').each(function() {
                    $('.ol-3', this).each(function(i) { $(this).prepend(getLevelDisplay(3, i)); });
                });
                $('.ol-4-ctr').each(function() {
                    $('.ol-4', this).each(function(i) { $(this).prepend(getLevelDisplay(4, i)); });
                });
                $('.ol-5-ctr').each(function() {
                    $('.ol-5', this).each(function(i) { $(this).prepend(getLevelDisplay(5, i)); });
                });
                $('.ol-6-ctr').each(function() {
                    $('.ol-6', this).each(function(i) { $(this).prepend(getLevelDisplay(6, i)); });
                });
                $('.ol-7-ctr').each(function() {
                    $('.ol-7', this).each(function(i) { $(this).prepend(getLevelDisplay(7, i)); });
                });
                $('.ol-8-ctr').each(function() {
                    $('.ol-8', this).each(function(i) { $(this).prepend(getLevelDisplay(8, i)); });
                })
                $('.ol-9-ctr').each(function() {
                    $('.ol-9', this).each(function(i) { $(this).prepend(getLevelDisplay(9, i)); });
                });
            }
        };
    })();

    DynamicNumbering.execute();
});
</script>

</head>
<body>
    <div class="ol-1-ctr">
        <div class="ol-1">First 1</div>
        <div class="ol-1">First 2</div>

        <div class="ol-2-ctr">
            <div class="ol-2">Second 1</div>
            <div class="ol-2">Second 2</div>

            <div class="ol-3-ctr">
                <div class="ol-3">Third 1</div>
                <div class="ol-3">Third 2</div>

                <div class="ol-4-ctr">
                    <div class="ol-4">Fourth 1</div>
                    <div class="ol-4">Fourth 2</div>

                    <div class="ol-5-ctr">
                        <div class="ol-5">Fifth 1</div>
                        <div class="ol-5">Fifth 2</div>

                        <div class="ol-6-ctr">
                            <div class="ol-6">Sixth 1</div>
                            <div class="ol-6">Sixth 2</div>

                            <div class="ol-7-ctr">
                                <div class="ol-7">Seventh 1</div>
                                <div class="ol-7">Seventh 2</div>

                                <div class="ol-8-ctr">
                                    <div class="ol-8">Eighth 1</div>
                                    <div class="ol-8">Eighth 2</div>

                                    <div class="ol-9-ctr">
                                        <div class="ol-9">Ninth 1</div>
                                        <div class="ol-9">Ninth 2</div>
                                        <div class="ol-9">Ninth 3</div>
                                    </div>
                                    <div class="ol-8">Eighth 3</div>
                                </div>
                                <div class="ol-7">Seventh 3</div>
                            </div>
                            <div class="ol-6">Sixth 3</div>
                        </div>
                        <div class="ol-5">Fifth 3</div>
                    </div>
                    <div class="ol-4">Fourth 3</div>
                </div>
                <div class="ol-3">Third 3</div>
            </div>
            <div class="ol-2">Second 3</div>
        </div>
        <div class="ol-1">First 3</div>
    </div>
</body>
</html>

Improvements

This is not beautiful code by any means, but it works for now.

I’m updating this script to be more generic, handle alternate numbering formats (like WBS), and handle nesting more gracefully.

Maybe I’ll post that when it’s ready?

In the meantime, I hope this helps someone out.