Design Principles
Neutral ink, semantic color, disciplined typography, bordered surfaces, and shadow only where something floats.
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. The system is shadcn base-nova used as-is, so the shape of every component
is upstream's; what follows is the judgement layered on top, and the parts a script can check are
enforced by design-lint and the design-audit skill.
Neutral ink, minimal color
The primary action color is a pure neutral ink, not a brand hue. --primary is
oklch(0.205 0 0) in light and oklch(0.922 0 0) in dark — shadcn's own achromatic neutral ramp.
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 brand
marker role. 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 & focus
Brand
Media chrome
Photo scrim
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
Headings, body, prose and numbers are all Geist (numbers with tabular-nums); code and
identifiers are Geist Mono. Hierarchy comes
from size, weight and color, on Tailwind's stock scale — text-sm is 14px, text-base is 16px,
and there are no named role utilities layered over them. What is layered on is optical: every step
above text-base carries Geist's own line-height and negative letter-spacing, declared once in the
theme, so a heading is correctly spaced without anyone reaching for tracking-*. See
Typography.
Use font-sans for everything textual, with tabular-nums for numbers and currency, and font-mono
for code and identifiers; the serif
face is a rationed display accent (emphasis words, pull-quotes), never running text.
Bordered surfaces, shadow where something floats
Resting surfaces are defined by borders, not elevation. Cards, inputs, panels and rows use
border-border with no drop shadow. Shadow means a surface genuinely floats above the page, and it
is Tailwind's own step at the weight upstream gives that component — shadow-sm, shadow-md or
shadow-lg.
Floating surface
active tab chip · floating sidebar · inset main
shadow-sm
Floating surface
popover · menu · select
shadow-md
Floating surface
sheet · submenu
shadow-lg
Corners derive from one --radius (0.625rem / 10px): rounded-sm 6px, rounded-md 8px,
rounded-lg 10px, rounded-xl 14px. Closely nested edges must be concentric.
--radius-sm
0.6 × --radius · 6px
--radius-md
0.8 × --radius · 8px
--radius-lg
1 × --radius · 10px
--radius-xl
1.4 × --radius · 14px
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 and gated fail-closed by the token contrast check: every status family
ships a second ink, <family>-text, for the page and for the family's own tint, and
--muted-foreground is held to the AA text floor on every neutral surface because it carries real
secondary copy. 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.