Design system · v3.17
A design system for everything we build.
@comitor/ui is the one package behind every Comitor product: primitives, composites, the application shell and whole screens to copy from, in English and Vietnamese.
pnpm add @comitor/uiReact 19 · Tailwind v4 · TypeScript strict · ESM only · MIT licensed
What’s in the box
Three tiers, one package, nine doors
Each tier is built only from the ones below it, and the site follows the same split. The package is cut by what each part needs installed, not by topic: the main entry has no optional peer at all, so an app only installs what it imports.
Components
54
Tier 1. shadcn/ui primitives, retuned so every colour goes through a role token.Composites
48
Tier 2. Tables, boards, steppers, pickers and inputs with the hard parts done.Shell
7
Tier 3. The app frame: sidebar, header, switchers, user menu, display axes.Patterns
10
Whole product screens built from the three tiers, with the code beside them.
Each count is the number of pages in that section of this site, read when the site is built.
@comitor/uiTier 1 and Tier 2, ComitorLocaleProvider with the English and Vietnamese tables, cn and the tokens. Runs anywhere React does — Vite, Storybook, a test runner.
Needs installed: No optional peer
@comitor/ui/shellTier 3: AppShell and its parts, the theme and display-axis providers, and the brand marks.
Needs installed:
nextnext-themes@comitor/ui/chartshadcn’s Chart primitives, plus ready-made line, bar and area charts and CHART_COLORS.
Needs installed:
recharts@comitor/ui/data-gridDataGrid: virtualised rows, keyboard range selection, copy and paste with Excel, pinned columns, filters, sorting, undo.
Needs installed:
@tanstack/react-table@comitor/ui/uploaderFileUpload, ImageUploadField and useFileUpload — Uppy driven headless, so the markup stays the package’s.
Needs installed:
@uppy/core@uppy/xhr-upload@comitor/ui/editorMarkdownEditor, which stores Markdown with a Linear-style bubble, and RichTextEditor, which stores HTML with a pinned toolbar — one Tiptap core underneath, and all thirteen @tiptap packages at one version.
Needs installed:
@tiptap/*lowlight@comitor/ui/chatThe frame of an AI assistant conversation: a thread that sticks to the bottom, messages drawn by the read-only MarkdownEditor with an “AI-generated” label, an agent's steps, sources, and a composer where Enter sends.
Needs installed:
@tiptap/*lowlight@comitor/ui/formThe react-hook-form binding: Form, FormField, FormMessage and the rest of the shadcn API.
Needs installed:
react-hook-form@comitor/ui/tokensToken values as plain objects, for canvas, PDF and HTML email. Never for Tailwind code — use the class.
Needs installed: No optional peer
Nine entries, plus styles.css. The installation guide has the peers for each, and the two names that really do mean different things at different doors. Installation
Rendered live
The real components, running on this page
Nothing below is a screenshot. Every tile imports from the published @comitor/ui 3.17.3 — edit the grid, drag the cards, hover the chart, tab to the avatars.
Data Grid
Docs: Data GridA spreadsheet for bulk entry. Double-click a cell to edit it, drag across cells to select a range, copy it, and paste straight from Excel.
Chart
Docs: ChartLine, bar and area charts on the chart tokens, with tooltips in the reader’s number format.
Status Pill
Docs: Status PillEvery tone in all three variants. Text and borders use each colour’s -ink step, measured at 4.5:1 or better in both themes.
Kanban
Docs: KanbanDrag a card with a mouse or a finger — or focus it, press Space, and move it with the arrow keys. Every move is announced to screen readers.
Quote the Lotus Office Park chargers
Book the pole inspection crane
Onboard Linh Võ to the Field app
Renew the portal’s TLS certificate
Publish the Q4 tariff sheet
Avatars
Docs: AvatarsInitials take the family and the given name — Nguyễn Văn An is NA, not NV — and the tone comes from the name, so one person is one colour on every screen.
Beyond the components
More than a component kit
The expensive part of a design system is not a button. It is language, colour, accessibility and the server boundary — decided once, written down, and checked before every release.
One provider for language and format
English is the default since 3.0.0, and Vietnamese is built in. One ComitorLocaleProvider at the root switches every string the package renders — visible text, accessible names, drag-and-drop announcements — and every format behind them: dates, numbers, relative time, the 12- or 24-hour clock.
Any other language starts from the English table and passes messages. Region stays separate: the reader’s language never decides a workspace’s currency or country.
Invoice INV-0431
Created
Colour is a contract
Every hue ships as three tokens, and the suffix says where each may appear. Components never name a raw colour, so light, dark and the high-contrast palette all follow from one set of classes.
--x- Fills a background, solid or as a tint.
--x-foreground- Text and icons sitting on that solid fill.
--x-ink- The same colour on the page: text, lone icons, borders.
Measured, not eyeballed: on white, text-success reads 3.13:1, under AA; text-success-ink reads 6.52:1. The default palette’s few known gaps are listed on the Colors page, and the high-contrast palette is the one the build fails over.
Five display axes, set by each user
Light or dark, contrast, the density of rows, the size of text and the brand colour — independent of each other, one attribute on <html> each, remembered per device. Try them: they apply to this whole site.
Darkens borders, switches and secondary text. Helpful on dim screens or outdoors.
Reduces vertical spacing between rows in tables, lists and menus.
Nothing ships ungated
Every control works from a keyboard, every name a screen reader speaks can be translated, and where a component knowingly falls short its page says so. Before a version reaches npm, 15 steps have to pass:
typecheckStrict TypeScript over the whole source.test:i18nNo Vietnamese text outside the Vietnamese tables.buildOne ES module per source file, so each keeps its own directive.verify:directivesEvery “use client” survives the build; no plain constant sits in a client file.smoke:entriesAll nine entries load in plain Node, through the exports map — and resolve under its require condition too, the path webpack takes for next/dynamic.check:exportspublint and Are the Types Wrong on the published shape.test:nav-activeThe sidebar lights the right row, queries included.test:grid-contractNo layout class that silently breaks a grid it is dropped into.test:grid-renderDataGrid rendered for real with react-dom/server.test:editor-markdownDocument → Markdown → document comes back unchanged, and the HTML the editors write keeps stored values, not drawn ones.test:gantt-treeGantt's day arithmetic, dependency rules and arrow routes, TreeView's move contract, roll-ups and numbering, and both rendered on the server in two languages.test:dom-renderMounted and re-rendered with react-dom/client on happy-dom — what is right on first render and wrong after a prop changes, like an avatar losing its image.test:a11y-gateThe contrast gate still catches every case that once slipped past it.lint:a11yContrast measured on compiled CSS, in both palettes and both themes.lint:palettes
Built for Server Components
The package ships one module per source file, so "use client" sits only in the files that need it and every constant lives in a module a Server Component can read. What cannot cross the boundary fails loudly instead of arriving as undefined.
This page is one: the counts near the top come from StatCard, rendered on the server. A prop that carries a function — a formatter, a date-fns locale — goes through a small client file of your own.
// app/products/[id]/page.tsx — a Server Component
import { Rating } from '@comitor/ui'
// ✓ strings, numbers and booleans cross to a client component
<Rating value={4.5} step={0.5} readOnly aria-label="Rated 4.5 out of 5" />
// ✗ a function does not — the page fails to render with
// "Functions cannot be passed directly to Client Components…"
<Rating value={4.5} readOnly itemLabel={(value) => `${value} étoiles`} />Patterns
Whole screens to copy from
Ten product screens assembled from the package, live, with the code beside each one. Start from the one closest to yours.
Web App
The whole frame: shell, workspace notices and a first-run tour, KPI cards, a members table with bulk actions, create and edit flows, setup and settings.
AppShellDataTableTourStepperCRM
A contact grid that filters, sorts and edits in place, the record behind the focused row, the deal board and the numbers it rolls up to.
DataGridKanbanKeyValuePhoneInputTasks
A task tracker: an editable task grid with a bulk-action bar, details edited in place, @mentions, a drag-and-drop board and a roadmap.
DataGridActionBarKanbanMentionHR
Nine screens of people management: dashboard, directory and profile, recruiting board and scorecard, onboarding, leave, attendance and payroll.
KanbanStepperDateRangePickerTimePickerCommerce
A storefront and its back office: product page, reviews, cart and a checkout that validates each step, then orders, inventory and a product editor.
StepperRatingDataGridCropperEmail
A three-pane mail client: folders, a filterable list whose labels fold into +N, a collapsible thread, bulk actions and a composer with send-later.
BadgeOverflowActionBarTagsInputTimePickerChat
A team inbox: filtered conversations, a grouped transcript, a composer with @mentions and send-later, presence, typing and delivery.
MentionTimePickerSearchFilterBarSocial
An invite-only network: a composer that names its audience, a feed, a member directory, private spaces, an activity inbox and privacy controls.
MentionCropperTagsInputMobile
A dispatch app at 375px: the sidebar handing over to MobileMenu, lists in two densities, detail in a drawer, sign-in, and a long form cut into steps.
MobileMenuResponsiveDialogStepperInputOTPLanding Page
Fourteen bands that stack into a public website, from the navbar to the footer — mostly layout on purpose, reaching for the package where it has the part.
BannerNavigationMenuAccordionRating
Quick start
From an empty Next app to the first component
Three steps take an empty Next app to its first rendered component. The installation guide has the rest — the optional peers, the PostCSS config, the language in a real product — and the mistake each step prevents.
01
Install the package and its peers
React 19 and Tailwind v4 are required; next, recharts and the rest only for the entry that needs them.
terminalpnpm add @comitor/ui pnpm add react react-dom lucide-react pnpm add -D tailwindcss @tailwindcss/postcss # optional peers — only for the entries you import (see Installation)02
Load the stylesheet
Tailwind first, then the package. The tokens and both palettes arrive with it.
app/globals.css/* app/globals.css — exactly these two lines, in this order */ @import "tailwindcss"; @import "@comitor/ui/styles.css";03
Wire the root layout
The font, the language, and the display axes — independent providers, so the nesting order does not matter.
app/layout.tsx// app/layout.tsx — a Server Component import { ComitorLocaleProvider } from '@comitor/ui' import { ContrastProvider, DensityProvider, FontSizeProvider, ThemeProvider } from '@comitor/ui/shell' import { Inter } from 'next/font/google' import './globals.css' // The package consumes --font-inter; it never loads a font itself. const inter = Inter({ subsets: ['latin', 'vietnamese'], weight: ['400', '500', '600', '700', '800'], display: 'swap', variable: '--font-inter', }) export default function RootLayout({ children }: { children: React.ReactNode }) { return ( // suppressHydrationWarning is required: the display providers touch // <html> before React hydrates, so server and client HTML never match. <html lang="vi" suppressHydrationWarning className={inter.variable}> <body className="min-h-dvh bg-background font-sans text-foreground antialiased"> {/* No provider renders English. A product passes the user's own language here rather than pinning one. */} <ComitorLocaleProvider locale="vi"> <ThemeProvider> <ContrastProvider> <DensityProvider> <FontSizeProvider>{children}</FontSizeProvider> </DensityProvider> </ContrastProvider> </ThemeProvider> </ComitorLocaleProvider> </body> </html> ) }
Latest release
@comitor/ui 3.17.3
An Avatar whose image is taken away shows its initials again.
1 fix · nothing breaks