A minimal CSS framework designed to embed into Go projects. This page is the spec — every primitive lives below. If you can't build a new page out of these blocks, add a primitive. Don't introduce a page-specific class.
The app shell: top bar with the brand and profile menu; a content frame underneath; an optional aside on the right of long-form pages.
Top bar — body > header with .brand and a profile menu (here without a signed-in user).
Page content goes here.
Full-window demos — open in a tab and resize across the 720px breakpoint to see the rails collapse:
nav.leftnav + <main> + <aside>.<main> + <aside> (article layout).Six heading levels. Plain by default — opt into a Wikipedia / MDN baseline rule with .h-rule, or apply globally inside an <article> for rendered prose.
With .h-rule on each heading.
A regular paragraph carries the body color. The quick brown fox jumps over the lazy dog.
A .muted paragraph uses the secondary foreground color.
A .fine-print paragraph: small, muted, with underlined inline links — for disclaimers below a CTA. Read our Terms and Privacy Policy.
An .empty paragraph: italic, muted — placeholder when a section has no content.
Inline code with backticks; a regular link in flowing text.
Multi-line fenced code block — copy.js injects a copy button on hover.
func main() {
fmt.Println("hello, world")
}
Single-line code block — no copy button (single-line snippets read as inline).
go run ./cmd/foo
An image inside an <article> gets a thin border and rounded corners, so a screenshot whose background matches the page doesn't blend into it.
One base class, stackable variants. Use <a class="button"> for navigation, <button> for actions. Modifiers: .button-primary, .button-secondary, .button-ghost, .button-danger, .button-sm.
Compact size — .button-sm.
With an icon — <svg class="icon"> alongside the label.
Disabled state.
One .row per labelled control. Wrap a stack of rows in <form class="stack"> to space them; cap with a button.
Stacked form — text, password, email, select, submit.
Input with prefix. .input-group wraps an .input-prefix and a regular <input>. Use when a portion of the value is fixed by the system (a URL prefix, a protocol, a currency) and only the tail is user-editable.
Input with trailing action. Same .input-group wrapper, but the right-hand member is a <button> tagged .input-action instead of a static prefix. Use for forms where typing and submitting belong to one visual unit — search boxes, signup flows, slug-edit rows. The button keeps the .button baseline plus a left divider that fuses it with the input.
Invalid state. Set aria-invalid="true" on the control; the red border survives once focus leaves and the .form-error message below the field carries the wording.
Read-only fact list — .row-inline-list.
Pure-CSS, radio-driven. Convention: radio ids tab-1, tab-2, …; matching panels tab-panel-1, etc. Up to four out of the box.
Panel one. Click a tab to switch.
Panel two.
Panel three.
Inline message blocks. Same shape, color role per variant. Use .alert-success as a one-shot success banner after a redirect — same primitive, no separate "flash" component.
Wrong username or password.
Settings saved.
Native <dialog> with the framework's chrome. data-modal-open="ID" on any clickable opens the dialog with that id; data-modal-close closes the surrounding dialog. ESC closes for free; clicking the backdrop closes via modal.js.
Live autocomplete inside a modal — fake in-memory data, no backend. Floating result list (.row-list-floating on a .row-floating parent) appears only while the input is focused. Row layout is supplied via a <template> tag in the HTML, with [data-slot] attributes marking where each field lands and [data-highlight] marking which fields to wrap in <mark> against the search query. Same JS, any layout. The demo picks a city from a generic list.
GitHub-flavoured callouts rendered from > [!TYPE] blockquotes in markdown. Five variants share the shape and differ by color role.
Note
Background context the reader can skip.
Tip
A recommendation that helps the reader.
Important
Something the reader should not miss.
Warning
Something that often goes wrong.
Caution
Destructive consequence if ignored.
Embedded location iframe rendered from > [!MAP] blockquotes in markdown. The body's first line is lat, lng; remaining lines join into an optional caption.
Round chip with user initials (or a Gravatar via img.avatar). Two sizes — default 30px header chip, .avatar-lg 160px for profile pages.
Coloured initials (.avatar-cN, N = userID % 12). Same hue index across themes — only the chroma/lightness shift between light and dark.
Large size for the profile page.
.avatar-frame wraps an avatar with a corner overflow menu — upload, replace, clear. Toggle the data-busy attribute on the wrapper to swap the trigger icon (default .toggle-default) for a spinner (.toggle-busy, pair with .spin).
Small coloured label. .tag sets the shape, .tag-cN (N = 0..7) picks a palette hue. The hue is the ink for text and border; the fill is a themed tint, so the same tag reads in light and dark.
As a superscript trailing a heading (<sup class="tag-row">), one example per level. The pill scales with the heading it trails.
A .row-list where the trailing element on each row is either a static .row-list-item-meta badge (read-only, e.g. owner row) or a <details> menu whose <summary> shows the current value plus a chevron. Clicking the role text itself opens the menu — bigger hit target than an overflow icon, and it makes "this is changeable" visually obvious. <details name="..."> makes opening one row auto-close the others. The avatar + name are wrapped in .row-list-item-link — an <a> that points at the subject's profile (inherits text colour, underlines on hover, doesn't fight the trailing menu for clicks).
One row in a vertical list of titled excerpts — search hit, blog index, inbox item. Title is a heading (h3 or h4) carrying a link, body is a short paragraph, .entry-meta is the muted timestamp / kind / author line below. Pair with <ol class="stack"> or <ul class="stack"> for the surrounding list (the .stack on a list strips bullets and the user-agent left padding).
Markdown is a widely-supported plain-text format that reads cleanly in any editor.
diff. The tree is the structure, not the markdown text.
Bordered container for grouped content.
Inline <code> with a one-click Copy button. Class is .url-pill for historical reasons — the body is generic, anything you need the user to paste somewhere works (URLs, tokens, prompts).
https://your-app.example/mcp
Title plus an icon-tab strip — eye / clock / gear — for the three sections every wiki/page surface shares: content, history, manage. Active tab gets the accent underline.
Navigation crumbs above the page-head. The last segment is the current location and is non-clickable.
Collapsible sidebar block: <details class="rail-section"> wrapping a <summary class="rail-section-summary"> with an .eyebrow heading and chevron. Works in either page rail (left nav or right aside). Pair with data-rail-section="NAME" + rail.js to remember per-section open/closed state in a cookie. The server reads the cookie and stamps the open attribute at render time so there's no flash.
From static/icons.txt. Run make icons-sync to fetch from Lucide / Simple Icons. Re-paste the resulting <symbol>s into the inline sprite at the top of this file when the manifest changes.