Design Principles
Neutral ink, semantic color, disciplined typography, flat surfaces, and two named shadow roles.
Last updated
VegaStack's look is restraint, with color spent only where it carries meaning. The product reads as
calm, dense, and typographic; color and elevation are spent sparingly so the few places that use them
actually mean something. Every rule below is enforced by design-lint and the design-audit skill — they
are not suggestions.
Neutral ink, minimal color
The primary action color is a near-neutral ink, not a brand hue. --primary is
oklch(0.353 0.003 75) in light and oklch(0.922 0.003 75) in dark — a warm, almost-chromaless tone.
Default buttons, focus rings, key text, and every selected / active state all sit in this neutral
register, so the interface stays quiet by default.
Color is rationed to a named role. Beyond neutral ink, color belongs to status families
(success, warning, destructive), blue info, categorical chart/tag palettes, or the deliberate
brand role used by VegaStack marketing surfaces. Product controls stay neutral unless color carries
one of those explicit meanings.
Surfaces & text
Action — neutral ink
Info — links & informational (the one chromatic accent)
Destructive
Success
Warning
Lines & utility
Charts — categorical series
Tags — categorical metadata (not status)
Sidebar surface
Spend nothing you don't have to
Reach for bg-primary (neutral ink) for actions, selection, and active
states; reach for a colored token only through its named status, information,
category, or brand role. Unnamed decoration is wrong.
Size does hierarchy, never bold
Headings, body, and prose are all Geist; code and data are Geist Mono. Hierarchy comes
from size, named tracking roles, and color. Weight is normally 400–500; the rare 600 tier is
available only through text-strong. Raw font-semibold and font-bold are lint-banned.
The ramp pairs size with a tightening letter-spacing rather than heavier weight: the display
tier (--text-display-xl 4.5rem at −0.06em, down to --text-display-sm 2rem at −0.04em)
through --text-h1 (1.5rem, −0.02em) to --text-h4 (1rem, weight 500). Use font-sans for
everything textual, font-mono for numbers, currency, and code; the serif face is a rationed
display accent (emphasis words, pull-quotes), never running text.
Flat surfaces, two named shadows
Surfaces are defined by borders, not elevation. Cards, inputs, and panels use border-border
with no drop shadow. The system ships two tightly scoped shadow roles:
--shadow-overlay:
0 4px 14px -4px oklch(0.13 0.002 75 / 0.1),
0 2px 4px -2px oklch(0.13 0.002 75 / 0.06);shadow-overlayis reserved for floating overlay surfaces.
It is the only shadow token, so there is no shadow-sm / shadow-md ladder to reach for and no
action finish either.
Overlay surface
Floating panel — popover, dialog, menu.
--shadow-overlay
Corners use the fixed 2/6/8/12/full roles. rounded-lg is the 12px container cap;
rounded-xl was removed and is lint-banned. Closely nested edges must be concentric.
--radius-xs
0.125rem · 2px
--radius-sm
0.375rem
--radius-md
0.5rem
--radius-lg
0.75rem
Semantic tokens only
Components reference semantic utilities (bg-primary, text-muted-foreground, border-border),
never raw palettes or hardcoded values. Tokens are OKLCH variables defined in
@vegastack/design-tokens/theme.css and bridged to Tailwind through @theme inline, so redefining a single
--primary in your app repaints every component in both themes.
// good — semantic tokens, themeable
<button className="bg-primary text-primary-foreground rounded-md" />
// bad — hardcoded hex / raw palette, opaque to overrides
<button className="bg-[#18181b] text-white rounded-[6px]" />design-lint fails any hardcoded hex/px, raw palette class (bg-neutral-900), or visual property in
an inline style — the only sanctioned inline-style use is assigning CSS custom properties (and a
single, file-scoped swatch-fill exception in ColorPicker).
4px spacing scale
Spacing follows Tailwind v4's 4px base unit — every gap, pad, and margin is a multiple of 4px via
the standard scale (p-1 4px, p-2 8px, p-4 16px, …). Stick to scale steps; don't reach for
arbitrary spacing values.
Motion is quiet and CSS-first
Motion uses three duration tokens (--duration-fast 150ms, --duration-base 200ms,
--duration-slow 300ms) and four easings (--ease-standard, --ease-emphasized, --ease-exit,
--ease-spring). Transitions are subtle and CSS-first; the JS motion library is installed by the
registry items that render Motion-driven behavior, including animated icons. prefers-reduced-motion: reduce is enforced globally in
@vegastack/design-tokens/base.css, so reduced-motion is honored without per-component work.
Duration
--duration-fast
150ms
--duration-base
200ms
--duration-slow
300ms
Easing
--motion-ease-standard
cubic-bezier(0.2, 0, 0, 1)
--motion-ease-emphasized
cubic-bezier(0.3, 0, 0, 1)
--motion-ease-exit
cubic-bezier(0.4, 0, 1, 1)
Accessible by default — WCAG 2.2 AA
Accessibility is a build rule, not a review step. Every component must pass axe and meet
WCAG 2.2 AA without dropping existing 2.1 assertions, with every applicable UI state implemented — default, hover, focus, loading, empty,
error, success, disabled. Interactive elements carry a visible :focus-visible ring: a
2px outline at 1px offset, keyed off --ring (which mirrors the neutral --primary ink). There
are no separate ring-width or offset tokens — the 2px / 1px convention is the ring.
Press Tab to move focus onto these controls and reveal the --ring outline.
Color choices are AA-aware: subtle/text variants are theme-tuned for on-surface contrast, and
--muted-foreground-faint is the one deliberately sub-AA tone, reserved strictly for placeholders.
The full contract — axe gate, keyboard semantics, and reduced motion — lives on the
Accessibility page.
Focus is non-negotiable
Never remove the focus outline to "clean up" a design. Keyboard users rely on
it, and the design-audit skill flags any interactive element that suppresses
:focus-visible.