Nguyên liệu · 1.404 từ · 6 phút đọc

CSS: Separating Style From Structure

Cascading Style Sheets ended the era of font tags and gave designers a real language for visual presentation.

#TL;DR

By 1996, the web was ugly — and getting uglier. Designers had no tools except HTML itself, so they abused <font> tags, spacer GIFs, and deeply nested <table> layouts to control how pages looked. Visual design was entangled with document structure, and every change to one risked breaking the other. Håkon Wium Lie and Bert Bos proposed a radical separation: HTML describes what content is, and a new language — Cascading Style Sheets — describes how it looks. CSS gave the web colors, fonts, layouts, and eventually animations, all without touching the HTML. The “cascade” — a system of layered rules with specificity-based conflict resolution — was confusing, powerful, and ultimately the right abstraction. CSS took years to gain full browser support and decades to become truly capable, but the principle it established — content and presentation are separate concerns — reshaped how software interfaces are built.

#The Font Tag Problem

The early web had no design tools. HTML was a structural language — headings, paragraphs, lists — with no mechanism for visual presentation. There was no way to set a font, choose a color, or control spacing.

So the browsers added hacks. Netscape introduced the <font> tag in 1995. It let you change the face, size, and color of text inline:

<font face="Arial" size="4" color="#FF0000">
  <b>Welcome to my homepage!</b>
</font>
<br><br>
<font face="Times" size="2" color="#333333">
  Last updated: January 1997
</font>

Every visual choice was embedded directly in the HTML. Want to change the font across an entire site? Edit every page, every tag, by hand. Want to make a three-column layout? Nest invisible tables inside tables, using transparent 1-pixel GIF images as spacers:

<table border="0" cellpadding="0" cellspacing="0" width="600">
  <tr>
    <td width="150" valign="top">
      <img src="spacer.gif" width="150" height="1">
      <!-- left sidebar -->
    </td>
    <td width="10"><img src="spacer.gif" width="10" height="1"></td>
    <td width="440" valign="top">
      <!-- main content -->
    </td>
  </tr>
</table>

This was the professional standard. Every major website in the mid-1990s was built this way. The HTML was incomprehensible, the pages were fragile, and content was locked inside a visual structure that was nearly impossible to change or reuse.

#”A Proposal That Nobody Wanted”

In October 1994, Håkon Wium Lie — a researcher working with Tim Berners-Lee at CERN, later at W3C — published a proposal called “Cascading HTML Style Sheets.” The core idea: move visual presentation out of HTML and into a separate declaration language.

/* Lie's original proposal used a simpler syntax, but the idea was the same */
h1 {
  font-size: 24pt;
  color: navy;
}

p {
  font-family: Georgia, serif;
  line-height: 1.6;
  margin-bottom: 1em;
}

Write the rule once. It applies everywhere. Change the rule, and every element on every page updates. The HTML stays clean — just structure and semantics. The CSS handles everything visual.

Lie wasn’t the only one with this idea. There were at least a dozen competing proposals — DSSSL, PSL96, JSSS (JavaScript-based stylesheets, backed by Netscape), and others. What Lie’s proposal had that the others didn’t was the cascade: a system for combining multiple stylesheets, from multiple sources, with clear rules for which style wins when they conflict.

Bert Bos, a computer scientist at CWI in Amsterdam who had been working on his own style sheet language, joined forces with Lie. Together they refined the proposal into the CSS Level 1 specification, published as a W3C Recommendation in December 1996.

#The Cascade

The “C” in CSS is its most powerful and most misunderstood feature. The cascade is the algorithm that resolves conflicts when multiple rules target the same element.

Styles come from three sources, in order of increasing priority:

  1. Browser defaults — the user agent stylesheet (why <h1> is big and bold by default)
  2. Author stylesheets — what the web developer writes
  3. User stylesheets — what the end user configures (accessibility overrides, custom fonts)

Within each source, conflicts are resolved by specificity — a scoring system based on how precisely a selector targets an element:

p { color: black; }              /* specificity: 0-0-1 (one element) */
.intro { color: gray; }          /* specificity: 0-1-0 (one class) */
#hero .intro { color: blue; }    /* specificity: 1-1-0 (one id + one class) */
p.intro { color: green; }        /* specificity: 0-1-1 (one class + one element) */

When two rules have equal specificity, the last one wins. When specificity differs, the higher score wins regardless of source order. This system is deterministic and predictable — but it creates a trap. Developers write increasingly specific selectors to override earlier rules, leading to specificity wars where !important becomes the nuclear option:

/* The specificity arms race */
.sidebar .widget .title { color: gray; }
.sidebar .widget .title.active { color: blue; }
.sidebar .widget .title.active:hover { color: red !important; }
/* Months later: "why can't I change this color?" */

The cascade is CSS’s most debated design decision. It enables powerful features — theming, design systems, progressive enhancement — but its complexity is a genuine cost. Every CSS methodology (BEM, OOCSS, CSS Modules, Tailwind) is, at its core, a strategy for managing the cascade.

#The Box Model

