Skip to content

Design decisions

This is the standard’s decision record: why it’s built the way it is, and the alternatives that were tried and dropped. The full reasoning lives in context — in a Why aside on the page each decision affects — so this page is the map, not the territory. Each entry links to where the thinking lives.

  • Intent-based, user-owned, visitor-first. A look is described by the job a value does (the background of a primary action), saved to your account, and — where you’ve expressed one — rendered your way over the app’s. Rejected: app-owned styling (the status quo, where your preference stays behind), and a flat color palette (the nearest prior art, standard.site, ships four color fields — a palette, not a system that can tell an action from a warning from a surface). → What is a mode?
  • The expression is the record key; themes are self-contained. Identity lives on the record key, not in an expresses field, because only the key enforces “one theme per concept per repo” — the protocol can’t check a cross-record constraint. Rejected: an expresses string/array (no uniqueness), a two-record values + binding normalization (extra write, deletion dangles the group), and a same-collection symlink (cycles, a privileged record). → Schema › the record key
  • Reuse is the owner’s tooling, not the protocol. A look answering to several names is several self-contained copies, kept in sync by the author’s build step — the network stores compiled artifacts, like published CSS, not a normalized database. → Schema › the record key
  • Per-aspect completeness. Aspects are optional, but each present aspect is complete. Rejected: requiring all 33 (forbids color-only/type-only) and partial aspects (forces a per-intent merge engine). → Schema › per-aspect completeness
  • An aspects container, not top-level color/typography. So a renderer emits everything under it as variables without guessing style from metadata. → Schema › aspects
  • Fonts are theme-level assets. They render to document-global @font-face, not scoped variables, so they sit beside aspects, not inside typography. → Schema › typographyAspect
  • Values are plain CSS strings. Format-agnostic, no transform layer; a malformed value just doesn’t render. → Schema › from value to CSS
  • An optional displayName, presentation only. Identity and matching stay on the record key; the display name exists for galleries and pickers — the app.bsky.feed.generator shape (semantic rkey as identifier, display name for presentation). An app never matches or dedupes on it. → Schema › theme
  • A thin profile. Themes are a one-to-many collection (like posts), so they live in their own collection, not folded into the singleton profile; and the reserved default key replaced the old defaultTheme pointer. → Schema › profile
  • A data-mode marker + cascade layers, not scoped placement. Boundaries are declared once at build time so the late-arriving viewer look is one appended <style> the cascade routes — no per-scope runtime injection. Rejected: position-based @scope placement. → Contract › Rule 3
  • A two-form data-mode grammar. Bare expression or disjoint aspect-fragments, never mixed — so ordering can never matter. Rejected: a list of bare expressions (ambiguous order). → Contract › Rule 4
  • The aspect delimiter is / — a character outside the record-key charset. No expression can ever contain the delimiter, so the parse is unambiguous by construction and no expression name needs reserving. Rejected: : (a legal rkey character — it forced convention-only “avoid the colon” rules, reserved color/typography as words, and left valid-but-unaddressable keys). → Contract › Rule 4
  • :where() keeps specificity flat; layers rank tiers, not peers. So layer order alone decides, and multi-author feeds need containment because the layer can’t separate peers. → Contract › Rule 5
  • @scope for multi-author containment. It bounds matching without adding specificity, so the layer contest (and viewer override) survives inside a scoped post. → Contract › Rule 7
  • Fill-through, not conflict. Each variable comes from the most-preferred source that sets it; the viewer’s guaranteed override is the accountability valve. → Contract › Rule 5
  • Expressions are a convention, not a registry. An opaque string compared for equality; good ids converge by network effect. Rejected: a canonical /expressions.json, a lexicon knownValues enum, and an AT-URI-referenced expression record — each read as authoritative or added a resolution dependency. → Common expressions › a convention, not a registry
  • One reserved word: default. The viewer’s baseline, designated by saving a theme at that key. → Common expressions › the one reserved word
  • Additive growth; a frozen core. The 33 intents were reviewed against the book and frozen (records can’t be tightened once on the network); likely additions (letterSpacing, density, motion) are deferred, not pre-included. → Schema › the core grows
  • Identity, hosting, and the protocol features leveraged — the DID, PDS, Tangled/Netlify setup, blob-stored fonts, firehose discovery, OAuth, and the standard.site lineage — are in the Colophon.