Skip to content

Design decisions

This page is the map to why the standard works the way it does. The full reasoning lives in context, in a Why aside on the page each decision affects, so this page collects the short version with a link to where the detail 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 rendered your way over the app’s, wherever you’ve expressed one. The obvious alternatives fall short: styling that lives inside the app leaves your preference behind the moment you leave it, and a flat color palette (the nearest prior art, standard.site, ships four color fields) is 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 constraint across records. An expresses field, string or array, can’t guarantee that uniqueness. Splitting a theme into a separate values record plus a binding costs an extra write and dangles the whole group if the values are ever deleted. A same-collection symlink (default → dark) admits cycles and a privileged “canonical” record. A self-contained record sidesteps all three: it can’t dangle or cycle, and resolving one is a single getRecord. → 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. Requiring all 33 intents would forbid a color-only or type-only look; allowing partial aspects would force a per-intent merge engine to reconcile two sources each filling in some of the variables. Complete-when-present avoids both. → 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 same shape app.bsky.feed.generator uses (a semantic rkey as identifier, a 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. Your baseline look is simply the theme saved at the reserved key default, matched like any other concept, so there’s no separate pointer field to keep in sync. → Schema › profile
  • A data-mode marker plus cascade layers, not scoped placement. Boundaries are declared once at build time, so the viewer’s look, which only arrives later, after sign-in, becomes one appended <style> the cascade routes: no per-scope runtime injection. Placement scoped to a DOM position instead would need a <style> injected at every scope root at the moment the viewer’s look arrives. → Contract › Rule 3
  • A two-form data-mode grammar. Bare expression or disjoint aspect-fragments, never mixed, so ordering can never matter. A value that lists several bare expressions would raise an immediate question (which one wins, and does order matter?) that the two-form grammar avoids by construction. → 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. A colon looks like a natural choice, but it’s a legal rkey character: using it would need convention-only “avoid the colon” rules, reserved words for color/typography, and would still leave some valid keys unaddressable. → 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. A canonical /expressions.json endpoint, a lexicon knownValues enum, or an AT-URI-referenced expression record would each read as authoritative, or add a resolution dependency a plain string doesn’t need. → 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 Mise en Mode book and frozen, since records can’t be tightened once they’re on the network. Likely additions like letterSpacing, density, and motion are deferred rather than pre-included: adding one later is a lossless optional field, while shipping it early would burden every theme with completeness for a value the book itself treats as unsettled. → 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.