Setting the scene
For most of the 2000s, building a website meant building it twice. Once for real browsers, once for Internet Explorer 6. IE6 shipped in 2001, became the default browser on Windows XP, and then sat there. No updates. No competition. No reason for Microsoft to care.
By the time the rest of the web had caught up to the CSS 2.1 spec, IE6 was years behind. Its box model was wrong. Its float implementation was broken. It didn't support min-height, max-width, transparent PNGs, or :hover on anything that wasn't a link.
There was no @supports. No feature detection. No graceful way to say 'use this rule everywhere except in the broken browser.' So developers built one. An entire vocabulary of parser exploits, invalid syntax, and weird selectors grew up around a single goal: get IE6 to either behave, or quietly accept a different stylesheet.
None of this knowledge is useful anymore. IE6 was finally retired from Windows 7 support around 2014, and IE itself was retired in 2022. But the hacks are a snapshot of what front-end work actually looked like, and some of them are genuinely beautiful in how broken they are.
The targeting hacks
The whole game was getting a rule to apply only in IE6 (to feed it a fix), or only outside IE6 (to hide a modern rule from it). The trick was always the same: write something that exploited a parser bug in one engine but not the others.
The most famous was the star-HTML hack:
* html .selector {
color: red;
}It worked because IE6 had a phantom element above <html> in its DOM tree, which no other browser did. The universal selector matched that phantom, and the rule only applied in IE6. It looked like nonsense, but it was perfectly valid CSS that simply matched nothing in standards-compliant browsers.
IE7 closed the phantom-element bug but introduced its own quirks, so a sibling hack appeared for it:
*:first-child + html .selector {
color: red;
}Then came the property prefix hacks. IE6 silently ignored a leading underscore on a property name; IE6 and IE7 both ignored a leading asterisk. Everyone else threw the rule away as invalid.
.selector {
color: red; /* all browsers */
*color: blue; /* IE6 and IE7 */
_color: green; /* IE6 only */
}Later versions kept the tradition going. IE8 and IE9 picked up \9 as a value suffix that only those parsers tolerated. IE8 alone read \0/. Paul Irish catalogued the full set in his 2009 post on browser-specific hacks, which became the reference everyone bookmarked.
The whole family was a beautiful kind of wrong. You weren't writing CSS; you were writing things that looked like CSS but were really keys cut for a specific broken lock.
hasLayout, the bug behind half the bugs
If you only learn one piece of IE6 trivia, learn this one. Internet Explorer had a proprietary internal flag called hasLayout. Elements that had it rendered roughly correctly. Elements that didn't had bizarre, unpredictable bugs: disappearing content, doubled margins, misaligned floats, broken backgrounds.
The flag wasn't exposed in any standard CSS property. You triggered it by setting one of a handful of properties to specific values, the most common being:
.fix-everything {
zoom: 1;
}zoom: 1 did nothing visible. It just flipped hasLayout on, and roughly half of the day's bugs went away. height: 1%, width: 1%, and display: inline-block worked too, with varying side effects.
Nobody outside the IE team really understood what hasLayout was. Microsoft's own documentation was sparse. A 2005 article on Satzansatz, plus a long-running thread on Position Is Everything, became the unofficial canon. Web developers swapped hasLayout fixes the way doctors once swapped folk remedies: it works, nobody knows why, just try it.
The double margin float bug
This one was a daily occurrence. Float an element and give it a margin in the same direction. IE6 doubled the margin.
.sidebar {
float: left;
margin-left: 10px; /* IE6 rendered this as 20px */
}The fix made no semantic sense:
.sidebar {
float: left;
margin-left: 10px;
display: inline; /* kills the double-margin bug */
}An element can't be both float: left and display: inline in any sensible reading of the spec, because floating implies block-level layout. But IE6 read it, shrugged, and stopped doubling the margin. The webfx guide put its success rate at '99% of the time,' which sounds like a joke but was the honest estimate.
min-height and max-width didn't exist
IE6 didn't support min-height, min-width, max-height, or max-width. None of them. But it did have one accidental gift: it treated regular height as if it were min-height, expanding the box if content pushed past the declared value.
So the workaround was a hack-on-hack:
.box {
min-height: 500px;
height: auto !important; /* real browsers */
height: 500px; /* IE6 ignores !important here */
}Real browsers honored !important and used height: auto. IE6 ignored !important on duplicated declarations and used the second height: 500px, which it then treated as min-height. Two bugs canceled out and produced correct behavior.
For max-width there was no equivalent. People fell back on expression(), which deserves its own section.
expression(), the original CSS-in-JS
Microsoft shipped a feature called dynamic properties that let you put JavaScript directly inside a CSS value. It looked like this:
.constrained {
width: expression(document.body.clientWidth > 776 ? "777px" : "auto");
}That's how people faked max-width. It's also how they faked position: fixed, calculated layouts, and did a hundred other things CSS couldn't do.
The catch was that expression() re-evaluated constantly. Every mouse move. Every keystroke. Every scroll event. On a page with a few expressions, you could watch CPU usage spike just by waving the cursor. Microsoft removed it in IE8's standards mode and deprecated it entirely in IE11.
Looking back, expression() is the original container query: a way to make CSS respond to runtime values. It was twenty years early, ruinously slow, and only worked in one browser. But the idea was right.
PNG alpha transparency
IE6 supported PNG, but not 24-bit PNG with an alpha channel. The transparent pixels rendered as solid grey. For an entire era of web design, this single bug shaped what was possible: drop shadows had to be baked into JPEGs, rounded corners had to use sliced GIFs, glassy UI was off the table.
The fix was a proprietary Microsoft filter, usually loaded through a behavior file:
.png-fix {
filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(
src='image.png', sizingMethod='scale'
);
}This worked for foreground images. For background images it didn't, because AlphaImageLoader replaced the image rather than rendering it as a background. So a small industry of .htc behavior scripts appeared: iepngfix.htc, DD_belatedPNG.js, supersleight. You dropped them into your project, applied a class, and prayed.
.htc files (HTML Components) were a Microsoft-only mechanism for attaching JavaScript behavior to elements through CSS. Effectively a parallel-track Web Components, eight years before Web Components existed. Also: only in IE.
:hover only worked on links
In IE6, the :hover pseudo-class only matched <a> elements with an href. Nothing else. No hovering on <li>, no hovering on <div>, no hovering on form controls.
The Suckerfish Dropdowns technique, published by Patrick Griffiths and Dan Webb in A List Apart in 2003, became the standard workaround. The CSS was simple:
li:hover ul {
display: block;
}To make it work in IE6, you also shipped a few lines of JavaScript that added a sfhover class on mouseover and removed it on mouseout. The CSS then matched both: li:hover ul, li.sfhover ul.
A more ambitious fix was Peter Nederlof's csshover.htc, a behavior file you attached to the body:
body {
behavior: url(csshover.htc);
}That single line retrofitted hover support onto every element in the page. It also added a measurable performance cost, but at the time that was an acceptable trade for being able to write hover styles on a navigation list.
Clearfix and the float era
Before flexbox and grid, layout was floats. And floats had a problem: a parent containing only floated children collapsed to zero height, because floats are removed from normal flow.
The classic fix was clearfix, refined over years into Nicolas Gallagher's micro clearfix:
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
.clearfix {
*zoom: 1; /* IE6 and IE7: trigger hasLayout */
}The :after pseudo-element with clear: both handled modern browsers. The *zoom: 1 was the IE6/IE7 leg, triggering hasLayout so the parent would contain its floats. Two unrelated bugs, one rule, every layout. Clearfix variants were probably the single most copy-pasted block of CSS in the history of the web.
The really weird ones
Some bugs had fixes that nobody could fully explain. From Maciej Caputa's ten-years-on retrospective:
Randomly content will disappear, poof, and it is gone. Luckily, just add
position: relativeto the unfortunate element.
The duplicate list-item bug was even stranger: IE6 would sometimes render the last <li> twice. The fix was to insert an HTML comment immediately before the closing </ul>.
<ul>
<li>Item</li>
<li>Item</li>
<!-- fixes IE6 duplicate li bug -->
</ul>An HTML comment, fixing a CSS rendering bug. The era was full of these. The three-pixel jog, where a floated element and adjacent text developed a mysterious 3px gap. The peekaboo bug, where content vanished and reappeared on scroll. The guillotine bug, where backgrounds got chopped off below the last float. Each one had a name, a Position Is Everything entry, and a folk-remedy fix that worked for reasons nobody fully agreed on.
What replaced all of this
The cleanest way to feed CSS to a specific IE version was never a hack at all. It was Microsoft's own escape hatch: conditional comments.
<!--[if IE 6]>
<link rel="stylesheet" href="ie6.css">
<![endif]-->Real browsers saw an HTML comment and skipped it. IE6 alone parsed the condition and loaded the stylesheet. Most large projects ended up with an ie6.css file full of every workaround in this article, scoped cleanly out of the main stylesheet.
Conditional comments were removed in IE10, which is roughly when developers stopped caring. By then IE6's market share had collapsed, @supports had shipped, and the rest of the browser ecosystem was advancing fast enough that targeting old IE specifically just wasn't worth the effort. Paul Irish summed up the consensus in his 2009 post:
I don't use CSS hacks anymore. I prefer to use conditional comments on the body tag.
By 2015 even that was overkill. By 2022, IE itself was gone.
Why any of this still matters
None of these hacks are useful today. You will not write * html in a stylesheet again. You will not need to remember which character prefix targets which IE version. expression() is gone. hasLayout is gone. The browser everyone was hacking around has been dead for over a decade.
But the shape of the work is the same. We still write CSS that tries to do things the spec doesn't quite support yet. We still ship workarounds for browser bugs that get cleaned up two years later. The vocabulary of * html and _color got replaced by @supports and feature queries, then by Baseline status, then by 'just check caniuse.' The hacks got cleaner. The instinct didn't.
Most of the snippets on this site exist because something that used to require a hack now has a real answer. Aspect ratios without the padding hack. Centering without the transform hack. Mobile viewport height without the 100vh hack. Those titles are descendants of this era. They're saying: there used to be a hack here, and now there isn't.
Sources and further reading
- Paul Irish - Browser-specific CSS hacks (2009). The reference table for selector and property hacks across IE6 to 10 and beyond.
- Maciej Caputa - IE6 hacks, ten years after. A retrospective with honest 'I never figured out why this worked' commentary.
- WebFX - Definitive Guide to Taming the IE6 Beast. Period-accurate inventory of bugs and workarounds, including the duplicate-list-item fix.
- CSS-Tricks - IE CSS bugs that'll get you every time. The classic short list: box model, double margin, no min-width, stepdown, no hover, no PNG alpha.
- Nicolas Gallagher - IE CSS hacks gist. Compact catalog of media-query and property hacks.