Webstir

CSS Playbook (Minimal Webstir CSS)

Conventions for writing CSS in Webstir templates and SSG sites. The goal is to keep styling predictable and reduce "naming entropy" (lots of one-off classes and tokens).

Principles

  • Tokens-first: use CSS custom properties instead of ad-hoc #hex/px values.
  • Tiny surface area: prefer a few layout building blocks + a few components over "utility everything".
  • Deterministic cascade: use cascade layers so "where do I put this?" is obvious.
  • Accessible defaults: focus rings, readable type, and sensible spacing are part of the system.

The Contract (v0)

This reflects the current SSG template CSS system.

File layout + layers

The SSG starter treats src/frontend/app/app.css as the single entrypoint:

  • It declares a stable @layer order (so overrides are deterministic).
  • It imports every stylesheet under src/frontend/app/styles/** (avoid nested imports inside those files).

Default layer order:

@layer reset, tokens, base, layout, components, features, utilities, overrides;

Core system files (SSG template today):

  • src/frontend/app/styles/reset.css
  • src/frontend/app/styles/tokens.css
  • src/frontend/app/styles/base.css
  • src/frontend/app/styles/layout.css
  • src/frontend/app/styles/components/markdown.css
  • src/frontend/app/styles/components/header.css
  • src/frontend/app/styles/components/buttons.css
  • src/frontend/app/styles/utilities.css

Page-level CSS lives next to the page and uses @layer overrides:

  • src/frontend/pages/home/index.css
  • src/frontend/pages/docs/index.css

Feature CSS is opt-in:

  • src/frontend/app/styles/features/*.css (added by enable commands)
  • app.css imports feature files under the features layer

Layout (classes)

Use a small fixed set of layout building blocks:

  • .ws-container — centered max-width container (uses --ws-container and --ws-container-pad)
  • .ws-stack — vertical layout with gap
  • .ws-cluster — inline row with wrap + gap
  • .ws-grid — responsive grid
  • .ws-sidebar — content + sidebar layout (uses --ws-sidebar above 48rem)

Tune layout building blocks with CSS variables instead of creating more classes:

<div class="ws-stack" style="--ws-gap: var(--ws-space-4)">
  ...
</div>

Components (data-ui + class-based)

SSG template components live in @layer components. Today that includes:

  • data-ui="btn" (preferred) and legacy .button / .button--primary aliases
  • .ws-icon-button (shared icon-only button chrome for menu + search triggers)
  • .ws-drawer-backdrop (shared drawer overlay; uses --ws-drawer-top)
  • .app-header, .app-header__inner, .app-brand, .app-nav, .app-menu (header + nav)
  • main > article sizing for Markdown pages (components/markdown.css)

Button variants implemented today:

  • data-variant="solid|ghost"
  • data-size="sm|lg" (default is the un-set "md" size)
  • data-tone="accent" (used by solid buttons)

Example:

<a data-ui="btn" data-variant="solid" data-tone="accent" href="/about">About</a>

Prefer aria-* and native attributes for state:

  • aria-current="page", aria-expanded="true|false", disabled, etc.

Tokens

Define and customize design values via CSS variables (custom properties).

Common token categories:

  • Color: --ws-bg, --ws-fg, --ws-muted, --ws-border, --ws-accent, --ws-accent-hover, --ws-focus
  • Space: --ws-space-1..8, --ws-gutter (used by containers)
  • Radius: --ws-radius-1..3
  • Type: --ws-font-sans, --ws-font-mono

Layout sizing tokens (use tokens instead of hardcoded px in templates):

  • --ws-container — default content container max width
  • --ws-shell-container — wider “chrome” container (header + docs layout)
  • --ws-article — prose width target (typically ch-based)
  • --ws-docs-sidebar, --ws-docs-toc — docs shell column widths
  • --ws-container-pad — side padding for .ws-container (defaults to --ws-gutter)

Scoping

Docs-only rules must be scoped under a stable attribute:

  • [data-scope="docs"] { ... }

This keeps "docs chrome" styles from leaking into app pages.

Docs layout specifics (SSG template):

  • .docs-layout sets --ws-container: var(--ws-shell-container)
  • .docs-layout__inner defines the grid using --ws-docs-sidebar and --ws-docs-toc
  • Breakpoints: 53.75rem collapses to one column; 68.75rem hides the TOC
  • Styles live in src/frontend/pages/docs/index.css under @layer overrides

SSG Layout Patterns (Current Templates)

App shell (header + main)

The shared header lives in src/frontend/app/app.html and components/header.css:

<header class="app-header">
  <div class="app-header__inner ws-container">...</div>
</header>

app-header__inner widens the container to --ws-shell-container.

Marketing/landing hero (home page)

The home page uses .hero + .hero__inner and is styled in src/frontend/pages/home/index.css under @layer overrides.

Docs layout (docs index + markdown)

Docs pages use a dedicated layout in src/frontend/pages/docs/index.html:

<section class="docs-layout" data-scope="docs">
  <div class="ws-container docs-layout__inner">...</div>
</section>

docs-layout widens the container and drives the sidebar + TOC grid.

Search UI styling

The search feature module can style its UI in one of two ways:

  • Default: injects an inline <style> tag (works even if your template doesn’t ship search CSS).
  • Preferred for SSG templates: styles live in CSS (app/styles/features/search.css) and JS is behavior-only.

When you run webstir enable search, Webstir opts you into CSS-based styling by:

  • Adding src/frontend/app/styles/features/search.css to your app
  • Adding @import "./styles/features/search.css"; to src/frontend/app/app.css
  • Setting data-webstir-search-styles="css" on the <html> element in src/frontend/app/app.html

To opt in manually, set:

<html lang="en" data-webstir-search-styles="css">

How To Make Changes (fast rules)

I need a new spacing/color/etc.

Add or adjust a token, then use it everywhere. Don’t introduce raw values in page CSS unless it’s truly page-specific.

I need a new reusable UI piece

Add a data-ui id and style it once in the component layer, then reuse it. Update the registry (keep the list of allowed ids short).

I need a one-off tweak for a single page

Keep it in that page’s index.css under @layer overrides. If it repeats, promote it into a layout building block/component.

Current data-ui Registry (SSG Template)

  • btn

Shared classes (SSG Template):

  • ws-icon-button
  • ws-drawer-backdrop