Vetrix UI Style Guide
The authoritative reference for the Vetrix design system. All new UI work should reference this document first.
Color System
All colors are defined in tailwind.config.ts as theme extensions. Use the vetrix-* token names everywhere — never hardcode hex values in component files.
Navigation
| Token | Hex | Usage |
|---|---|---|
vetrix-nav |
#0747A6 |
TopNav background |
vetrix-nav-border |
#0052CC |
TopNav border, hover backgrounds in nav |
vetrix-nav-text |
#B3D4FF |
Text and icons inside TopNav |
Brand / Interactive
| Token | Hex | Usage |
|---|---|---|
vetrix-primary |
#0052CC |
Buttons, links, active states |
vetrix-primary-hover |
#0747A6 |
Hover state for primary elements |
vetrix-primary-light |
#DEEBFF |
Active sidebar item background |
vetrix-info |
#0052CC |
Same as primary; use for info badges |
Surfaces
| Token | Hex | Usage |
|---|---|---|
vetrix-canvas |
#F4F5F7 |
Page/app background (body) |
vetrix-surface |
#FFFFFF |
Cards, panels, table rows |
vetrix-surface-hover |
#F4F5F7 |
Row/item hover background |
Borders
| Token | Hex | Usage |
|---|---|---|
vetrix-border |
#DFE1E6 |
Default borders on cards, tables, inputs |
vetrix-border-strong |
#C1C7D0 |
Emphasized borders (focus rings, dividers) |
Text
| Token | Hex | Usage |
|---|---|---|
vetrix-text-primary |
#172B4D |
Body text, headings, labels |
vetrix-text-secondary |
#6B778C |
Secondary / helper text, timestamps |
vetrix-text-disabled |
#A5ADBA |
Disabled inputs, placeholder text |
Semantic / Status
| Token | Hex | Usage |
|---|---|---|
vetrix-success |
#00875A |
Success text, icons |
vetrix-success-light |
#E3FCEF |
Success badge/alert background |
vetrix-warning |
#FF991F |
Warning text, icons |
vetrix-warning-light |
#FFFAE6 |
Warning badge/alert background, impersonation banner |
vetrix-danger |
#DE350B |
Error text, destructive button fill, overdue indicators |
vetrix-danger-light |
#FFEBE6 |
Danger badge/alert background |
Typography
Font Families
Sans (default): -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
Mono: ui-monospace, SFMono-Regular, "SF Mono", monospace
Apply mono to clone URLs, commit SHAs, code snippets, and diff content via font-mono.
Text Scale
| Class | Size | Weight | Usage |
|---|---|---|---|
text-xs |
12px | varies | Labels, badges, timestamps, table column headers |
text-sm |
14px | normal | Body text, table rows, nav items, form fields |
text-base |
16px | normal | Page body prose (rare) |
text-lg |
18px | font-semibold |
Section headings (<h1> at page level) |
Heading pattern: text-lg font-semibold text-vetrix-text-primary
Sub-heading pattern: text-sm font-semibold text-vetrix-text-primary
Label pattern: text-xs font-medium text-vetrix-text-primary (form labels) or text-xs font-semibold text-vetrix-text-secondary uppercase tracking-wide (group labels)
Spacing
Vetrix uses Tailwind's default spacing scale (1 unit = 4px). Standard values in use:
| Value | px | Common context |
|---|---|---|
0.5 |
2px | Tight badge padding |
1 |
4px | Icon-to-icon gaps |
1.5 |
6px | Button vertical padding (sm), nav item vertical padding |
2 |
8px | Standard gap between items in a row |
3 |
12px | Horizontal padding in table cells, card padding (compact) |
4 |
16px | Standard page padding (p-4), horizontal nav padding |
6 |
24px | Content-area padding (p-6) in settings/dashboard |
8 |
32px | Large empty-state padding |
12 |
48px | Loading/empty state tall padding |
Breakpoints
Vetrix uses Tailwind's default breakpoint values, with two custom breakpoints added (xs and nav-break):
| Name | Min-width | Design intent |
|---|---|---|
| (none) | 0px | Mobile — single column, most secondary content hidden |
xs: |
480px | Large phone landscape — between narrow mobile and small tablet |
sm: |
640px | Small tablet — show secondary labels, hints, table columns |
md: |
768px | Tablet — show tertiary table columns (dates/times), CI/CD nav link |
nav-break: |
800px | Controls TopNav's search box width/visibility — below it the search box drops to a full-width row, at/above it search renders inline (300px) (TopNav.tsx's SearchForm, nav-break:block/nav-break:hidden) |
lg: |
1024px | Desktop — show all content, full sidebar layout |
xl: |
1280px | Wide — content area max-width cap (max-w-screen-xl) |
2xl: |
1536px | Ultra-wide — not currently used |
Responsive strategy
Vetrix follows mobile-first. Start with the mobile layout and add sm:/md:/lg: prefixes to expand. Prefer hidden sm:block over block sm:hidden for progressive disclosure.
Common progressive disclosure patterns:
Columns (table): hidden sm:table-cell (show at 640px+)
hidden md:table-cell (show at 768px+)
hidden lg:table-cell (show at 1024px+)
Nav labels: hidden sm:block (show text at 640px+)
Nav items: hidden md:block (show link at 768px+)
Layout System
Shell structure
<html>
<body bg-vetrix-canvas>
<AppShell>
<TopNav /> fixed h-12 z-40 top-0 full-width
<div flex-1 pt-12> offset for fixed nav
<Sidebar /> shrink-0 border-r (48px collapsed / 224px expanded)
<main flex-1> overflow-auto bg-vetrix-canvas
<page content> p-4 (standard) or p-6 (settings/dashboard)
</main>
</div>
<Footer /> h-10 border-t shrink-0
</AppShell>
</body>
</html>
Page container
Most pages use p-4 as the root container. Settings and dashboard use p-6. There is currently no global max-width on the content area — pages expand to fill all available horizontal space.
Sidebar widths
| Context | Collapsed | Expanded |
|---|---|---|
| Main repo/settings sidebar | w-12 (48px) |
w-56 (224px) |
| Dashboard repo list | — | 280px (inline style) |
| Admin sidebar | — | w-52 (208px) |
Component Patterns
Buttons
Base: inline-flex items-center justify-center gap-1.5 rounded font-medium transition-colors
sm size: h-7 px-3 text-xs
md size: h-8 px-4 text-sm
Variants:
primary bg-vetrix-primary text-white hover:bg-vetrix-primary-hover
secondary border border-vetrix-border bg-vetrix-surface text-vetrix-text-primary hover:bg-vetrix-surface-hover
danger bg-vetrix-danger text-white hover:bg-red-700
ghost text-vetrix-text-secondary hover:bg-vetrix-surface-hover hover:text-vetrix-text-primary
Form inputs
Container: flex flex-col gap-1
Label: text-xs font-medium text-vetrix-text-primary
Input: h-8 w-full rounded border border-vetrix-border px-3 text-sm bg-vetrix-surface
focus:ring-2 focus:ring-vetrix-primary focus:border-transparent
disabled:bg-vetrix-canvas disabled:text-vetrix-text-disabled
Error text: text-xs text-vetrix-danger mt-0.5
Hint text: text-xs text-vetrix-text-secondary mt-0.5
Cards / panels
Standard: rounded border border-vetrix-border bg-vetrix-surface p-4
Compact: rounded border border-vetrix-border bg-vetrix-surface p-3
Sectioned: rounded border border-vetrix-border bg-vetrix-surface
├─ header: border-b px-4 py-3 bg-vetrix-canvas
└─ body: p-4
Tables
Outer: overflow-hidden rounded border border-vetrix-border bg-vetrix-surface
Table: w-full border-collapse text-sm
TH row: border-b border-vetrix-border bg-vetrix-canvas
TH cell: px-3 py-2 text-left text-xs font-semibold text-vetrix-text-secondary uppercase tracking-wide
TR: border-b border-vetrix-border last:border-0 hover:bg-vetrix-surface-hover
TD cell: px-3 py-2 text-sm text-vetrix-text-primary
For scrollable tables (wide content): wrap table in overflow-x-auto.
Badges
Base: inline-flex items-center rounded font-medium
sm size: text-xs px-1.5 py-0.5
md size: text-sm px-2 py-0.5
Variants:
success bg-vetrix-success-light text-vetrix-success
warning bg-vetrix-warning-light text-vetrix-warning
danger bg-vetrix-danger-light text-vetrix-danger
info bg-vetrix-primary-light text-vetrix-primary
neutral bg-vetrix-canvas text-vetrix-text-secondary
Modals
Backdrop: fixed inset-0 bg-black/30
Positioner: fixed inset-0 flex items-center justify-center p-4
Dialog: w-full rounded-lg bg-vetrix-surface shadow-xl
+ max-w-sm / max-w-md (default) / max-w-lg / max-w-2xl
Header: flex items-center justify-between border-b px-4 py-3
Title: text-sm font-semibold text-vetrix-text-primary
Body: p-4
Footer: flex justify-end gap-2 border-t px-4 py-3
Empty states
Container: flex flex-col items-center justify-center py-12 text-center
Icon: text-4xl or h-12 w-12 text-vetrix-text-secondary mb-4
Heading: text-sm font-semibold text-vetrix-text-primary
Description: text-sm text-vetrix-text-secondary max-w-sm mt-1
Action: mt-4 (Button or Link)
Icon Usage
Vetrix uses Heroicons (@heroicons/react).
- 20/solid — for inline icons inside buttons, badges, table cells, and compact UI
- 24/outline — for nav icons, notification bell, and any standalone icon larger than a button
Size reference: h-3.5 w-3.5 (button icons) · h-4 w-4 (standard) · h-5 w-5 (nav/prominent) · h-6 w-6 (logo/avatar)
Always add aria-hidden="true" on decorative icons.
Z-Index Scale
| Value | Context |
|---|---|
z-10 |
Dropdowns and popovers |
z-20 |
Select/Combobox dropdown lists |
z-40 |
TopNav fixed header |
z-50 |
Modals and overlays |
Animation / Transitions
Vetrix uses Tailwind's built-in transition-colors for interactive hover states and transition-all duration-200 for the sidebar collapse animation. No custom keyframe animations are defined. Use these sparingly and consistently.
Server Components vs Client Components
Decision Flowchart
Use a Server Component (default — no 'use client') when the component:
- Fetches data and renders it (read-heavy pages)
- Has no user interactivity (no
useState,useEffect, event handlers) - Does not use browser-only APIs (
window,document,localStorage)
Use a Client Component ('use client') when the component:
- Manages interactive state (
useState,useReducer) - Uses effects or refs (
useEffect,useRef) - Handles user events (
onClick,onChange,onSubmit) - Depends on browser APIs or React context (
useContext,useQuery)
Architecture Pattern
page.tsx (Server Component — async, fetches data)
└── Suspense boundary (streaming fallback)
└── PageContent.tsx ('use client' — receives data as props)
└── Interactive children (forms, dropdowns, pagination)
Server-Side Data Fetching
Use serverFetch() from @/lib/api/server for data in Server Components:
import { serverFetch } from '@/lib/api/server'
const data = await serverFetch<User>(`/api/v1/users/${owner}`)
if (!data) notFound()
Passing Server Data to Client Components
Use initialData in useQuery for reactive updates after server-side fetch:
// Server Component
const data = await serverFetch<Items>('/api/v1/items?page=1')
return <ItemList initialData={data} />
// Client Component
const { data } = useQuery({
queryKey: ['items', page],
queryFn: () => api.list({ page }),
initialData: page === 1 ? initialData : undefined,
})
Rules
- Server Components cannot use hooks — no
useState,useEffect,useQuery. - Server Components CAN import Client Components (
AppShell,Button, etc.). - Client Components cannot import Server Components — but can receive them as
children. AppShellstays client-side — it uses auth hooks and notification polling.- Suspense boundaries are required around async Server Components for streaming HTML.
- Use
awaitin Server Components;use()in Client Components for promise resolution.