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 sonner

What is already decided

DecisionValue
StackNext.js (App Router) · Tailwind CSS v4 · shadcn/ui
StyleVega — shadcn's classic density, the successor of new-york
BaseBase UI (@base-ui/react), the CLI default
Themeneutral, as generated by the CLI. Brand color is reserved, not applied.
Color schemeLight only. The theme is forced to light; there is no toggle.
FontsInter for text, Geist Mono for code — both via next/font
IconsLucide
ComponentsCopied into components/ui by the CLI. Never edited.
BlocksOfficial shadcn blocks, used as installed: sidebar-03 for navigation, dashboard-01 for the console
Navbar64 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. 1

    The components are the source of truth.

    Nothing in components/ui is ever edited. If something needs to look different, the theme changes in app/globals.css, or the call site passes a className. Updating is pnpm dlx shadcn@latest add <name> --overwrite, and it must always be safe to run.

  2. 2

    Light mode only.

    Interfaces are designed once, for a light ground. The CLI writes a .dark block into the stylesheet and it stays there, untouched, so the file remains the default — but nothing activates it: the theme is forced to light in app/layout.tsx, and there is no toggle anywhere.

  3. 3

    Color comes from the theme tokens.

    Product UI uses bg-primary, text-muted-foreground, border-border and the rest of the semantic set. Never a raw hex, never a palette utility like bg-blue-500. This is what lets the palette change in one file when the brand color is applied.

  4. 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 is dashboard-01 — it is used as installed. The same problem is never solved twice, differently.

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

BlueReserved
#0069c1 · Primary, when applied
Blue DarkReserved
#003869 · Emphasis, when applied

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

bg-backgroundoklch(1 0 0)Page and card ground
bg-foregroundoklch(0.145 0 0)Text, icons, tooltips
bg-cardoklch(1 0 0)Card surface
bg-card-foregroundoklch(0.145 0 0)Text on cards
bg-popoveroklch(1 0 0)Menus, dialogs, sheets
bg-popover-foregroundoklch(0.145 0 0)Text in popovers

Brand

bg-primaryoklch(0.205 0 0)Default button, checked controls, links
bg-primary-foregroundoklch(0.985 0 0)Text on primary

Supporting

bg-secondaryoklch(0.97 0 0)Secondary button, secondary badge
bg-secondary-foregroundoklch(0.205 0 0)Text on secondary
bg-mutedoklch(0.97 0 0)Quiet surface, tab list, hover wash
bg-muted-foregroundoklch(0.556 0 0)Descriptions, placeholders
bg-accentoklch(0.97 0 0)Focused menu and select items
bg-accent-foregroundoklch(0.205 0 0)Text on accent
bg-destructiveoklch(0.577 0.245 27.325)Destructive text and tints

Lines

bg-borderoklch(0.922 0 0)Hairlines, separators
bg-inputoklch(0.922 0 0)Input and select borders
bg-ringoklch(0.708 0 0)Focus ring, at 50 %

Charts

bg-chart-1oklch(0.87 0 0)Series 1
bg-chart-2oklch(0.556 0 0)Series 2
bg-chart-3oklch(0.439 0 0)Series 3
bg-chart-4oklch(0.371 0 0)Series 4
bg-chart-5oklch(0.269 0 0)Series 5

Sidebar

bg-sidebaroklch(0.985 0 0)Sidebar surface
bg-sidebar-foregroundoklch(0.145 0 0)Sidebar text
bg-sidebar-primaryoklch(0.205 0 0)Sidebar primary
bg-sidebar-primary-foregroundoklch(0.985 0 0)Text on sidebar primary
bg-sidebar-accentoklch(0.97 0 0)Active and hovered items
bg-sidebar-accent-foregroundoklch(0.205 0 0)Text on active items
bg-sidebar-borderoklch(0.922 0 0)Sidebar hairline
bg-sidebar-ringoklch(0.708 0 0)Sidebar focus ring

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.

