Web brutalism: stark white ground, pure black ink, structure left visible, type at sizes that stop being typography, and no radius, shadow, gradient or transition anywhere. Craigslist sits at one end of this register. The fashion and gallery sites that set 200px Helvetica over Times body copy sit at the other. Both are refusing the same thing.
The refusal is the aesthetic. That distinction is the whole point of this repository, because a theme in this register and a stylesheet somebody abandoned halfway look similar in a screenshot and are nothing alike underneath. Every absence here is a token set to zero or a rule written down with its cost stated, rather than a value nobody got around to choosing.
The screenshot above is demo/index.html, a fictional status page called Plainstate. Open it live, or clone the repo and open the file. There is no build step, no framework, no node_modules, and no server to start.
The demo declares no color of its own. It links tokens/tokens.css, components/components.css and components/structure.css and reads every value from them, so it stays honest about what the theme actually produces.
There is a second page worth opening: components/index.html renders all six components and the exposure layer with the markup to copy.
A visual style is a set of coordinates, not a mood. This theme sits at one point in the creative direction framework, which sets brand direction on four axes.
These four positions are not an interpretation. The framework's own showcase already carries this register as an archetype, filed under the reference brands you would expect: Bloomberg Terminal, Drudge Report, Hacker News, and the Brutalist Websites archive. The coordinates below are that archetype's, read off the framework rather than argued for here.
| Axis | Position | What the position buys |
|---|---|---|
| Tone register | Provocative | Permission to narrow the audience on purpose. A reader who wants to be reassured leaves on the first screen, and that is the position working rather than failing. |
| Aesthetic philosophy | Expressive Maximalist | Volume, spent on scale and contrast instead of on quantity. The page is loud with two colors and one typeface, which is the discipline this position asks for. |
| Audience relationship | Peer | The browser's own link colors and the reader's own conventions. Peer is why this register keeps the underline rather than teaching a house style. |
| Sensory ambition | Functional | Aesthetics answering to clarity. The table stays a table and the numbers keep their decimals. |
Those four position names are the exact strings the framework uses. If you want the long version of any of them, the links go to the position page.
Two of these are worth reading together. Expressive Maximalist with Functional is the rarer pairing, and the framework documents it directly: the maximalism is the dialect the audience already speaks, deployed in service of getting a job done rather than in service of resonance.
Clone once, then pick the path that matches your stack.
git clone --depth 1 https://github.com/rampstackco/brutalist-web-themePlain CSS. Copy the two directories and link them in order. This is the whole install.
cp -r brutalist-web-theme/tokens brutalist-web-theme/components your-project/styles/<link rel="stylesheet" href="/styles/tokens/tokens.css" />
<link rel="stylesheet" href="/styles/components/components.css" />
<link rel="stylesheet" href="/styles/components/structure.css" />The third line is optional in the sense that nothing breaks without it. It is not optional in the sense that the exposure layer is most of what makes this register recognizable, which is covered below.
Tailwind v4. One import. theme.css pulls in tokens.css and maps it onto Tailwind's theme namespaces, so you get bg-bw-ground, border-bw, text-bw-mega, grid-cols-bw.
@import "tailwindcss";
@import "./styles/tokens/theme.css";Tailwind v3. Load the tokens in your stylesheet, then register the preset.
@import "./styles/tokens/tokens.css";
@tailwind base;
@tailwind components;
@tailwind utilities;// tailwind.config.js
module.exports = {
presets: [require("./styles/tokens/preset.js")],
content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
};Already on shadcn/ui. The tokens are namespaced --bw-* so they will not clobber yours. Bridge the two in your global stylesheet and shadcn's components inherit the register:
:root {
--background: var(--bw-ground);
--foreground: var(--bw-ink);
--card: var(--bw-ground);
--primary: var(--bw-ink);
--primary-foreground: var(--bw-ground);
--destructive: var(--bw-alarm);
--border: var(--bw-ink);
--ring: var(--bw-focus-color);
--radius: var(--bw-radius);
}Expect to remove shadcn's shadow utilities by hand. The bridge above can hand it a radius of zero, and there is no token to hand it for elevation, because this theme declares none.
tokens/tokens.css is the single source of truth. Every literal value in the theme appears there exactly once; theme.css and preset.js hold no values of their own and point back at it with var(). Change a hex there and the demo, the components, the exposure layer and both Tailwind adapters follow.
The file is annotated. Each group of tokens carries a comment naming the axis the choice serves and why, so the ground and ink group explains itself:
/* GROUND AND INK
Aesthetic philosophy axis. Pure white and pure black, the two values a
browser reaches for when nobody has told it otherwise. Softer pairings
are the sound of a design system apologizing for the contrast it needs. */tokens/theme.css carries a verification header rather than a promise. Every namespace in the v4 adapter was compiled and the output declarations were read back, on three Tailwind versions, with controls that prove which lines produced which rules. Two of the twelve namespaces it uses are undocumented, and the header says which two, what they compile to, and what to switch to if a future release drops them.
CUSTOMIZE.md is the half-finished layer, and it is half-finished deliberately. It documents retheming as axis moves rather than as a color picker: pick an axis, move along it, change the two or three tokens that carry the move. One move is worked through with before and after values. Two more are sketched so the format is obvious enough to finish yourself.
Tokens can say how thick a rule is. They cannot say that the grid is drawn instead of implied, that a table is left looking like a table, or that the page carries a ticker. Those decisions live in components/structure.css.
It holds four treatments: the drawn grid, whose column lines are painted from the same token the track list uses and show in the tracks nothing occupies; the raw table, with every cell edge drawn and figures set tabular; the ticker, which is the one piece of motion this register owns; and the oversized type steps, which are fluid so a headline is a statement on a desktop and still inside the viewport on a phone.
It also states the anti-polish rules as rules. Six refusals, each with the cost it accepts written next to it: no radius, no shadow, no transition, no gradient, browser link colors kept, structure left visible. A register defined only by what it looks like gets copied badly, and the rules are there so the next person copies the argument rather than the screenshot.
What is not refused: focus rings, 44px touch targets, measured contrast, real headings, and markup that works with JavaScript disabled. The register is hostile to the reader's expectations about polish. It is not hostile to readers who navigate by keyboard, tap with a thumb, or need the contrast to read at all. Dropping those does not make a page more brutalist, it makes it broken, and broken is a different thing that happens to photograph the same.
Consuming this from a Claude skill. The design-standards skill asks for a project's design tokens as a required input and offers to define a working set when none exist. Point it at tokens/tokens.css instead. The file covers every category the skill asks for, in the order it asks: color with measured contrast ratios, spacing scale, type scale, radius. Every text pairing in it clears WCAG AA, and the ratios are in the comments so the skill's contrast pass has nothing left to compute. The radius category is present and its value is zero, which is an answer rather than a gap.
Neobrutalism and brutalist web design share a word root and almost nothing else. They are separate archetypes with opposite intentions, and the shared syllable is the only reason anyone confuses them.
Neobrutalism is friendly. It borrows this register's honesty about structure, the visible border and the shadow that admits it is a shadow, and then spends real effort on legibility, touch targets, saturated flat color and a press animation that feels good. A reader is meant to enjoy using it.
What this repo packages refuses that enjoyment as its argument. The ground is white because white is the default, the type is the machine's own, the links are the browser's blue, and nothing eases. The reader is not being welcomed. That is disqualifying for the soft register, which is the clearest way to tell the two apart.
The vocabulary follows the same split. Offset shadows, saturated fills, tilt, stickers, hand drawn marks and the press gesture belong to the sibling and are its language rather than shared property. Nothing in this repository borrows them, and a page that mixes both is not composing two registers, it is undoing the position of each.
If you came here looking for the friendly one, it is neobrutalism-theme, which borrows this register's honesty about structure and then spends real effort on legibility, touch targets and contrast. That effort is what disqualifies it from being this, and its own adjacency section says so from the other side.
MIT. See LICENSE. Use it commercially, fork it, rename the tokens, ship it. No attribution required.
Issues and pull requests are welcome here. For questions, ideas, and anything conversational, use the discussions on the claude-skills repo, which is where all discussion for these repos lives.
