stratum

package module
v0.0.0-...-bc48ed4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 2 Imported by: 0

README

stratum

Live design-system reference

A minimal CSS framework designed to embed into Go projects.

Tokens, a layered cascade, ~20 components, an icon sprite, and two tiny vanilla-JS helpers — vendored as a single Go module. A Go server gets a styled UI by importing the package, mounting one fs.FS, and linking one stylesheet. No npm, no build step, no preprocessor.

Native CSS Custom Properties + @layer + a sprinkle of color-mix(). Works in any browser that ships @layer (Chrome 99 / Firefox 97 / Safari 15.4 — 2022+).

Why it exists

Most CSS frameworks are either heavy (Bootstrap, Tailwind — with their own toolchain) or drop-in single-stylesheet kits (Pico, Simple.css) that stop short of components. Stratum sits in between: ~20 tiny components, plus tokens and utilities, sized for an internal app or a side-project where shipping a Node toolchain alongside a single Go binary is wrong. The design-system page is the spec — if you can't build a new page out of what's documented there, the answer is to add a primitive, not a page-specific class.

Install (Go)

import "github.com/wikilayer/stratum"

Mount the static FS under /static/ (use it directly, or layer your own files on top via fs.FS composition):

http.Handle("/static/*", http.StripPrefix("/static/",
    http.FileServer(http.FS(stratum.Static))))

Link from your base template:

<link rel="stylesheet" href="/static/style.css">
<script src="/static/theme.js" defer></script>
<script src="/static/copy.js" defer></script>

That's it. Everything else is plain HTML + classes.

Cascade order (@layer)

reset → tokens → base → layout → components → utilities

Declared in static/style.css. Every rule in the framework is wrapped in its layer. Layers later in the list win in the cascade — so utilities can always override a component, components can always override base typography, and so on.

Tokens

All colours, spacing, radii, type sizes, shadows, durations live in static/css/base/tokens.css as CSS variables. There's a light default and a html[data-theme="dark"] override; prefers-color-scheme: dark falls through to the same dark values when no theme is explicitly chosen.

Hardcoding a hex inside a component is a smell. Add a token first.

Highlights:

Variable Meaning
--bg, --bg-elevated, --bg-subtle, --bg-hover, --bg-active Surface levels
--fg, --fg-muted, --fg-subtle, --fg-on-accent Text levels
--border, --border-muted Two contrast levels for separators
--accent, --accent-hover, --accent-bg Brand accent colour set
--danger*, --success-*, --callout-* Semantic colour roles
--text-xs … --text-4xl Type scale (1.25 ratio, body 17px)
--space-1 … --space-8 4-step spacing scale, in rem
--radius-sm/md/lg/pill Corner radii
--header-h, --aside-w, --content-max Page-shell sizes
--duration-fast, --easing-out Motion

Base layer

Sits above tokens, below components. Three files:

  • reset.cssbox-sizing: border-box, zero default margins on text blocks, font: inherit on form controls, display: block on media. Nothing more.
  • typography.css — body font, six heading levels with a 1.25-ratio scale, paragraphs with bottom margin, <code>/<pre>/<blockquote>. Headings are clean by default; opt into the Wikipedia / MDN baseline rule per element with .h-rule, or globally inside an <article>.
  • layout.css — page chrome (sticky body > header, .brand, .content frame, optional <aside> rail). Plus reusable column primitives (.column*) and .split for two-column pages.

Layout primitives

.column, .column-narrow, .column-wide

Reading-width columns, centred. Pick one when a page wants to constrain its content to a single readable line.

<section class="column-narrow">…</section>     <!-- 32em — auth, dialogs -->
<section class="column">…</section>             <!-- 38em — onboarding, prose -->
<article class="column-wide">…</article>        <!-- 720px — legal docs -->
.split, .split-side, .split-main

Two-column page with a fixed-width side rail and a fluid main column. Below 720px collapses to one stack with the side moved on top.

<div class="split">
  <aside class="split-side">…</aside>
  <section class="split-main">…</section>
</div>
nav.leftnav

Sticky left rail next to <main>, mirror of the right <aside> rail. Use for dashboard navigation, section links, anything always-visible left of the content. Stacks above main on mobile. .rail-section works inside either rail.

<div class="content">
  <nav class="leftnav">
    <section class="rail-section">
      <h3 class="eyebrow">Recent</h3>
      <ul>…</ul>
    </section>
  </nav>
  <main>…</main>
  <aside>…</aside>
</div>

Components

Every component lives in its own file under static/css/components/. Markup conventions below.