Dangerred-50 · red-500Errors and destructive outcomes
Warningyellow-50 · yellow-500Risky states, limits almost reached
Successgreen-50 · green-500Confirmed outcomes
Infoblue-50 · blue-500Product information
Neutralneutral-50 · neutral-500General notes — final use to be defined
  • 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 neutral family, 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.

UtilityWhere
hover:bg-primary/80Default button hover
hover:bg-mutedOutline, ghost and toggle hover
bg-destructive/10 text-destructiveDestructive button and badge
ring-ring/50Focus ring
ring-foreground/10Card, dialog and menu edge
bg-black/10Dialog and sheet scrim
text-foreground/60Inactive 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

text-xs · 12pxSend the invite before noonBadges, kbd, helper text, sidebar group labels
text-sm · 14pxSend the invite before noonControls, table cells, card body, menus
text-base · 16pxSend the invite before noonBody text, card titles, inputs on touch screens
text-lg · 18pxSend the invite before noonEmpty-state titles, dialog titles
text-xl · 20pxSend the invite before noonSmall headings
text-2xl · 24pxSend the invite before noonSection titles, stat values
text-3xl · 30pxSend the invite before noonStat values on wide cards
text-4xl · 36pxSend the invite before noonDisplay
text-5xl · 48pxSend the invite before noonWordmark

Weights

WeightUtilityUse
400font-normalBody, descriptions
500font-mediumButtons, labels, card titles, tabs, active nav
600font-semiboldSection headings, stat values
700font-boldWordmark

Rules

  • Headings use font-heading (an alias of the sans face) with tracking-tight. Body keeps the default tracking and leading-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 md breakpoint 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

0.52px
14px
1.56px
28px
2.510px
312px
416px
624px
832px
1248px

Control sizes

Live controls at every size the components ship. Text stays 14 px in all of them.

20 px

Fixed measurements

MeasureValue
Control height36 px default · 32 px small · 40 px large · 24 px extra small
Control padding10 px (px-2.5); 8 px when an icon leads or trails
Badge20 px tall, 8 px padding
Card padding24 px (--card-spacing); 16 px with size="sm"
Gap between cards16 px, 24 px from the md breakpoint (gap-4 md:gap-6)
Page padding16 px, 24 px from the lg breakpoint (px-4 lg:px-6)
Sidebar width16 rem (256 px); 18 rem on mobile; 3 rem collapsed to icons
Console sidebar18 rem (288 px) — dashboard-01 overrides --sidebar-width
Navbar4 rem (64 px), fixed — --header-height in the console
Sidebar menu button32 px default · 28 px small · 48 px large
Dialog width28 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

6px
rounded-sm0.6 × base · Kbd, select items
8px
rounded-md0.8 × base · Buttons, inputs, menus, toggles
10px
rounded-lg1 × base · Alerts, empty states, tab lists
14px
rounded-xl1.4 × base · Cards, dialogs
18px
rounded-2xl1.8 × base ·
22px
rounded-3xl2.2 × base ·
26px
rounded-4xl2.6 × base · Badges (reads as a pill)
full
rounded-full9999 px · Avatars, radios, switches

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

shadow-xs
Buttons (outline), inputs, cards, checkboxes, switches, toggles
shadow-sm
Active tab, floating and inset sidebars
shadow-md
Dropdown and select menus
shadow-lg
Sheets, submenus
shadow-xl
Chart tooltips
  • 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

Six variants: default, secondary, outline, ghost, link, destructive. The destructive variant is a tint, not a solid — Vega keeps red quiet until it is pressed.

components/reference/examples/button-variants.tsx

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.

components/reference/examples/button-sizes.tsx

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.

components/reference/examples/input-demo.tsx

Enter all 16 digits.

Textarea

As Input. Grows with its content (field-sizing: content) from a 64 px minimum.

components/reference/examples/textarea-demo.tsx

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

components/reference/examples/select-demo.tsx

Checkbox

