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.
The premise
Section titled “The premise”- 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 data model
Section titled “The data model”- The expression is the record key; themes are self-contained. Identity lives on the record key, not in an
expressesfield, because only the key enforces “one theme per concept per repo” — the protocol can’t check a cross-record constraint. Rejected: anexpressesstring/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
aspectscontainer, not top-levelcolor/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 besideaspects, 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 — theapp.bsky.feed.generatorshape (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
defaultkey replaced the olddefaultThemepointer. → Schema › profile
How looks resolve
Section titled “How looks resolve”- A
data-modemarker + 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@scopeplacement. → Contract › Rule 3 - A two-form
data-modegrammar. 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, reservedcolor/typographyas 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@scopefor 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
Naming & growth
Section titled “Naming & growth”- 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 lexiconknownValuesenum, and an AT-URI-referencedexpressionrecord — 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
Where the rest lives
Section titled “Where the rest lives”- 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.