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 with the WorldWideWeb browser. This browser already supports style sheets, multiple fonts, and justification. But there is no universal standard. Every browser vendor treats rendering differently. This inconsistency is the exact problem CSS will eventually solve.
NCSA Mosaic 1.0 introduces the <img> tag. Tim Berners-Lee
actually wanted a different approach for media. Background colors become possible. Styling is still entirely
per-browser. No standard exists yet.
<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 are 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 using <TABLE> for visual layout instead of 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." 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 are written entirely in
JavaScript and applied via the DOM. document.tags.P.marginLeft = 12;. The W3C is not
enthusiastic. Netscape ships it in Navigator 4. They quietly drop it later. Total market adoption is near
zero.
CSS1 is published as an official standard. 68 properties. Colors, fonts, margins, padding, borders, text alignment, and basic display values. This version has 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. Dozens of properties are ignored. Some values do surprising things. But it ships, and web developers start experimenting.
Netscape 4 ships with CSS support. It is famously buggy. Simple things like
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. It is not the spec,
but it is closer. The box model bug remains. position: absolute works more predictably. The
DHTML era
begins: JavaScript starts manipulating CSS properties to animate elements.
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 inherits a non-standard box model from IE5 for Windows. width
includes padding and
border instead of only the content area. It has dozens of layout bugs. It does not update for 5 years. This
version shapes every CSS technique for the next decade.
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.
The star hack (* html .selector) confuses IE6 into applying a rule it
should ignore. 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. This is a non-standard IE-only property that does
nothing
visible but flips the internal flag. It appears in every serious IE-compatible stylesheet.
Floats are the main layout tools of this era. Sidebar left, content right. But
floated elements are removed from the normal flow. Their parent collapses to zero height. The fix is a
generated ::after pseudo-element. It uses
content: ''; display: table; clear: both; on every layout container. This gets copy-pasted into
every stylesheet for 15 years.
Firefox 1.0 launches as the alternative to IE6. Its rendering is faster and its 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 implemented. Browser vendors start using vendor prefixes like -webkit-,
-moz-, and -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, and 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 finally becomes a W3C Recommendation 13 years after its predecessor. This is the same year IE9 ships — the first IE version with serious CSS3 support including SVG, border-radius, and CSS3 selectors.
Chrome 21 ships with display: flex. This is the third and final version
of the Flexbox syntax. It replaces the experimental display: box from 2009. Vertical centering
becomes possible with one line of CSS. Clearing floats slowly stops being the main topic of layout
discussion.
@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 is finally stable across all major browsers. Floats — the layout tool of every major CSS framework — are gradually replaced. Bootstrap 4 will eventually drop its float-based grid for Flexbox in 2018.
IE6 finally reaches end of life. This browser version had slowed down progress 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. This coordinated release avoids the vendor prefix era. Two-dimensional layout is finally native to CSS. Rules and columns are defined in the stylesheet. Items are placed by grid area name. Float-based layout frameworks become unnecessary overnight.
CSS custom properties, --my-color: blue; and
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.
: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.
Examples include form:has(:invalid) and li:has(input:checked). CSS
developers wanted a parent selector since the 1990s. It took 30 years because of performance concerns.
Browsers must recalculate styles when a descendant changes. This requires solving difficult
invalidation problems. Chrome 105, Safari 15.4, and Firefox 121 now support it.
oklch() and the new color spaces.
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.