16 px, rounded-sm; checked fills primary. Use a Checkbox when the change needs saving.

components/reference/examples/checkbox-demo.tsx

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.

components/reference/examples/radio-group-demo.tsx

Switch

A switch applies on toggle. If the change needs a Save button, it is a Checkbox.

components/reference/examples/switch-demo.tsx

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.

components/reference/examples/field-demo.tsx
Workspace

Shown to everyone you invite.

Use letters, numbers and hyphens only.

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.

components/reference/examples/card-demo.tsx
Invite a teammate
They get access to every project in this workspace.

Badge

20 px pill. Variants default, secondary, outline, ghost, destructive, link. Status badges use outline with a leading 12 px icon.

components/reference/examples/badge-demo.tsx
DefaultSecondaryOutlineGhostDestructiveLinkDoneIn process

Table

Rows wash to muted on hover. Numeric cells are right-aligned with tabular-nums. Status is a Badge.

components/reference/examples/table-demo.tsx
InvoiceCustomerStatusAmount
INV-1041NorthwindPaid$1,200.00
INV-1042ContosoPending$480.00
INV-1043FabrikamPaid$3,150.00
INV-1044TailspinOverdue$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.

components/reference/examples/notice-cards.tsx

Toast (Sonner)

Toasts confirm an outcome and leave. One Toaster in app/layout.tsx; call toast() anywhere. It inherits the popover colors.

components/reference/examples/sonner-demo.tsx

Tabs

Two list variants: default (a muted tray) and line (an underline). The active trigger takes the foreground color.

components/reference/examples/tabs-demo.tsx
Your name, email and password.
Your name, email and password.

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.

components/reference/examples/dialog-demo.tsx

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.

components/reference/examples/sheet-demo.tsx

Tooltip

Foreground on background, 12 px text. One TooltipProvider wraps the app in app/layout.tsx.

components/reference/examples/tooltip-demo.tsx

Separator

A 1 px line in the border color, horizontal or vertical.

components/reference/examples/separator-demo.tsx

Workspace settings

Name, timezone and default locale.

GeneralMembersBilling

Avatar

Round, with initials as the fallback. AvatarGroup overlaps several and can end with a count.

components/reference/examples/avatar-demo.tsx
CNDM
ALJTEL
+3

Toggle group

A segmented control. Single selection with multiple={false}; spacing={0} joins the items into one bar.

components/reference/examples/toggle-group-demo.tsx

Button group

Joins buttons, inputs and text into one control.

components/reference/examples/button-group-demo.tsx
https://

Item

A list row with media, content and actions. Variants default, outline and muted.

components/reference/examples/item-demo.tsx
Q3 report

Updated 2 hours ago by Dana

Usage this month

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.

components/reference/examples/empty-demo.tsx
No projects yet
Create your first project to start tracking work.

Kbd

Keyboard hints, 12 px on a muted chip.

components/reference/examples/kbd-demo.tsx
Search /SaveSToggle sidebar B

Spinner

A 16 px loader that inherits the current color. Inside a button, disable the button while it spins.

components/reference/examples/spinner-demo.tsx
Syncing

Skeleton

Placeholder shapes in the accent color while content loads.

components/reference/examples/skeleton-demo.tsx

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

StateTreatmentClasses
Hover, solidBackground to primary/80hover:bg-primary/80
Hover, quietOutline, ghost, toggles and rows wash to mutedhover:bg-muted
PressMoves down one pixel. No scale, no color changeactive:translate-y-px
Focus3 px ring in the ring color at 50 %, border takes the ring colorfocus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:border-ring
InvalidDestructive border and a 20 % destructive ringaria-invalid:border-destructive aria-invalid:ring-destructive/20
Disabled50 % opacity; buttons ignore the pointer, inputs show not-alloweddisabled:opacity-50
LoadingA Spinner replaces the leading icon and the button is disabled<Button disabled><Spinner />Saving</Button>
ExpandedTriggers that open a menu keep the hover wash while openaria-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.

