Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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

--background
--foreground
--card
--card-foreground
--popover
--popover-foreground
--surface-1
--surface-2
--surface-3
--muted-foreground
--muted-foreground-faint
--border

Action — neutral ink

--primary
--primary-foreground
--primary-hover
--primary-active

Info — links & informational (the one chromatic accent)

--info
--info-foreground
--info-hover
--info-active
--info-subtle
--info-text

Destructive

--destructive
--destructive-foreground
--destructive-hover
--destructive-active
--destructive-subtle
--destructive-text

Success

--success
--success-foreground
--success-hover
--success-active
--success-subtle
--success-text

Warning

--warning
--warning-foreground
--warning-hover
--warning-active
--warning-subtle
--warning-text

Lines & utility

--border
--input
--ring
--track
--overlay

Charts — categorical series

--chart-1
--chart-2
--chart-3
--chart-4
--chart-5
--chart-6
--chart-7
--chart-8

Tags — categorical metadata (not status)

--tag-blue
--tag-blue-subtle
--tag-blue-text
--tag-cyan
--tag-cyan-subtle
--tag-cyan-text
--tag-green
--tag-green-subtle
--tag-green-text
--tag-lime
--tag-lime-subtle
--tag-lime-text
--tag-yellow
--tag-yellow-subtle
--tag-yellow-text
--tag-orange
--tag-orange-subtle
--tag-orange-text
--tag-red
--tag-red-subtle
--tag-red-text
--tag-pink
--tag-pink-subtle
--tag-pink-text
--tag-magenta
--tag-magenta-subtle
--tag-magenta-text
--tag-purple
--tag-purple-subtle
--tag-purple-text

Sidebar surface

--sidebar
--sidebar-foreground
--sidebar-primary
--sidebar-primary-foreground
--sidebar-accent
--sidebar-accent-foreground
--sidebar-border
--sidebar-ring

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.

Do
Use bg-primary for product actions; use status, info, category, or brand tokens only for their named roles.
Don't
Invent an unnamed accent or use a chromatic token where neutral ink communicates the same thing.

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.

Geist — the quick brown fox--text-display-xl · 4.5rem / 4.75rem · −0.06em
Geist — the quick brown fox--text-display-lg · 3.5rem / 3.75rem · −0.05em
Geist — the quick brown fox--text-display-md · 2.5rem / 2.75rem · −0.045em
Geist — the quick brown fox--text-display-sm · 2rem / 2.25rem · −0.04em
Geist — the quick brown fox--text-h1 · 1.5rem / 2rem · −0.02em
Geist — the quick brown fox--text-h2 · 1.25rem / 1.75rem · −0.015em
Geist — the quick brown fox--text-h3 · 1.125rem / 1.5rem · −0.01em
Geist — the quick brown fox--text-h4 · 1rem / 1.375rem
Geist — the quick brown fox--text-label · 0.875rem / 1.25rem
Geist — the quick brown fox--text-label-sm · 0.75rem / 1rem
Geist — the quick brown fox--text-code · 0.8125rem / 1.25rem
Geist — the quick brown fox--text-code-sm · 0.75rem / 1rem

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.

Do
Let size, named typography roles, and color carry hierarchy; reserve text-strong for rare emphasis.
Don't
Use raw tracking, font-bold, or font-semibold utilities.

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-overlay is 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

Do
Separate surfaces with border-border and use shadow-overlay for overlays — nothing else.
Don't
Elevate cards, nest equal-elevation cards, or invent a generic shadow ladder.

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).

Do
Style with semantic tokens: bg-primary, text-muted-foreground, border-border, rounded-md.
Don't
Hardcode hex or px, use raw palettes (bg-neutral-900), or put visual props in inline styles.

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.

1 · calc(var(--spacing) * 1) · 4px
2 · calc(var(--spacing) * 2) · 8px
3 · calc(var(--spacing) * 3) · 12px
4 · calc(var(--spacing) * 4) · 16px
6 · calc(var(--spacing) * 6) · 24px
8 · calc(var(--spacing) * 8) · 32px
12 · calc(var(--spacing) * 12) · 48px
16 · calc(var(--spacing) * 16) · 64px

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.

Do
Keep a visible 2px focus-visible ring and meet AA contrast on every interactive element.
Don't
Strip focus outlines or rely on muted-foreground-faint for anything but placeholders.

On this page