.button
<a class="button button-primary"  href="…">…</a>
<a class="button button-secondary" href="…">…</a>
<a class="button button-ghost"     href="…">…</a>
<button class="button button-danger">…</button>
<a class="button button-primary button-sm" href="…">…</a>

Modifiers stack. Use <a> for navigation, <button> for actions. Icons go inline before the label.

.row, .field, .input, .choice-group

Form primitives.

.row is a label-on-top stack. .field is the same but targets a single isolated field with a tighter label.

<form class="stack">
  <div class="row">
    <label class="row-label" for="x">Display name</label>
    <input id="x" type="text" required>
  </div>
  <button class="button button-primary">Save</button>
</form>

.row-inline-list / .row-inline render a read-only <dl> of Label: value rows.

.choice-group / .choice / .choice-name / .choice-desc is a fieldset of mutually-exclusive radios, each option a clickable card with icon + name + description. Selected state uses :has(input:checked).

.form-layout / .form-layout-main / .form-layout-aside is a generic two-column form (main + 200px sidebar slot) — used on /settings for the avatar.

Inputs / selects inside .row and .field get the framework's text-input look automatically. Custom <select> chevron is painted with two CSS gradients so it follows the theme.

.tabs

Pure-CSS, radio-driven. Up to four tabs out of the box; extend the selector pairs in tabs.css for more.

<div class="tabs">
  <input type="radio" id="tab-1" name="my-tabs" checked>
  <input type="radio" id="tab-2" name="my-tabs">
  <div class="tabs-bar" role="tablist">
    <label for="tab-1" role="tab">First</label>
    <label for="tab-2" role="tab">Second</label>
  </div>
  <div class="tab-panel-1" role="tabpanel">…</div>
  <div class="tab-panel-2" role="tabpanel">…</div>
</div>
.alert