Every element in CSS is a rectangular box. The box model defines how that box is sized:

┌──────────────────────────────────────┐
│              margin                  │
│  ┌────────────────────────────────┐  │
│  │           border               │  │
│  │  ┌──────────────────────────┐  │  │
│  │  │        padding           │  │  │
│  │  │  ┌──────────────────┐   │  │  │
│  │  │  │     content      │   │  │  │
│  │  │  │  (width/height)  │   │  │  │
│  │  │  └──────────────────┘   │  │  │
│  │  └──────────────────────────┘  │  │
│  └────────────────────────────────┘  │
└──────────────────────────────────────┘
.card {
  width: 300px;       /* content width */
  padding: 20px;      /* space inside the border */
  border: 1px solid;  /* the visible edge */
  margin: 16px;       /* space outside the border */
  /* total rendered width: 300 + 40 + 2 = 342px */
}

In the original CSS box model, width referred to the content width. Padding and border were added on top. This meant a width: 300px element with padding: 20px was actually 340 pixels wide. Developers hated this — you had to do arithmetic for every layout calculation.

Internet Explorer, famously, got it “wrong” — IE’s box model included padding and border inside the width. When CSS3 introduced box-sizing: border-box, it turned out IE had been right all along. Today, virtually every CSS reset starts with:

*, *::before, *::after {
  box-sizing: border-box;
}

#The Layout Evolution

CSS’s biggest weakness for its first fifteen years was layout. The language could style text, colors, and spacing beautifully, but it couldn’t do the one thing designers needed most: place elements on a page in a flexible, predictable grid.

Floats (CSS1, 1996) — designed for wrapping text around images, hijacked for page layout. Columns were float: left divs with calculated widths. Every float-based layout needed a “clearfix” hack to prevent container collapse. It worked, barely.

Positioning (CSS2, 1998) — position: absolute let you place elements by pixel coordinates, but they were removed from the normal flow, making responsive design impossible.

Flexbox (2012) — the first layout system designed for layout. One-dimensional: arrange items in a row or column, distribute space, align cross-axes. Finally, centering something vertically was one line of CSS:

.container {
  display: flex;
  justify-content: center;
  align-items: center;
}

Grid (2017) — two-dimensional layout. Rows and columns defined explicitly, with items placed into named areas. The layout system designers had been waiting for since 1996:

.page {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "sidebar main aside"
    "footer footer footer";
  min-height: 100vh;
}

It took twenty-one years from CSS1 to CSS Grid. The web’s visual design was held back not by a lack of ambition but by the slow evolution of a language that had to remain backward-compatible with every page ever written.

#The Browser Compatibility War

CSS’s adoption was a disaster of inconsistency. Each browser implemented different subsets of the specification, with different bugs, different defaults, and different interpretations of ambiguous rules.

Internet Explorer 6 (2001) was the worst offender and the most dominant browser. It had a broken box model, no support for PNG transparency, and CSS bugs so numerous that developers catalogued them with names: the Peekaboo bug, the Double Margin bug, the Holly hack. Entire websites existed just to document IE6’s CSS deviations.

The result was a decade of CSS hacks — syntactic tricks that exploited browser parsing differences to serve different styles to different browsers:

/* Conditional comments for IE (real code from real websites) */
.element { width: 300px; }       /* all browsers */
* html .element { width: 310px; } /* IE6 only (the star hack) */

The CSS Zen Garden (2003) — a single HTML page restyled by hundreds of designers using only CSS — proved what the language could do when browsers cooperated. It became the most influential argument for CSS adoption and for browser standards compliance. The message: the problem isn’t CSS. The problem is browsers that don’t implement it correctly.

#What CSS Got Right

CSS is older than most of the developers who use it, and it’s still the only language for styling the web:

  • Separation of concerns — pulling presentation out of HTML was the right architectural choice. It made redesigns possible without rewriting content, enabled themes and dark modes, and allowed the same HTML to look completely different on desktop, tablet, and mobile. This principle influenced software design far beyond the web.
  • Declarative over imperative — CSS says what the result should look like, not how to compute it. display: flex; align-items: center; says “center these” without specifying the centering algorithm. This makes CSS accessible to non-programmers and allows browsers to optimize rendering in ways imperative code can’t.
  • Progressive enhancement — CSS is designed to be ignored when not understood. A browser that doesn’t support Grid falls back to whatever layout the elements had before. This means new features can ship without breaking old browsers — a forward-compatibility guarantee that almost no other language provides.
  • The cascade itself — for all its complexity, the cascade enables things no alternative system matches: layered design tokens, user accessibility overrides, print stylesheets, and responsive design that adapts to screen size with a single media query. The systems that try to avoid the cascade (CSS-in-JS, utility classes) end up reimplementing parts of it.

CSS was dismissed as “not a real programming language” for years. It doesn’t have variables (it does now), it can’t do math (it can now), it can’t handle layout (it handles it beautifully now). The language that was too limited in 1996 has quietly become one of the most sophisticated styling systems ever built — and it still styles every page on the web.