WhereWhat moves
Controlstransition-all on buttons; color and box-shadow on inputs. Default duration, default easing.
Menus, dialogsfade-in-0 zoom-in-95 from tw-animate-css, 100 ms.
SheetsSlide from the edge with a 200 ms ease-in-out.
SidebarWidth and margin, 200 ms linear, when it collapses.
SwitchThe knob slides with the component's own transition.
Everything elseNothing. 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

LayoutDashboard
ChartBar
Folder
FileText
Users
Settings2
Search
Bell
Calendar
Plus
Download
Copy
Trash
Check
X
ChevronDown
EllipsisVertical
CircleCheck
CircleAlert
TrendingUp

Sizes

SizeUtilityWhere
16 pxsize-4Inside buttons, inputs, menus, alerts, sidebar items — applied by the component
12 pxsize-3Inside badges and extra-small buttons
20 pxsize-5Sidebar header tile
24 pxsize-6Empty-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.

Sidebar

shadcn's sidebar-03 with a Traqto header: a 64 px logo row — the tile and the wordmark, nothing else — whose bottom border continues the navbar's line. Below it, one group of sections, each opening a submenu (SidebarMenuSub). The current page is the active sub-item; parents are plain links in medium weight. A SidebarRail on the edge collapses it. Here it is rendered in the flow with collapsible='none'; in the console it is fixed and becomes a drawer under 768 px.

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.

components/site-header.tsx

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.

components/reference/examples/page-header.tsx

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.

components/section-cards.tsx
Total Revenue
$1,250.00
+12.5%
Trending up this month
Visitors for the last 6 months
New Customers
1,234
-20%
Down 20% this period
Acquisition needs attention
Active Accounts
45,678
+12.5%
Strong user retention
Engagement exceed targets
Growth Rate
4.5%
+4.5%
Steady performance increase
Meets growth projections

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.

components/chart-area-interactive.tsx
Total Visitors
Last 3 months

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.

components/data-table.tsx
HeaderSection TypeStatus
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
Page 1 of 7

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.

components/reference/examples/form-panel.tsx
Workspace settings
Name, timezone and notifications for everyone in this workspace.

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.

components/reference/examples/empty-state.tsx
No documents yet
Upload a file or create one to get started.
No results
Nothing matches “quarterly”. Try a different term.
Could not load documents
Check your connection and try again.

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

DecisionValue
AnatomySidebarProvider → AppSidebar from sidebar-03 + SidebarInset → navbar → page header → stat cards → chart card → data table
Page groundSidebar 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.
Sidebarsidebar-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
Navbar4 rem (64 px), fixed at the top; trigger, breadcrumb, search, profile. The page title lives in the page header below it
ContentFills the inset. Padding px-4 lg:px-6, blocks stacked with gap-4 md:gap-6
GridsContainer queries on @container/main — 1, 2 or 4 stat cards across
ScrollingOnly the content scrolls. The sidebar is fixed and scrolls its own list; the navbar never moves
Density36 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.

RuleIn practice
CasingSentence case for headings, buttons, labels, menu items. Never Title Case or all caps.
ButtonsImperative verbs: Save changes, Send invite, Delete.
LabelsShort nouns: Card number, Default timezone.
SuccessState the outcome and stop: Invitation sent.
ErrorsState the fix, not just the failure: Update your billing details to continue.
ConfirmationsName the consequence: This removes the project and its 12 documents.
PunctuationMinimal. Exclamation marks sparingly, only in positive toasts.
EmojiNone, anywhere.
Numbers$49, $1,200, UTC−5 — with a real minus sign. Tabular numerals in columns.
AddressThe 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

  1. Change values in :root only. Keep every token name.
  2. Keep the OKLCH format the CLI uses, and check contrast for primary-foreground on primary and muted-foreground on background.
  3. Do not touch components/ui. If a component looks wrong after the change, the value is wrong, not the component.
  4. The approved Traqto mapping is in DESIGN-SYSTEM.md.