Inline message block. Variants: .alert-error, .alert-success. Always left-aligned (won't inherit .text-center). Use .alert-success as a one-shot banner after a redirect — same shape, no separate primitive needed.

.modal

Dialog box on top of a backdrop. Built on the native <dialog> element — ESC closes, click on backdrop closes (wired by modal.js), focus trapped, no scroll-lock gymnastics. Three sub-zones: header / body / footer.

<button class="button" data-modal-open="add-member">+ Add member</button>

<dialog id="add-member" class="modal">
  <header class="modal-header">
    <p class="modal-title">Add member</p>
    <button class="modal-close" data-modal-close aria-label="Close">×</button>
  </header>
  <div class="modal-body">…form fields…</div>
  <footer class="modal-footer">
    <button class="button" data-modal-close>Cancel</button>
    <button class="button button-primary">Add</button>
  </footer>
</dialog>

data-modal-open="ID" on any clickable opens the dialog with that id. data-modal-close on any clickable inside the dialog closes it. Load /static/modal.js once on the page.

.callout

GitHub-flavoured callouts (> [!NOTE] rendered from markdown). Variants: .callout-note, .callout-tip, .callout-important, .callout-warning, .callout-caution. Wrap the title in .callout-title with an icon.

.map, .map-caption

Embedded location iframe (rendered from > [!MAP] blockquotes in markdown). The container clips a single full-width <iframe> to a rounded card; an optional .map-caption strip sits below.

<div class="map">
  <iframe src="https://maps.google.com/maps?q=51.4779,0.0015&z=15&output=embed" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>
  <div class="map-caption">Royal Observatory, Greenwich</div>
</div>

Bordered surface for grouped content. .card-grid lays cards in a responsive grid. Add .card-link to an <a> that wraps the whole card — it gets the flex-row layout, link colour, and accent border on hover.

<ul class="card-grid">
  <li><a class="card card-link" href="…"><svg class="icon">…</svg> Title</a></li>
</ul>
.avatar, .avatar-lg

Round chip with initials, or <img class="avatar"> for a Gravatar.

.tag, .tag-cN, .tag-row

Small coloured label (a node tag, a status marker). .tag is the shape; .tag-cN (N = 0..7) picks one of 8 palette hues. The hue is the ink for text and border, the fill is a themed tint of it, so a tag stays legible in both themes with no per-tag override. Wrap a trailing cluster in .tag-row to sit it beside a heading.

<h3>How we write code<span class="tag-row">
  <span class="tag tag-c0">for-agent</span>
  <span class="tag tag-c1">checklist</span>
</span></h3>
.url-pill

Copy-this-code block — a <code> value paired with a compact .copy-btn (wired by copy.js). The name says URL because that's the canonical use, but the body is a generic <code> and works for any short string the user needs to paste somewhere.

<div class="url-pill">
  <code>https://example.com/api</code>
  <button class="copy-btn" type="button"
          data-label-copy="Copy" data-label-copied="Copied">Copy</button>
</div>
.page-head, .page-tabs

Title plus an icon-tab strip on the same baseline. The active tab's underline replaces the page-head's bottom border locally. Ideal for "this page has a Content / History / Settings switcher" layouts.

.breadcrumb
<nav class="breadcrumb">
  <a href="/">home</a><span class="sep">/</span>
  <a href="/x">Section</a><span class="sep">/</span>
  <span class="current">Current</span>
</nav>
.menu-host, .menu-toggle, .dropdown

Click-to-reveal menu built on <details>. The header avatar dropdown is the canonical use; the panel inside hosts .dropdown .item rows and optional .dropdown-section blocks.

<details class="menu-host">
  <summary class="menu-toggle">…</summary>
  <div class="dropdown" role="menu">
    <a class="item" href="…">Settings</a>
    <div class="dropdown-section">
      <div class="label">Theme</div>
      <div class="segmented">…</div>
    </div>
  </div>
</details>
.segmented

Horizontal radio-style picker (button-group). Use aria-pressed="true" to mark the active option.

.feed, .feed-item, .feed-time, .feed-actor, .feed-action, .feed-target

Flat list of timestamped activity entries. .feed-action carries a coloured tag — variants by suffix (feed-action-INSERT, …-UPDATE, …-DELETE; rename in your CSS if your domain uses different verbs). .feed-target-gone strikes through a target whose object no longer exists.

.data-table, .meta

Generic admin / settings table. .meta is the same idea but for a <dl> of read-only Label: value facts.

Right-rail widgets — table of contents with active-link highlight, recent-activity list with title + relative time.

.block, .block-id

Addressable section inside long-form content. <section class="block" id="…"> carries scroll-margin-top matching the sticky header; .block-id is the gutter #-link rendered by markdown post-processing.

Utilities

Single-purpose helpers in static/css/utilities.css. Add new ones sparingly — most "I need this in two places" patterns belong in a component.

Class Effect
.muted secondary foreground colour
.empty italic + muted, for "no data" placeholders
.fine-print small + muted text, for disclaimers under forms
.text-center, .text-left, .text-right text-align
.stack > * + * vertical rhythm via margin-top (lobotomized owl)
.cluster horizontal flex with gap and wrap
.inline-form display: inline for inline POST forms
.icon 1em-square inline SVG, follows currentColor
.h-rule thin baseline rule under a heading (Wikipedia / MDN look). Headings are clean by default — opt in per <h1>/<h2>. Applies automatically to all h1/h2 inside an <article> so rendered markdown gets the rule for free
.eyebrow small uppercase muted label — sidebar section titles, dropdown labels, and other "small caps over a list" places

Icons

The sprite at static/icons.svg is regenerated from static/icons.txt. Manifest format: one name per line (fetched from Lucide via unpkg), or name | url to fetch from any URL (used for brand icons Lucide doesn't ship — Simple Icons CC0).

make icons-sync

Rendering:

<svg class="icon" aria-hidden="true">
  <use href="/static/icons.svg#globe"/>
</svg>

The sprite uses stroke="currentColor" for outline icons and fill for solid brand glyphs, so colour follows the surrounding text.

License: outline icons are Lucide / Feather (ISC + MIT subset). Brand icons are Simple Icons (CC0). Full text lives in static/icons.LICENSE.txt and ships with the sprite.

JavaScript helpers

  • theme.js — wires every [data-theme-set] button. Writes a theme cookie (light / dark / blank for auto), applies data-theme on <html> immediately, updates aria-pressed on the buttons. Server is responsible for reading the cookie on each request and emitting <html data-theme="…"> on the initial render to avoid flash.
  • copy.js — wires every .copy-btn inside a .url-pill. Copies the <code> value to clipboard, swaps the button label to data-label-copied, then back after 1500ms. Localised labels stay in templates, not in JS.
  • modal.js — wires [data-modal-open="ID"] triggers and [data-modal-close] close-buttons. Uses native <dialog>.showModal() / .close(); adds click-on-backdrop-to-close on top of what the platform gives you for free.

All three are zero-dependency, ~30 lines each, safe to load with defer.

Adding a component

  1. New file in static/css/components/<name>.css. Wrap rules in @layer components { … }. Keep selectors flat — no id selectors, no deep nesting.
  2. @import it from static/style.css in the components block.
  3. Document the markup convention in this README under Components.
  4. Add a live example to design-system/index.html.
  5. Re-check imports use generic class names. If the name only fits one page of one app, you missed an abstraction — pick a shape-based or role-based name instead.

Adding a token

static/css/base/tokens.css. Define under :root first, then add the dark-theme override in html[data-theme="dark"] and (when the value differs from light) the matching prefers-color-scheme: dark block. Reference via var(--…) everywhere else.

Design system

design-system/index.html is a standalone reference — opens with file://, no server needed. It lives next to the framework so any change to a primitive can be sanity-checked alongside the docs in seconds.

make design-system   # opens it in the default browser

If you add a primitive and don't add an example here, future-you will reinvent it. Update the page.

Constraints

Things this framework deliberately doesn't do:

  • No preprocessors. Native CSS only. If you reach for Sass, the rule isn't generic enough.
  • No build step. A single static folder, served as-is.
  • No JavaScript framework. Two tiny .js files, both vanilla, both optional.
  • No !important, no id selectors, no deep nesting. Specificity stays flat so utilities reliably override components.
  • No page-specific classes. If a name only fits one page (.login, .profile-grid, .consent-actions), it's the wrong abstraction. Compose pages from the primitives above.

Layout

stratum/
├── stratum.go              ← the only Go file: exports Static fs.FS
├── go.mod
├── Makefile                ← icons-sync, design-system targets
├── README.md
├── CLAUDE.md               ← notes for assistants working on this package
├── static/
│   ├── style.css           ← entry: @layer order + @imports
│   ├── icons.{svg,txt,LICENSE.txt}
│   ├── theme.js
│   ├── copy.js
│   ├── modal.js
│   └── css/
│       ├── base/{tokens,reset,typography,layout}.css
│       ├── components/*.css
│       └── utilities.css
├── design-system/
│   └── index.html          ← live reference, file://-friendly
└── cmd/
    └── icons/              ← icon-sprite generator (Lucide + Simple Icons)

Documentation

Overview

Package stratum is a minimal CSS framework designed to embed into Go projects. Tokens, a layered cascade, ~20 components, an icon sprite, and two tiny vanilla-JS helpers — vendored as a Go module so a server gets a styled UI by mounting one fs.FS and linking one stylesheet. No npm, no build step, no preprocessor.

Wire it up:

import "github.com/wikilayer/stratum"

http.Handle("/static/*", http.StripPrefix("/static/",
    http.FileServer(http.FS(stratum.Static))))

Then link /static/style.css from your template. See README and the design-system reference at https://wikilayer.github.io/stratum/.

Index

Constants

View Source
const CSSLayerOrder = "@layer reset, tokens, base, layout, components, utilities;"

CSSLayerOrder is the @layer declaration that needs to ship before any of the CSSAssets so the cascade resolves deterministically. Drop it into a tiny inline <style> in <head> to avoid taking another network round-trip just to declare layer precedence.

Variables

View Source
var CSSAssets = []string{
	"css/base/tokens.css",
	"css/base/reset.css",
	"css/base/typography.css",
	"css/base/layout.css",
	"css/components/button.css",
	"css/components/field.css",
	"css/components/avatar.css",
	"css/components/tag.css",
	"css/components/url-pill.css",
	"css/components/callout.css",
	"css/components/map.css",
	"css/components/card.css",
	"css/components/table.css",
	"css/components/breadcrumb.css",
	"css/components/dropdown.css",
	"css/components/rail.css",
	"css/components/article.css",
	"css/components/page-head.css",
	"css/components/tabs.css",
	"css/components/alert.css",
	"css/components/modal.css",
	"css/components/feed.css",
	"css/components/entry.css",
	"css/utilities.css",
}

CSSAssets lists every CSS file in the bundle in cascade order. Hosts can either link static/style.css (one request that chains N more @import requests, all render-blocking and serial) or iterate this slice to emit N parallel <link rel="stylesheet"> tags — the latter is dramatically faster on cold loads with HTTP/2 multiplexing.

When iterating, also inline CSSLayerOrder in a <style> block before the first <link> so layer precedence is fixed regardless of which stylesheet finishes loading first.

View Source
var Static = mustSub(embedded, "static")

Static is the embedded asset tree rooted at static/. Mount it under /static/ (or wherever) on your HTTP server, and link the produced URLs from your templates: /static/style.css, /static/icons.svg, /static/theme.js, /static/copy.js.

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
cmd
icons command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL