A History of CSS
30 years of styling the web. The proposals nobody accepted, the hacks everyone used, and the features we waited a decade for.
Tim Berners-Lee releases HTML at CERN. There is no way to style a page. The browser decides what everything looks like. Font size, colors, spacing — all hardcoded per implementation. This is fine for sharing documents. Not for building anything that needs to look a particular way.
The NCSA Mosaic browser introduces the <img> tag (over objections
from Berners-Lee, who wanted a different approach). Background colors become possible. Styling is still
entirely per-browser. There is no standard.
<FONT> tag. The web's first styling tool.
Netscape introduces <FONT face="..." size="..." color="...">. It
works. Everyone uses it. It is deeply presentational HTML — structure and style tangled together. The same
tag repeated on every paragraph. Every heading. Every cell in every table. A site with 50 pages means 50
files to update when you change the font.
Netscape ships <CENTER> and <BLINK> as HTML
tags. More importantly, browsers start honoring <TABLE> for visual layout — not just
tabular data. Transparent 1x1 GIFs stuffed into table cells become a technique for pixel-perfect spacing.
This approach will dominate for the next decade.
Håkon Wium Lie publishes a proposal called "Cascading HTML Style Sheets" at the Mosaic and the Web conference in Chicago. The key idea is the cascade: multiple style sheets each contributing rules, with weights to resolve conflicts. Lie is working at CERN with Berners-Lee at the time. This is the document CSS descends from.
Bert Bos, who had been developing a separate style sheet proposal for the Argo browser, contacts Lie. They merge their ideas. The cascading aspect from Lie's proposal survives. The name "Cascading Style Sheets" is settled on. They bring the work to the W3C.
Netscape counters with JavaScript Style Sheets — styles written entirely in
JavaScript, applied via the DOM. document.tags.P.marginLeft = 12;. The W3C is not enthusiastic.
Netscape ships it in Navigator 4, then quietly drops it. Total market adoption: near zero.
CSS1 is published as an official standard. 68 properties. Colors, fonts, margins, padding, borders, text alignment, basic display values. No layout properties — floats and positioning come later. The spec exists. Now someone has to implement it correctly.
Internet Explorer 3 is the first widely used browser to support CSS. The implementation is partial and buggy. The box model behaves differently than the spec. Some properties are ignored. Some values do surprising things. But it ships, and web developers start experimenting.
Navigator 4 adds CSS support that manages to be worse than IE3's.
position: relative breaks layout. Dynamic styles via JavaScript fail silently. Tables with
font-size produce wrong results. The short version: web developers learn to write CSS
defensively, testing in both browsers and assuming nothing works the same way twice.
Internet Explorer 4 ships with meaningfully better CSS support. Still not the spec,
but closer. The box model bug remains. position: absolute works more predictably. The DHTML era
begins — JavaScript manipulating CSS properties to animate elements. CSS is starting to feel like a real
tool.
CSS Level 2 is published. Major additions: the position property,
z-index, @media types (including print), the content
property for generated content, cursor, and the :before and :after
pseudo-elements. CSS now has a proper layout model on paper. In browsers, support is still fragmented.
IE and Netscape implement CSS differently enough that the same stylesheet produces different results in each browser. Web developers respond by writing browser-specific rules, using JavaScript to detect the browser, or simply giving up on CSS and staying with tables. "Best viewed in Internet Explorer 4.0" banners proliferate.
Tasman, the rendering engine in Internet Explorer 5 for Mac, passes the CSS1 test suite better than any browser of its time. It gets the box model right. Developers who test on it are briefly optimistic. Then IE6 for Windows ships, and optimism evaporates.
Internet Explorer 6 achieves
near-total market dominance. It implements the box model wrong: width includes padding and
border instead of only the content area. It has dozens of layout bugs. It does not update for 5 years. For
the next decade, every CSS technique is shaped by what IE6 can and cannot do.
IE6's wrong box model means width calculations are off everywhere. The fix: use a
CSS parsing bug. Writing voice-family: "\"}\""; voice-family: inherit; confuses IE into
stopping early, so you can feed the wrong-box-model IE a faked-up width and all other browsers the real one.
This is what production CSS looks like in 2002.
* html .foo {}. IE6 incorrectly treats * as matching the
root element, so this rule only applies in IE6. Every front-end developer knows this selector by heart. It
becomes the standard way to write IE-only overrides. CSS files develop a two-column structure: the real
rule, then the starred override underneath.
zoom: 1. Not a real property. Works anyway.
IE has a concept called "hasLayout" — an internal flag that controls whether an
element manages its own size and position. Many IE bugs only affect elements without layout. The fix is
triggering hasLayout by applying zoom: 1 — a non-standard IE-only property that does nothing
visible but flips the internal flag. It appears in every serious IE-compatible stylesheet.
The entire layout model of this era is floats. Float a sidebar left, float content
right, done. Except floated elements are removed from the normal flow, so their parent collapses to zero
height. The fix: a generated ::after pseudo-element with
content: ''; display: table; clear: both; after every layout container. Copy-pasted into every
stylesheet for 15 years.
Firefox 1.0 launches as the first serious challenge to IE's dominance in years. Its CSS support is accurate. Selectors work. The box model is correct. Developers who use it feel what CSS was supposed to be. IE still has 90%+ market share, but things are starting to shift.
The W3C starts CSS2.1, a corrected version of CSS2 that reflects what browsers
actually implement rather than what the spec intended. Meanwhile, browsers begin shipping experimental CSS3
features behind vendor prefixes: -webkit-, -moz-, -ms-,
-o-. Every new feature needs four lines. Some features end up prefixed for seven years.
-webkit-border-radius. Rounded corners.
Before this, rounded corners required 3–4 images, extra markup, and nested divs.
Safari 3 ships -webkit-border-radius, and developers use it the same day. Firefox follows with
-moz-border-radius. IE users see square corners for another three years. Designers start asking
for rounded corners constantly.
The W3C splits CSS into separate modules: Selectors, Colors, Backgrounds, Transforms, Transitions, Animations, Flexbox, Grid. Each can advance at its own pace. This is why there is no single "CSS3 release date" — different modules ship in different browsers across different years. CSS development is now continuous, not version-based.
CSS Transitions (-webkit-transition) and CSS Transforms
(-webkit-transform: rotate(45deg)) arrive first in Safari, then Firefox. For the first time,
you can animate a property by changing its value — the browser interpolates the intermediate frames.
JavaScript animators start losing their jobs to a single line of CSS.
CSS2.1, the corrected and normative version of CSS2, finally achieves Recommendation status. It took from 2004 to 2011. The spec now matches what browsers actually implement. This is also the year IE9 ships — the first IE version with serious CSS3 support including SVG, border-radius, and CSS3 selectors.
The first Flexbox draft used display: box with completely different
property names. The spec was revised twice before stabilizing. By 2012, -webkit-flex ships in
Chrome. By 2013, Firefox has it. For the first time, vertical centering is one line of CSS. Clearing floats
stops being a topic of discussion — slowly.
@keyframes and CSS Animations ship.
CSS Animations add @keyframes, animation-duration,
animation-timing-function, animation-iteration-count. Complex multi-step
animations without a single line of JavaScript. jQuery's .animate() function starts looking
obsolete. Vendors still require prefixes: @-webkit-keyframes, @-moz-keyframes.
Flexbox reaches baseline support without vendor prefixes. The clearfix hack starts dying out. Vertical centering becomes easy. Float-based grid systems — which had been the foundation of every major CSS framework — are gradually replaced. Bootstrap 4 will eventually drop its float-based grid for Flexbox in 2018.
Microsoft officially ends support for IE6 on January 12, 2016. It had been unsupported since 2014, but enterprises kept using it. At peak, IE6 had over 90% market share. It held back CSS adoption for a decade. Its death is celebrated by web developers worldwide. Some buy champagne. Some make t-shirts. Some do both.
Chrome 57 and Firefox 52 ship CSS Grid on the same day — a coordinated release specifically to avoid the vendor prefix era repeating. Two-dimensional layout, native to CSS, for the first time. Rows and columns defined in the stylesheet. Items placed by grid area name. A decade of float-based layout frameworks become unnecessary overnight.
CSS Grid snippetCSS custom properties — --my-color: blue;,
color: var(--my-color); — ship in all major browsers. They are real variables that cascade,
inherit, and can be updated at runtime. Sass preprocessors lose their main selling point. Theming with
JavaScript becomes possible without inline styles: change one custom property on :root and
every element that uses it updates.
clamp() widely available. Fluid sizing without media
queries.
clamp(min, preferred, max) picks a value within bounds.
font-size: clamp(1rem, 2.5vw, 2rem) produces fluid typography that scales with the viewport but
never goes below 1rem or above 2rem. No media queries. No JavaScript. The technique that had required a full
fluid typography library fits in one property value.
@layer ships. The cascade gets explicit order control.
@layer lets you group rules into named layers and declare their
priority order: @layer base, components, utilities;. Utilities always win over components,
regardless of specificity. The !important wars — where teams escalate specificity to override
each other's styles — have a structural solution. Third-party library styles can be put in a low-priority
layer and overridden cleanly.
Native CSS nesting ships in Chrome 112, Firefox 117, and Safari 16.5. You can write
.card { color: red; &:hover { color: blue; } } without a preprocessor. Sass users shrug.
PostCSS-nesting maintainers update their READMEs. The main argument for using Sass in 2024 is mostly habit.
Container Queries ship in all major browsers. Style an element based on the size of its container, not the viewport. A card component that reflows its layout when placed in a narrow sidebar without needing to know anything about the page it's in. Developers had been asking for this since media queries were introduced. It took a decade.
Container Queries snippet:has() ships. The parent selector, finally.
- Pure CSS. No JavaScript.
- Parent styles child state.
- Check to see :has() work.
:has() matches an element if it contains a matching descendant:
form:has(:invalid), li:has(input:checked), section:has(h2). CSS
developers had wanted a parent selector since the 1990s. The reason it took 30 years is performance:
browsers must recalculate styles any time a descendant changes, which requires solving difficult
invalidation problems. Chrome 105, Safari 15.4, Firefox 121.
oklch() and the new color spaces.
oklch(), oklch(), display-p3,
lab(), lch() and other wide-gamut color spaces ship in all major browsers. OKLCH's
L (lightness), C (chroma), H (hue) axes are perceptually uniform: adjusting L actually changes perceived
brightness, not just a mathematical value. You can build a design system palette by locking H and C and
varying L. The result is visually consistent where HSL is not.
@starting-style. Entry animations without JavaScript.
@starting-style defines the style an element should transition from
when it first appears in the DOM. Before this, entry animations required JavaScript to add a class after
insertion. Now: @starting-style { opacity: 0; } triggers a CSS transition on the element's
first render. Useful for dialogs, tooltips, and anything that appears dynamically.
if(). Inline conditionals in CSS values.
CSS if() allows conditional values inline:
color: if(style(--variant: danger): red; else: blue). It brings branching logic into CSS values
without needing separate classes or multiple rules. Combined with custom properties, it means a component
can express its own variant logic in CSS. Currently shipping in Chrome Canary and behind flags. Baseline
status: limited.