Traqto design system
One system, decided once.
shadcn/ui Vega on Base UI, Tailwind CSS v4, light mode only. This page is both the documentation and a specimen of itself.
Overview
Traqto builds its interfaces on shadcn/ui's Vega style, on Base UI, with Tailwind CSS v4, in light mode only. This page is the reference: every component and block below is the real thing, rendered live, with the source that renders it beside it.
One setup, always the same result
A new Traqto project starts from the default shadcn scaffold with the Vega preset. Nothing is customized on the way in, so any two projects that run this command get identical components.
pnpm dlx shadcn@latest init --template next --base base --preset vega
pnpm dlx shadcn@latest add button card input label table tabs dialog dropdown-menu sidebar sonnerWhat is already decided
| Decision | Value |
|---|---|
| Stack | Next.js (App Router) · Tailwind CSS v4 · shadcn/ui |
| Style | Vega — shadcn's classic density, the successor of new-york |
| Base | Base UI (@base-ui/react), the CLI default |
| Theme | neutral, as generated by the CLI. Brand color is reserved, not applied. |
| Color scheme | Light only. The theme is forced to light; there is no toggle. |
| Fonts | Inter for text, Geist Mono for code — both via next/font |
| Icons | Lucide |
| Components | Copied into components/ui by the CLI. Never edited. |
| Blocks | Official shadcn blocks, used as installed: sidebar-03 for navigation, dashboard-01 for the console |
| Navbar | 64 px, fixed at the top: trigger · breadcrumb · search · profile menu |
How to read this page
Sections follow the order a designer or engineer needs them: foundations first (brand, color, type, spacing, radius, elevation), then the components, then the blocks that compose them, then the console — the screen every product screen inherits from — and finally the tokens file to copy.
Each example has a Preview tab and a Code tab. The code is the file that renders the preview, read at build time. Copy it; it works as is.
Principles
Five rules with no exceptions. If a design needs to break one, the design changes — not the rule.
- 1
The components are the source of truth.
Nothing in
components/uiis ever edited. If something needs to look different, the theme changes inapp/globals.css, or the call site passes aclassName. Updating ispnpm dlx shadcn@latest add <name> --overwrite, and it must always be safe to run. - 2
Light mode only.
Interfaces are designed once, for a light ground. The CLI writes a
.darkblock into the stylesheet and it stays there, untouched, so the file remains the default — but nothing activates it: the theme is forced to light inapp/layout.tsx, and there is no toggle anywhere. - 3
Color comes from the theme tokens.
Product UI uses
bg-primary,text-muted-foreground,border-borderand the rest of the semantic set. Never a raw hex, never a palette utility likebg-blue-500. This is what lets the palette change in one file when the brand color is applied. - 4
Anatomy comes from shadcn, compositions come from blocks.
Structure, density and proportion are Vega's. Where an official block exists — the navigation is
sidebar-03, the console isdashboard-01— it is used as installed. The same problem is never solved twice, differently. - 5
No decoration beyond what the components bring.
No background images, textures or patterns. No custom animation. No emoji in product UI. Gradients, blur and shadows appear only where Vega itself uses them: the tint on the stat cards, the fill under the chart, the scrim behind a dialog.
Brand
No logo, logotype, mark or other brand file exists yet. Until one does, the wordmark is the product name set in the system's own type.
Wordmark
Traqto, in Inter at weight 700, tracking −0.01 em, sentence case, in the foreground color. Never stretched, condensed, outlined, shadowed, rotated, or placed on a busy ground.
Traqto
Traqto
Traqto
<span className="font-heading text-3xl font-bold tracking-[-0.01em]">Traqto</span>Reserved brand colors
Two colors belong to Traqto. They are defined, but not in the theme: the theme is the Vega neutral default until a palette is applied by hand. The approved mapping to theme tokens is in DESIGN-SYSTEM.md.
Still to be produced
- Logotype and symbol
- Clear space and minimum sizes
- Single-color and reversed variants
- Misuse examples
- Favicon and app icon at 16 / 32 / 180 / 512
- Open Graph templates
- A brand typeface decision
Color
The theme is shadcn's neutral palette, exactly as the CLI generated it. Every color in the product is one of these tokens; the raw values live in app/globals.css and are read from there to render this section.
Surfaces
Brand
Supporting
Lines
Charts
Sidebar
Notice cards
The palette for informative boxes, in five tones. The colors come straight from Tailwind's default palette — the only place the product reaches past the theme tokens. Two steps per tone, always the same pair: 50 is the card background; 500 is the icon, the border and the accents. The title is the foreground; the description is the muted foreground. The boxes themselves are the Alert component — see Components for the live anatomy and code.
- Only these five tones, only these two steps: 50 and 500. No 100, no 200, no 700, no other hues.
- The 50 step is a card background, never a page, section or sidebar background.
- The title is the regular foreground; the description is the muted foreground — on all five tones. The tone lives in the background, the border and the icon, never in the copy.
- The neutral tone uses the
neutralfamily, the same gray family as the theme.
States are derived, not added
Vega derives hover, pressed and tinted states with opacity modifiers on the same tokens. There are no extra hover or subtle tokens to maintain.
| Utility | Where |
|---|---|
| hover:bg-primary/80 | Default button hover |
| hover:bg-muted | Outline, ghost and toggle hover |
| bg-destructive/10 text-destructive | Destructive button and badge |
| ring-ring/50 | Focus ring |
| ring-foreground/10 | Card, dialog and menu edge |
| bg-black/10 | Dialog and sheet scrim |
| text-foreground/60 | Inactive tab label |
When the brand palette is applied, only the values in :root change. Token names, utilities and components stay as they are.
Typography
Inter for text and Geist Mono for code, both loaded with next/font in app/layout.tsx. The scale is Tailwind's default; the components decide which step they use.
Scale
Weights
| Weight | Utility | Use |
|---|---|---|
| 400 | font-normal | Body, descriptions |
| 500 | font-medium | Buttons, labels, card titles, tabs, active nav |
| 600 | font-semibold | Section headings, stat values |
| 700 | font-bold | Wordmark |
Rules
- Headings use
font-heading(an alias of the sans face) withtracking-tight. Body keeps the default tracking andleading-relaxedor the component's own. - Control text is 14 px everywhere — buttons, inputs, selects, menus, tabs, table cells. Inputs render at 16 px below the
mdbreakpoint so touch devices do not zoom. - Digits that line up in columns take
tabular-nums: stat values, amounts, counts, pagination. - Code and tokens use
font-mono, one step smaller than the surrounding text. - No uppercase labels. Sentence case everywhere, including eyebrows.
Spacing
Tailwind's default scale, 4 px per step. The components fix their own sizes; the numbers below are what they actually use, so nothing here is a preference.
Steps in use
Control sizes
Live controls at every size the components ship. Text stays 14 px in all of them.
Fixed measurements
| Measure | Value |
|---|---|
| Control height | 36 px default · 32 px small · 40 px large · 24 px extra small |
| Control padding | 10 px (px-2.5); 8 px when an icon leads or trails |
| Badge | 20 px tall, 8 px padding |
| Card padding | 24 px (--card-spacing); 16 px with size="sm" |
| Gap between cards | 16 px, 24 px from the md breakpoint (gap-4 md:gap-6) |
| Page padding | 16 px, 24 px from the lg breakpoint (px-4 lg:px-6) |
| Sidebar width | 16 rem (256 px); 18 rem on mobile; 3 rem collapsed to icons |
| Console sidebar | 18 rem (288 px) — dashboard-01 overrides --sidebar-width |
| Navbar | 4 rem (64 px), fixed — --header-height in the console |
| Sidebar menu button | 32 px default · 28 px small · 48 px large |
| Dialog width | 28 rem (448 px) from the sm breakpoint |
Content width is not capped by the components. The console lets the grid fill the inset; this page caps itself at max-w-6xl for reading comfort.
Radius
One base value, --radius: 0.625rem (10 px). Every other step is a multiple of it, mapped in the @theme block of globals.css.
Scale
Small buttons clamp their corner with rounded-[min(var(--radius-md),10px)] so a bigger base radius never turns a 32 px control into a pill. Changing the base value changes the whole system at once; do not set radii per component.
Elevation
Tailwind's default shadow scale, used sparingly. Surfaces that sit on the page get an edge, not a shadow: cards, dialogs and menus draw ring-1 ring-foreground/10.
Scale
- Shadows are neutral. There are no colored shadows and no custom shadow values; the scale above is the whole vocabulary.
- Hovering a card changes nothing. Cards are not interactive; the controls inside them are.
- Overlays use a light scrim,
bg-black/10, with a small backdrop blur — both come from the Dialog and Sheet components and are not adjusted.
Components
Every component is shadcn/ui's, installed with the CLI and used as shipped. Listed with the variants the system uses. Buttons are imperative verbs; labels are short nouns.
Install any of them with pnpm dlx shadcn@latest add <name>. The file lands in components/ui and stays exactly as the registry wrote it.
Button sizes
xs 24 px · sm 32 px · default 36 px · lg 40 px, plus icon variants at each size. Icons inside buttons are 16 px, or 12 px in xs.
Input and Label
36 px tall. Always paired with a Label through htmlFor. Invalid inputs take aria-invalid and get a destructive border and ring; disabled inputs drop to 50 % opacity.
Enter all 16 digits.
Textarea
As Input. Grows with its content (field-sizing: content) from a 64 px minimum.
Select
Matches Input in height and border. Pass items to the root so the trigger shows labels instead of values. Sizes default (36 px) and sm (32 px).
Checkbox
16 px, rounded-sm; checked fills primary. Use a Checkbox when the change needs saving.
A summary of activity, sent every Monday.
Radio group
16 px, round; checked fills primary. One choice from a short list that should stay visible.
Switch
A switch applies on toggle. If the change needs a Save button, it is a Checkbox.
Applies as soon as you toggle it.
Field
The layout primitive for forms: FieldSet, FieldLegend, FieldGroup, Field, FieldLabel, FieldDescription, FieldError. Field handles vertical, horizontal and responsive orientations.
Card
White surface, rounded-xl, shadow-xs and a 10 % foreground ring instead of a border. 24 px spacing, 16 px with size='sm'. CardAction sits top-right of the header.
Badge
20 px pill. Variants default, secondary, outline, ghost, destructive, link. Status badges use outline with a leading 12 px icon.
Table
Rows wash to muted on hover. Numeric cells are right-aligned with tabular-nums. Status is a Badge.
| Invoice | Customer | Status | Amount |
|---|---|---|---|
| INV-1041 | Northwind | Paid | $1,200.00 |
| INV-1042 | Contoso | Pending | $480.00 |
| INV-1043 | Fabrikam | Paid | $3,150.00 |
| INV-1044 | Tailspin | Overdue | $96.00 |
Alert
The informative box, in five tones on one anatomy: title states the outcome, description states the next step, and it stays until dismissed. The colors are applied at the call site; the palette and its rules — steps 50 and 500 only — are defined in the Color section.
Toast (Sonner)
Toasts confirm an outcome and leave. One Toaster in app/layout.tsx; call toast() anywhere. It inherits the popover colors.
Tabs
Two list variants: default (a muted tray) and line (an underline). The active trigger takes the foreground color.
Dialog
Centered, 448 px wide from the sm breakpoint, with a 10 % scrim. Confirmations name the consequence in the description and put the destructive action last.
Sheet
A panel from the edge of the screen, 24 rem wide from the sm breakpoint. Use it for secondary edits that should not leave the page.
Tooltip
Foreground on background, 12 px text. One TooltipProvider wraps the app in app/layout.tsx.
Separator
A 1 px line in the border color, horizontal or vertical.
Workspace settings
Name, timezone and default locale.
Avatar
Round, with initials as the fallback. AvatarGroup overlaps several and can end with a count.
Toggle group
A segmented control. Single selection with multiple={false}; spacing={0} joins the items into one bar.
Pagination
Ghost links; the current page is outline.
Item
A list row with media, content and actions. Variants default, outline and muted.
Updated 2 hours ago by Dana
1,204 of 5,000 events
Empty
The empty-state primitive: a dashed surface with media, title, description and actions. See Blocks for the three tones.
Kbd
Keyboard hints, 12 px on a muted chip.
Spinner
A 16 px loader that inherits the current color. Inside a button, disable the button while it spins.
Skeleton
Placeholder shapes in the accent color while content loads.
States & motion
Every state is already in the components. The table lists what Vega does, taken from the class names, so you can recognize it — not so you can re-implement it.
Live states
The “Focused” button above wears the focus classes permanently so the ring can be seen without a keyboard. Real focus rings appear on focus-visible only.
Treatments
| State | Treatment | Classes |
|---|---|---|
| Hover, solid | Background to primary/80 | hover:bg-primary/80 |
| Hover, quiet | Outline, ghost, toggles and rows wash to muted | hover:bg-muted |
| Press | Moves down one pixel. No scale, no color change | active:translate-y-px |
| Focus | 3 px ring in the ring color at 50 %, border takes the ring color | focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:border-ring |
| Invalid | Destructive border and a 20 % destructive ring | aria-invalid:border-destructive aria-invalid:ring-destructive/20 |
| Disabled | 50 % opacity; buttons ignore the pointer, inputs show not-allowed | disabled:opacity-50 |
| Loading | A Spinner replaces the leading icon and the button is disabled | <Button disabled><Spinner />Saving</Button> |
| Expanded | Triggers that open a menu keep the hover wash while open | aria-expanded:bg-muted |
Motion
Motion is whatever the component ships with. There is no custom animation in the product and no reason to add one.
| Where | What moves |
|---|---|
| Controls | transition-all on buttons; color and box-shadow on inputs. Default duration, default easing. |
| Menus, dialogs | fade-in-0 zoom-in-95 from tw-animate-css, 100 ms. |
| Sheets | Slide from the edge with a 200 ms ease-in-out. |
| Sidebar | Width and margin, 200 ms linear, when it collapses. |
| Switch | The knob slides with the component's own transition. |
| Everything else | Nothing. No scroll reveals, no bounces, no springs. |
Iconography
Lucide, through lucide-react. 24 × 24 grid, 2 px stroke, round caps and joins, no fill. Icons inherit the color of their control and are sized by it.
Icons in use
Sizes
| Size | Utility | Where |
|---|---|---|
| 16 px | size-4 | Inside buttons, inputs, menus, alerts, sidebar items — applied by the component |
| 12 px | size-3 | Inside badges and extra-small buttons |
| 20 px | size-5 | Sidebar header tile |
| 24 px | size-6 | Empty-state media |
Usage
Put the icon inside the control and let the control size it. Pass a size class only when the icon stands alone.
import { PlusIcon } from "lucide-react"
<Button>
<PlusIcon />
New project
</Button>Icon-only buttons always carry an aria-label. No emoji in product UI, ever.
Blocks
A block is a settled arrangement of components. The same problem is never solved twice, differently. Most of these come straight from shadcn's blocks — sidebar-03 for navigation, dashboard-01 for the rest; the remaining ones are compositions of the components above and introduce no new size, color or shadow.
Blocks from the registry keep their sample content (Documentation v1.0.0, Getting Started, Total Revenue). Replace it with product copy that follows the Voice section; do not restyle the block.
Navbar
One 64 px row, fixed at the top of the content area — it never scrolls; only the page does. Left: sidebar trigger, separator, breadcrumb (section › page). Right: search with the / shortcut, and the profile menu on the avatar. Below the md breakpoint the breadcrumb keeps only the current page and the search becomes an icon button. The screen title is not here: it lives in the page header.
Page header
Opens the content area. Eyebrow, title, description on the left; an outline button and a primary button bottom-aligned on the right, stacking below on narrow screens.
Workspace
Projects
Everything your team is tracking, grouped by client.
Stat cards
Four cards: label, value with tabular numerals, an outline badge with the trend, and a two-line footer. The grid goes 1 → 2 → 4 across, keyed to the container's width, never 3.
Chart card
A Card with a title, description and a segmented range control in CardAction; a Recharts area chart through ChartContainer. The range control becomes a Select under 767 px of card width.
Data table
Tabs with counts and column controls on top; a TanStack table with drag handles, row selection, status badges, inline numeric inputs and a row menu; selection summary and pagination in the footer. Rows open a drawer with details.
| Header | Section Type | Status | Target | Limit | Reviewer | |||
|---|---|---|---|---|---|---|---|---|
Cover page | In Process | Eddie Lake | ||||||
Table of contents | Done | Eddie Lake | ||||||
Narrative | Done | Eddie Lake | ||||||
Narrative | Done | Jamik Tashpulatov | ||||||
Narrative | In Process | Jamik Tashpulatov | ||||||
Narrative | In Process | Jamik Tashpulatov | ||||||
Narrative | In Process | Jamik Tashpulatov | ||||||
Narrative | Done | |||||||
Technical content | Done | |||||||
Narrative | Done |
Form panel
A Card with a bordered header and footer. Rows are responsive Fields: label and description on the left, control on the right from the md container width, stacked below it. Sections separate with FieldSeparator.
Shown in the sidebar and invites.
Used for reports and schedules.
Applies as soon as you toggle it.
Empty state
Three tones on the same Empty primitive: no data, no results, error. The error tone recolors the icon only; the surface stays white.
Console
The reference screen. Every product screen inherits this by default: app shell → navbar → page header → stat cards → chart card → data table. If a screen does not look like this, the screen is wrong. It is shadcn's dashboard-01 block with the sidebar-03 sidebar and the Traqto navbar, served at /console.
Already decided
| Decision | Value |
|---|---|
| Anatomy | SidebarProvider → AppSidebar from sidebar-03 + SidebarInset → navbar → page header → stat cards → chart card → data table |
| Page ground | Sidebar and content share the top edge (variant sidebar, not inset). The sidebar header and the navbar are both 64 px; their bottom borders meet at the sidebar's vertical border as one continuous line. |
| Sidebar | sidebar-03 — a 64 px logo row (tile + Traqto wordmark, nothing else), then sections with submenus, active sub-item; 18 rem wide (--sidebar-width), off-canvas drawer under 768 px |
| Navbar | 4 rem (64 px), fixed at the top; trigger, breadcrumb, search, profile. The page title lives in the page header below it |
| Content | Fills the inset. Padding px-4 lg:px-6, blocks stacked with gap-4 md:gap-6 |
| Grids | Container queries on @container/main — 1, 2 or 4 stat cards across |
| Scrolling | Only the content scrolls. The sidebar is fixed and scrolls its own list; the navbar never moves |
| Density | 36 px controls, 14 px control text, 24 px card spacing — everywhere |
The page
app/console/page.tsx — the whole screen is this file plus the blocks above.
import { AppSidebar } from "@/components/app-sidebar"
import { ChartAreaInteractive } from "@/components/chart-area-interactive"
import { DataTable } from "@/components/data-table"
import { SectionCards } from "@/components/section-cards"
import { SiteHeader } from "@/components/site-header"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import data from "./data.json"
export default function Page() {
return (
<SidebarProvider
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
"--header-height": "calc(var(--spacing) * 16)",
} as React.CSSProperties
}
>
<AppSidebar />
<SidebarInset className="h-svh overflow-hidden">
<SiteHeader />
{/* The body is the only thing that scrolls; the navbar and the sidebar stay put. */}
<div className="flex flex-1 flex-col overflow-y-auto">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<div className="flex flex-col gap-1 px-4 lg:px-6">
<h1 className="font-heading text-2xl font-semibold tracking-tight">
Data fetching
</h1>
<p className="text-sm text-muted-foreground">
Sections, targets and reviewers for this document.
</p>
</div>
<SectionCards />
<div className="px-4 lg:px-6">
<ChartAreaInteractive />
</div>
<DataTable data={data} />
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
)
}Install it in a new project with pnpm dlx shadcn@latest add dashboard-01, then pnpm dlx shadcn@latest add sidebar-03 --overwrite so the submenu sidebar replaces the one dashboard-01 ships. The blocks bring their own sample data and the components they need.
Voice & content
Plain, direct, professional-neutral. Product-interface copy, not marketing.
| Rule | In practice |
|---|---|
| Casing | Sentence case for headings, buttons, labels, menu items. Never Title Case or all caps. |
| Buttons | Imperative verbs: Save changes, Send invite, Delete. |
| Labels | Short nouns: Card number, Default timezone. |
| Success | State the outcome and stop: Invitation sent. |
| Errors | State the fix, not just the failure: Update your billing details to continue. |
| Confirmations | Name the consequence: This removes the project and its 12 documents. |
| Punctuation | Minimal. Exclamation marks sparingly, only in positive toasts. |
| Emoji | None, anywhere. |
| Numbers | $49, $1,200, UTC−5 — with a real minus sign. Tabular numerals in columns. |
| Address | The reader is you. The product does not say we. |
Tokens
The files that make a project a Traqto project. Both are exactly what the CLI generated; copy them verbatim.
app/globals.css
The theme. Light values in :root, the Tailwind mapping in @theme inline. The .dark block is the CLI's and stays unused — see Principles.
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-heading: var(--font-sans);
--font-sans: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}components.json
The CLI contract: style, base color, icon library and aliases. Any project with this file and the same CLI version installs the same components.
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-vega",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}Light mode, enforced
The one deliberate addition to the default template: the theme provider is forced to light in app/layout.tsx, so neither the system preference nor the template's hotkey can switch the scheme.
import type { Metadata } from "next"
import { Geist_Mono, Inter } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
import { Toaster } from "@/components/ui/sonner"
import { TooltipProvider } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" })
const fontMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-mono",
})
export const metadata: Metadata = {
title: "Traqto Design System",
description:
"The reference for the Traqto design system: shadcn/ui Vega, Tailwind CSS v4, light mode only.",
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html
lang="en"
suppressHydrationWarning
className={cn("antialiased", fontMono.variable, "font-sans", inter.variable)}
>
<body>
{/* Light mode only: the theme is forced, the `d` hotkey is inert. */}
<ThemeProvider forcedTheme="light">
<TooltipProvider>{children}</TooltipProvider>
<Toaster />
</ThemeProvider>
</body>
</html>
)
}Applying a palette later
- Change values in
:rootonly. Keep every token name. - Keep the OKLCH format the CLI uses, and check contrast for
primary-foregroundonprimaryandmuted-foregroundonbackground. - Do not touch
components/ui. If a component looks wrong after the change, the value is wrong, not the component. - The approved Traqto mapping is in
DESIGN-SYSTEM.md.