stylesheet.org is a single stylesheet for building nice-looking sites out of plain, standards compliant HTML. You write the page with the elements HTML already has, and it looks right, with a class only where HTML has no element for the job.
I made it because I was building a lot of sites at once, and I wanted every one of them to start from the same place: one design system, one way of laying things out, and markup that's equally readable to a browser, a screen reader, and me. It grew out of the CSS for an admin system I'd built before, which was already most of the way there, and it has become the stylesheet I reach for on anything new.
The philosophy
The aim is to be standards compliant, and to avoid the sea of divs and classes that most modern front ends turn into. Here's a panel with a title and a Save button, written with Tailwind's utility classes:
<div class="flex items-center justify-between
gap-4 px-6 py-4 bg-white rounded-xl
shadow-md border border-slate-200">
<h2 class="text-xl font-bold tracking-tight
text-slate-900">Title</h2>
<button class="inline-flex items-center px-4
py-2 rounded-lg bg-indigo-600 text-white
font-medium hover:bg-indigo-700">Save</button>
</div>
And here's the same panel with stylesheet.org:
<div class="row panel">
<h2>Title</h2>
<span class="stretch"></span>
<button>Save</button>
</div>
The heading is a heading and the button is a button, and they look like the site's headings and buttons because that's what they are. The only classes are for things HTML has no element for: a row, a panel, and the space that pushes the button to the right. The design lives in the stylesheet, once, instead of being spelled out again on every element of every page.
The basic principles
When a component needs something beyond a bare element, it climbs a ladder, and stops at the first rung that works:
- A plain element. A
button, atable, adetails, aprogress. Most things stop here. - An attribute the HTML needs anyway. A switch is a checkbox with
role="switch", which a screen reader needs to hear, so the stylesheet styles that. Something loading hasaria-busy="true", so that's what shows the spinner. - A class, where HTML has no element and no attribute for the job:
.row,.grid,.card,.tabset. - JavaScript, only where HTML and CSS genuinely can't do it, and only for the part that needs it. A toast's animation is pure CSS, and the script just adds the element and removes it after a timeout.
Custom tags are never used, because they mean nothing to a browser or a screen reader, and IDs are avoided wherever HTML allows. Every component's page says plainly whether it needs a class or JavaScript.
The components
The components are grouped into eight sections, mapped out across 109 component pages and being filled in as I go:
| Section | Components | For example |
|---|---|---|
| Structure | 9 | Page, section, layout, sidebar layout |
| Navigation | 10 | Navigation bar, breadcrumb, tabset, table of contents |
| Content | 18 | Prose, code blocks, footnotes, margin notes |
| Container | 12 | Card, accordion, callout, carousel |
| Overlay | 10 | Dialog, drawer, popover, tooltip |
| Data | 15 | Table, statistics, timeline, charts |
| Form | 25 | Form layout, switch, range, file upload |
| Feedback | 10 | Progress, meter, busy state, toast |
The overview lists every component with what it's built on, the classes it needs, and whether it needs JavaScript: none, optional, or required. Most need none. A few of them took more thought than the rest.
Tabs
Tabs were the hardest. The first version was a strip of links and a panel, which is really navigation, and nested divs felt weak. So I looked at the ways a set of tabs can be built:
- The ARIA tabs pattern, with
role="tablist",role="tab"androle="tabpanel", is the standard. But it needs an ID on every panel foraria-controls, and JavaScript to switch between them. And arole="tab"without the script behind it is worse for a screen reader than no role at all. - A
navof links witharia-currentis right when each tab is a different page, but not for switching content within one. - Details elements sharing a
nameopen one at a time, which is exactly an accordion, but they don't make a strip. Restyling them as tabs neededdisplay: contents, which broke the ordering in Chrome. - Radio buttons, with CSS's
:has()showing the panel after the checked one, need no JavaScript and no IDs, and they're a real form control: a native radio group gets arrow keys, and "1 of 3", for free.
The radio buttons won. Each label holds a radio, and its panel is whatever element comes straight after it, with no class:
<div class="tabset">
<label><input type="radio" name="view" checked> Overview</label>
<div>…the Overview content…</div>
<label><input type="radio" name="view"> Rules</label>
<div>…the Rules content…</div>
</div>
.tabset > label:has(input:checked) { background: var(--accent); }
.tabset > label:has(input) + * { order: 2; }
.tabset > label:has(input:not(:checked)) + * { display: none; }
A few details came out of using it for real. The tabs and panels sit side by side in the markup rather than in two lists, so a set can have any number of tabs, straight from a database, with no CSS for each position. A script can read the selection from the radio's change event. A tabset inside another goes inside a form, which scopes the radio names, so every set can use the same name. And there's exactly one class, .tabset, on a plain div, because nothing in HTML means "a set of tabs". The trade-off is that a screen reader announces them as radio buttons rather than tabs, which is honest about what they are.
Accordions
An accordion is the opposite case: HTML already has it. Each row is a details with a summary, giving them the same name makes only one open at a time, and the .accordion class just joins the rows into one bordered block. The browser owns the open and closed state, so there's no JavaScript at all.
Cards
.card is purely styling, so it can go on whatever element the content really is: an article, a section, a p. A card that's a link all over has two forms. The card can be the a itself, which is simple but means it can't contain any other links, or a .stretch-link can stretch the heading's link over the whole card, which allows other links inside, at the cost of the text not being selectable.
Switches, dialogs and busy states
These are the attribute rung of the ladder. A switch is <input type="checkbox" role="switch">, with no class. Anything loading gets aria-busy="true", which is what tells a screen reader to wait, and the stylesheet shows the spinner from the same attribute. A dialog is a dialog opened by a button with commandfor, with no JavaScript. That does bring an ID back, because commandfor names the dialog it opens, so the dialogs are where the rule against IDs bends.
The layout system
Layout uses the same rule as the user interface library in Angular best practices for large teams: components carry no outer margin, and spacing is always the parent's job. Everything is laid out in rows, columns and grids, and each of them spaces its immediate children with a single gap:
.row, .column { display: flex; gap: var(--gap); }
.row { flex-direction: row; }
.column { flex-direction: column; }
.row > *, .column > * { flex: 0 0 auto; min-width: 0; min-height: 0; }
.stretch { flex: 1 1 0; }
.grid { display: grid; gap: var(--gap); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
Children keep their natural size unless they're told to .stretch into the space that's left, and an empty .stretch is a spacer, as in the panel above. Because the spacing comes from the parent, anything put in a row is spaced correctly, including components the stylesheet has never heard of, like a React component or a custom widget. Sizes that are genuinely one-off go in an inline style.
The standard page layout
A page has one shape. The body holds a header for the site's navigation, any number of nav elements for the section's navigation, one main, and a footer:
<body>
<header> … site navigation … </header>
<nav> … section navigation … </nav>
<main> … the page itself … </main>
<footer> … site footer … </footer>
</body>
Inside main, every heading opens a section of its own, with the heading as its first child, and the sections nest to mirror the outline of the page, an h2 for each section, an h3 for each subsection inside it, and so on:
<main>
<h1>Page title</h1>
<section>
<h2>A section</h2>
<section>
<h3>A subsection</h3>
</section>
</section>
</main>
At first I wasn't sure section tags were worth having at all. What settled it is that a heading belongs to the content under it, and wrapping the two together in a section gives the CSS complete control: a section can take a background without any extra markup, and the vertical rhythm comes from the tree, with more space between top-level sections than between subsections. Headings carry no spacing of their own, and nothing on the page needs a class to be spaced correctly.
A full CSS reference
The site also contains a full CSS reference: everything in CSS that ships in browsers today, meaning supported without a prefix in at least one of Chrome, Firefox and Safari, with no drafts. That comes to 1,308 entries, built from the W3C's machine-readable CSS specifications and MDN's browser compatibility data:
- Properties: 580 of them, across 20 themes, from the box model and flexbox to anchor positioning and scroll-driven animation.
- Selectors: the basic selectors, combinators, pseudo-classes and pseudo-elements.
- At-rules: with their descriptors, media features and container features.
- Functions: colour, maths, gradients, transforms, easing and the rest.
- Types: with their units, keywords and named colours.
- A–Z: every entry by name, and a search on the reference's front page.
Every entry has the same table of facts: its syntax as the specification gives it, its initial value, what it applies to, whether it's inherited and animatable, the first browser version that supports it, and a link to the specification.
See also
- Stylesheet.css - the stylesheet on the Development page
- Angular best practices for large teams - the same layout rule, in an Angular user interface library
- My AI workflow in 2026 - building stylesheet.org and the other sites with Sitehoster