mostlycss

69 components, built on native HTML and the platform APIs that landed in the last year. No framework, no runtime dependency and no build step unless you want one. 47 of the 69 ship no JavaScript at all.

One link tag

Paste this into an empty file and open it. That is the whole install: a reset, the full token set, 2 skins, light and dark, and every component in the sidebar.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://ui.bodge.tools/dist/css/mostlycss.css">
  </head>
  <body>
    <button class="btn btn-primary" type="button">It already looks like something.</button>
  </body>
</html>

Your CSS wins

Every rule this library ships lives in a layer. Unlayered CSS — which is what you write, and what every WordPress plugin writes — beats a layer at any specificity. That is what lets the class names stay short and unprefixed: a plugin's own .btn overrides ours without touching !important.

@layer ui.reset, ui.tokens, ui.base, ui.components, ui.blocks, ui.utilities;

Skin and mode are two different things

A skin is an identity — a palette, four font families, a set of radii. A mode is light or dark, and only ever those two. They are orthogonal, and neither is required:<html data-skin="sepia" data-theme="dark">. With no skin you get the default one; with no mode you get whatever the reader's system asks for — which is why the button in the top bar is the only theme control on this site.

The contract states what a fourth skin has to define, and the generator writes one for you. This site's own skin is a fourth-party consumer of exactly that contract, with no privileged access to anything.

The platform first, in one example

The sidebar you navigated here with is one element. On a phone it is a popover; on a desktop it is an ordinary sticky column. Not two components, not one component with a mode — the same markup, with three lines of CSS above the breakpoint overriding the user agent's own [popover]:not(:popover-open) { display: none }.

Because it is a real popover, the top layer, Escape, light dismiss and the focus behaviour are the browser's. The button that opens it says popovertarget and nothing else. A drawer built the usual way — a div, a state flag, a click-outside listener, a key handler, a focus trap and a scroll lock — is roughly a hundred lines that can disagree with itself, to arrive at what the platform already had.

One thing it does not give you: Chromium does not currently expose the invoker's aria-expanded. So the shell sets it from the popover's own toggle event — which is also the only way to get it right when the popover closes by light dismiss rather than by the button. That is the shape of nearly every honest answer here: take what the platform gives, check what it actually gives, and write the remainder down.

Where the JavaScript is

In one component. range registers a behaviour because the platform has no single element for a dual-thumb slider. Every other component in the sidebar is a native element with a class on it, and the state you can see — checked, disabled, invalid, pressed, open — is the state the platform already tracks, read directly, never mirrored into a class that can disagree with it.

The vermilion mark under the figure at the top of a component page means that and nothing else.