Skip to content

Design Conventions

Ernest edited this page Jul 14, 2026 · 13 revisions

The design-side sibling of API Conventions. Where API Conventions states what the code conventions are, this page states what good design looks like in Astryx — and why the defaults are what they are — so designers and engineers can build in compliance and reviewers can evaluate against a shared bar.

This page is content, not process: it states the conventions and marks how each is verified. It gathers what already lives in the token system, component source, and review rubrics — it doesn't invent. The 🟢 checks are run by the Night Watch Component Auditor; the 🟡/🔴 calls are made in Component Hardening Protocol L3. New standards enter via the Component Specification Protocol; L3 decisions roll back up into this page so judgments compound.

Marker Meaning Verified by
🟢 Objective — clear pass/fail, no taste [[Night Watch Component Auditor
🟡 Assisted — tool flags, human confirms Auditor → [[Component Hardening Protocol
🔴 Judgment — proportion, composition, fit [[Component Hardening Protocol

Principles

  • Understand the intention behind a decision. Tokens encode the system's design thinking, so they're a reliable guide — follow the token when in doubt. But they're a guide, not absolute law: a token used in the wrong role is still wrong. Know why a value exists so you can tell the difference.
  • Themes control look, core controls structure. Every visual value references a token; the theme decides what it resolves to.
  • Minimize visuals per state. Every distinct visual for the same state is overhead a user must learn. Reuse an existing representation before inventing a new one.
  • Consistency is scoped to the family first, then the system.
  • Design for every state — rest, hover, focus, active, disabled, loading, and the relevant status/selected states.
  • Guidance over enforcement. Capability, not runtime design guardrails.

Design Foundations — the intention behind token choices

Each foundation pairs a design principle with the token category that expresses it. The defaults aren't arbitrary — they encode these intentions, which are also the rubric for judging whether a custom theme is well-designed. For each: why it matters, then the principle, the smells that signal drift, and what it looks like got right.

Clear relationship hierarchy (Spacing)

Why it matters. Before a word is read, proximity has already told the user what belongs together (Gestalt). Get relationships right and a dense screen still feels organized; get them wrong and no color or border will rescue it. Spacing does structural work — so it must be varied and intentional, not one value everywhere.

Principle

Space is the primary signal for how elements relate. Closely-related things sit tight; separate concerns sit apart; and the gap grows with each level of grouping — label→input < fields in a group < groups in a section < section→section. The 4px grid (--spacing-*, 2px half-steps for optical work) gives enough steps to express these tiers.

Smells
  • 🟢 Off-grid values — anything off the 4px scale (2px half-steps allowed)
  • 🟢 Inverted nesting — a child gap wider than its parent reads backwards
  • 🟡 Monotonous spacing — one value carrying most gaps (≤3 unique values across a component or block) flattens relationships
  • 🟡 Nested cards — containers inside containers instead of spacing to group
Got it right

you can squint at the layout and still see the groupings; gaps step up monotonically with grouping order; every value traces to a spacing token. Tight where related, generous where separate — never uniform.

Consistent vertical rhythm (Size · Density)

Why it matters. Rhythm is what makes a stack of controls feel engineered rather than assembled. Because size and density are set by different props, they drift apart easily. Visual size and touch size are also different jobs: a control can look compact but must still offer a 44px hit area.

Principle

Elements come in two flavors: fixed-height ones use size props (control heights sm 28 / md 32 / lg 36px); variable-height ones (inputs, list rows, menu items) use density props (internal padding). Both must be tuned together so a stack of mixed elements shares one vertical rhythm — a md Button and a default-density input should land on the same height.

Smells
  • 🟢 Off-scale heights — a fixed-height control whose height isn't 28 / 32 / 36
  • 🟢 Small target — an interactive hit area below 20px for high precision pointers
  • 🟡 Size/density mismatch — a fixed-height control and a variable-height one that don't align in the same row
  • 🟡 Cramped padding — density padding too low for the font size, or text flush to the container edge
Got it right

fixed- and variable-height elements in a row share a baseline; size and density were adjusted together. (Open: density padding may deserve a themeable token so rhythm can be tuned system-wide.)

Concentric shapes (Radius · Spacing)

Why it matters. Non-concentric corners are one of the most recognizable tells of unconsidered UI — the inner curve visibly fights the outer. Astryx derives child radii via calc() so concentricity falls out; a theme that sets radius freely breaks it.

Principle

Radius is chosen by an element's role (--radius-inner 4 · -element 8 · -container 12 · -page 28 · -full), and a nested element's radius equals its parent's minus the padding between them (r_inner = r_outer − gap) so curves stay parallel. This ties radius to the spacing scale, not to free choice.

Smells
  • 🟢 Radius off the documented scale — a value outside the role scale
  • 🟢 Non-concentric nestingr_inner ≠ r_outer − gap (a 10px radius in a 12px card with 8px padding should be ~4px)
  • 🟡 Thick accent border on a rounded corner — border clashes with the radius
  • 🔴 Side-tab accent — a thick colored stripe on one edge of a rounded card (a classic AI tell)
Got it right

every nested pair satisfies r_inner ≈ r_outer − gap; radius comes from a role token; sharpness/roundness is one theme dial (radius.multiplier), not per-element overrides.

Predictable stacking (Elevation)

Why it matters. Elevation is a promise about what sits above what. Get the order wrong and content renders underneath where it should cover (a dropdown clipped by a sticky header, a toast behind a modal). Shadow strength carries meaning too — an over-strong shadow reads as a higher layer than intended.

Principle

Outer shadows ascend --shadow-low < -med < -high to signal height, and height implies a stacking order: base < dropdown < sticky < overlay/modal < toast < tooltip. Input state rings are a separate inset family, not elevation.

Smells
  • 🟢 Arbitrary z-index — magic numbers instead of the semantic order
  • 🟢 Clipped overflow — an overflow:hidden ancestor cutting off a menu/tooltip/popover that must escape
  • 🟡 Elevation/z-index mismatch — a surface that looks higher but stacks lower, so something appears beneath it
  • 🟡 Hairline border + wide diffuse shadow together — commit to a defined edge or soft elevation
  • 🔴 Colored glow shadows — keep depth quiet, not tinted
Got it right

each surface's shadow tier matches its place in the stacking order; nothing renders underneath a higher layer; popovers escape their containers; shadows are subtle enough that you feel depth without noticing the blur.

Content legibility (Typography)

Why it matters. Hierarchy is what lets the eye triage a screen, and it comes from real contrast in size and weight — not many sizes that are almost the same. Line-heights snap to the 4px grid so text shares the spacing rhythm.

Principle

Type is a geometric scale (base 14, ratio 1.2) exposed as roles — headings 1–6, body/large/label/code/supporting, display 1–3 — with four meaningful weights (400 body · 500 label+dataviz · 600 titles+headings · 700 strong). Base+ratio is a personality dial: lower/tighter reads dense (dashboards), higher/larger reads editorial (reading surfaces).

Smells
  • 🟢 Off-role sizing — a raw size instead of a role token
  • 🟢 Flat hierarchy — adjacent size steps under 1.25× (14/15/16 blur together)
  • 🟢 Tight leading — line-height under 1.3 on body (target 1.5)
  • 🟢 Tiny text — body under 12px
  • 🟢 Over-long lines — measure over 75ch
  • 🟢 Crushed or wide tracking — letter-spacing over 0.05em on body, or display crushed past legibility
  • 🟢 All-caps body / justified body
  • 🟡 Gradient text
Got it right

heading, body, and supporting text are unmistakably different at a glance; sizes come from role tokens; leading gives multi-line text room; line length stays ~65–75ch; and the scale's density matches the surface's intent.

Appropriate visual emphasis (Color)

Why it matters. Contrast is legibility, and emphasis is hierarchy: color is how the eye finds the primary action and reads status. The wrong neutral (a resting fill where an overlay belongs) reads as the wrong thing even when the shade looks similar. Overlays must be alpha, not opaque, so a tint composites over any surface.

Principle

A color is only meaningful relative to what it sits on, so foreground and background are decided together. Every token ships a light-dark() pair; neutrals are chosen by role--color-overlay-hover (states), --color-track (rails), --color-muted (containers with content), --color-secondary (self-contained elements). --color-wash is page-level only.

Smells
  • 🟢 Low contrast — any fg/bg below WCAG AA (4.5:1 text, 3:1 large/UI); light-gray placeholder on white is the classic fail
  • 🟢 Pure black/white — untinted #000/#fff instead of tinted neutrals
  • 🟢 Opaque overlay — a solid hover fill instead of an alpha tint
  • 🟡 Color alone — status by hue with no icon/text
  • 🟡 Gray on color — washed-out and dead
  • 🔴 Every action emphasized — when every button is primary, nothing is
Got it right

every fg/bg pair passes AA in both modes; the neutral matches the element's role; interaction tints are alpha overlays; status pairs color with an icon; and one clear primary action carries the emphasis.

Predictable movement (Motion)

Why it matters. Motion should clarify a change, not perform. Natural movement decelerates smoothly and animates cheap properties; anything else reads as either janky or gimmicky, and unbounded motion is an accessibility problem.

Principle

Motion matches the visual weight of the change: micro-interactions use the fast band (~175ms), entrances/exits medium (~410ms), continuous/large motions slow, all eased with --ease-standard. Reduced-motion collapses transitions to 0s.

Smells
  • 🟢 Ignoring prefers-reduced-motion
  • 🟢 Layout-property animation — animating width/height/margin causes jank; animate transform/opacity instead
  • 🟡 Bounce / elastic easing — dated and tacky; real objects decelerate smoothly (ease-out-quart/quint/expo)
  • 🔴 Decorative motion — image hover scale/rotate and other movement without meaning
Got it right

duration matches the change's weight; easing decelerates smoothly; only transform/opacity animate; motion carries meaning; and reduced-motion is honored.


Consistent State Representations

Every component expresses state. Astryx organizes state visuals by who drives the change — the user, the system, or an agent. Each state has a small set of approved visual representations. Reuse one before inventing. Picking the right representation for an archetype is 🔴 human judgment.

User Interaction States Visuals

States a person drives directly by pointing, clicking, typing, or tabbing.

State Visuals Representation
Rest
neutral default
base tokens, no interaction
Hovered (Overlay style)
:hover, guarded
--color-overlay-hover tint layered over the base
Hovered (Ring style)
:hover on a field
--color-border-emphasized border + --shadow-inset-hover ring
Pressed
:active
scale(0.98) + --color-overlay-pressed
Focused (Outline style)
:focus-visible
2px --color-accent outline at 3px offset
Focused (Ring style)
:focus-visible on a field
--color-accent border + inset ring
Selected (Filled style)
toggles · aria-checked, value
--color-accent fills the control to signal on
Selected (Surface style)
segmented · aria-checked
active segment lifts on --color-background-surface + --shadow-low
Selected (Edge style)
tabs / entries / steps · aria-selected/-current
--color-accent edge bar / underline marks the current item
Selected (Border style)
cards · isSelected
--color-accent border + inset ring around the chosen card
Selected (Depressed style)
list & nav rows, toggle buttons · aria-current/-selected/-pressed
--color-neutral / --color-accent-muted fills the current item

Selection is one intention with many treatments and many code states — split above by the visual style. Pick the style that fits your component and stay consistent within its family.

System Interaction States Visuals

States the system drives: disabling, waiting, validating, and reporting outcome. Status always pairs a semantic token with an icon — never color alone. Status is shown by prominence tier — the same meaning scaled from quiet in-flow surfaces up to bold transient overlays.

State Visuals Representation
Disabled
isDisabled → native disabled; disabledMessage
--color-text-disabled, muted and non-interactive; a message explains why
Loading (Skeleton style)
isLoadingaria-busy
a skeleton placeholder stands in for content not yet arrived; dimensions stay stable
Processing (Spinner style)
isLoadingaria-busy
in-place content hidden (color: transparent) with a spinner over it, or a spinner in the field's end slot; dimensions stay stable
Status (Muted style)
low prominence, persistent · status.type
muted --color-{status}-muted background + colored text/border + icon — banners and in-flow surfaces (destructive actions read as error)
Status (Inverted style)
high prominence · status.type
solid --color-{status} fill with --color-on-{status} text — compact chips and high-severity toasts
Temporal (Overlay style)
brief, high prominence, transient
--color-background-inverted overlay that appears briefly and dismisses — toasts and tooltips

Prominence tiers, one meaning. A status renders quietly as a muted surface (banners, inputs, destructive actions — persistent, in-flow), boldly as an inverted / solid fill (badges, high-severity toasts), and briefly as a temporal overlay (default toasts, tooltips). The token + icon keep the meaning constant across tiers.

Agent Interaction States Visuals

Mostly undefined. The intentions are named but the visuals aren't settled — the highest-value open design questions for an AI-native system. Propose treatments via the Component Specification Protocol; keep new visuals close to the User / System language.

State Visuals Representation
Thinking
agent reasoning / processing
Reasoning
chain-of-thought, thinking out loud
Streaming
receiving incremental text
Tool Executing
backend tool running
Awaiting Input
waiting for user response
Syncing / Synchronized
state synchronization
Inspecting
agent browsing / inspecting UI
Rendering
UI being generated & mounted

What's checkable, and where it's checked

The conventions above split by how they're verified. This page owns the what and why; the mechanics live in the linked docs.

Kind Reader Home
🟢 Objective — tokens-only, 4px grid, concentric radius, AA contrast, alpha overlays, type hierarchy/leading/size, motion safety, elevation↔z-index Agent / linter Implemented by the [[Night Watch Component Auditor
🟡 Assisted — monotonous spacing, size/density rhythm, color-alone status Agent flags, human confirms Flagged by the Auditor as Needs Review; resolved in [[Component Hardening Protocol
🔴 Human — right representation for the archetype, proportion, density, composition, AI tells Human [[Component Hardening Protocol

One source of truth. These conventions are the source; the Auditor's checks derive from them. When a convention changes here, update the corresponding Auditor check. When the Auditor finds a pattern not covered here, add it here first — see the Auditor's keep in sync rule.


Design Conventions for Templates

Templates — page templates (full-page scaffolds) and blocks (drop-in patterns) — carry design responsibility beyond any single component. These consolidate the existing Contributing Templates rubric, the Night Watch Designer axes, and Component Hardening Protocol L3 — a checklist, not a new invention.

Component & token purity (🟢)

  • Compose from Astryx, not raw HTML (VStack/Grid/Card over <div>, Text/Heading, Button, Link, Table, FormLayout). Necessary exceptions: <img> with a CDN src, <form> for native submit.
  • Icons via Icon / icon props — never raw <svg>.
  • Style through props, not custom CSS. Tokens, never raw values.

Layout & structure (🟢)

  • Page root is Layout or Center, content-only — templates don't ship AppShell (the host owns chrome); the exception is the Shell - category.
  • App chrome vs in-page nav: omit global nav; section nav → LayoutPanel start slot; page headers → LayoutHeader.
  • Responsive grids use Grid + minChildWidth, not fixed columns. Blocks are single-pattern, ~20–100 lines, with a correct aspectRatio.

Content & imagery (🟡)

  • Realistic mock data — never "Lorem ipsum" / "Item 1". Images from the astryx CDN only. Accurate .doc.mjs (required fields, complete componentsUsed).

Visual quality (the design-judge axes) (🔴) — beyond purity, a template is judged as a design on the same 5 axes the vision judge uses, the page-level analog of Hardening L3:

Axis What it checks
Layout fidelity Structural regions, grid, stacking — reads as an intentional page
Visual hierarchy Sizing, weight, eye flow — passes the squint test
Spacing & alignment Consistent, on-grid, proportional
Component fidelity Right components/variants, correct affordances
Color & theming Correct surface/accent usage, holds in light + dark

Bar: every gallery template should grade B or above on the Contributing Templates rubric and read well on the 5 visual axes. Purity/structure is largely automatable (🟢); the visual axes are the human layer (🔴).


Design-vs-Structure Boundary

Themes control what things look like; core controls what things are.

Change Where Why
"This gray should be darker" Theme Already references the right token
"Buttons rounder" Theme Role fixed; value is a theme call (respect concentricity)
"Badge should use a different gray than body text" Core Token selection changes the contract
"Add a compact Card variant" Core Variants are structural
"This needs a hover state" Core Interaction states are behavior

Corollary: a raw value in core is fixed by replacing it with the right token reference — never by swapping one raw value for another.


Using These Conventions

Designers: you don't write specs. Highest-value contributions: themes (judged against the foundations' rationale), gap reports, stories, templates, and hardening review. See Contributing with AI Assistants.

Reviewers (Hardening L3): let the Night Watch Component Auditor run the 🟢 objective checks first, then judge the 🟡/🔴 calls — state representation, proportions, density, and composition. When a review resolves a new design question, record the decision back into this page so the conventions compound.


Related

Clone this wiki locally