@unity-advisory/ui
v1.2.0
Published
Unity Advisory's React component library — accessible, themeable, Tailwind v4 powered.
Maintainers
Readme
@unity-advisory/ui
Unity Advisory's open-source React component library. Built for production apps that need to look consistent, ship fast, and stay accessible.
@unity-advisory/ui is a comprehensive, accessible, themeable React component library powered by Tailwind CSS v4, Radix UI, and the Unity Advisory design system. It ships 80+ components, a Recharts-based charting layer, a curated icon set, a hooks library, and a token-based theming system — all in one tree-shakeable npm package with first-class TypeScript support.
If you've used shadcn/ui, MUI, Mantine or Chakra UI, you'll feel at home.
Table of contents
- Why this library?
- Quick start
- Building a dashboard (recipe)
- Installation
- Framework setup
- Project setup
- Import strategies
- Theming
- Component catalog
- Hooks
- Utilities
- Polymorphism: the
asChildpattern - Forms
- Charts
- Icons
- Accessibility
- Server Components & SSR
- TypeScript
- Browser support
- Versioning
- Documentation & playground
- Contributing
- License
Why this library?
| | @unity-advisory/ui |
| --- | --- |
| Accessible by default | Every interactive primitive is built on Radix UI, React Aria, or other audited headless libraries. WCAG 2.2 AA target. axe-core in CI via Storybook a11y addon. |
| Tailwind v4 native | First-class support for Tailwind v4. Tokens live in CSS variables and ship as a Tailwind @theme preset, so utilities like bg-primary, text-muted-foreground, ring-ring Just Work. |
| TypeScript first | Every component is fully typed. Polymorphic asChild props, CVA variants, and discriminated unions for variant props. Strict types on hooks and utilities. |
| Tiny payloads | Per-component subpath exports (@unity-advisory/ui/button), sideEffects map, and tsdown build mean consumers only pay for the components they import. |
| Server Component safe | The 'use client' directive is preserved per-component, so Next.js Server Components stay on the server unless they need to be interactive. |
| Themeable | Three-layer design tokens (primitive → semantic → component) exposed as CSS custom properties. Light, dark, and system modes ship out of the box. |
| Polymorphic | Most primitives accept asChild, so they render as your own element (e.g. <Button asChild><Link href="/x">Open</Link></Button>). |
| Dual ESM / CJS | Native ESM with CJS fallback, validated by publint and @arethetypeswrong/cli in CI. |
Quick start
pnpm add @unity-advisory/ui react react-dom tailwindcss/* app/globals.css (or src/index.css) */
@import "tailwindcss";
@import "@unity-advisory/ui/preset";
@import "@unity-advisory/ui/styles.css";
@source "../node_modules/@unity-advisory/ui/dist";import { ThemeProvider } from '@unity-advisory/ui';
import { Button } from '@unity-advisory/ui/button';
export default function App() {
return (
<ThemeProvider defaultTheme="system">
<Button>Get started</Button>
</ThemeProvider>
);
}That's the whole installation. The rest of this README covers everything else you'd want to do.
Building a dashboard (recipe)
This is the canonical layout for a product page in @unity-advisory/ui. Copy it verbatim and adapt the content — every spacing, header, sidebar and grid concern is handled by the library, so you don't have to remember gap-* utilities or compose AppShell by hand.
'use client';
import {
DashboardShell,
PageHeader,
Section,
Sidebar,
SidebarContent,
SidebarHeader,
SidebarItem,
SidebarProvider,
SidebarSection,
StatCard,
StatGrid,
ThemeToggle,
Button,
Avatar,
AvatarFallback,
Badge,
Card,
CardContent,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@unity-advisory/ui';
import { Bell, Briefcase, DollarSign, LayoutDashboard, Plus, Users } from 'lucide-react';
export default function OverviewPage() {
return (
<SidebarProvider>
<DashboardShell
header={
<div className="flex w-full items-center gap-3">
<span className="text-sm font-semibold">Overview</span>
<div className="ml-auto flex items-center gap-2">
<ThemeToggle />
<Avatar size="sm">
<AvatarFallback>AT</AvatarFallback>
</Avatar>
</div>
</div>
}
sidebar={
<Sidebar>
<SidebarHeader>
<span className="text-sm font-semibold">UA Design System</span>
</SidebarHeader>
<SidebarContent>
<SidebarSection title="Navigate">
<SidebarItem icon={<LayoutDashboard />} active>
Overview
</SidebarItem>
<SidebarItem icon={<Users />} badge={<Badge variant="secondary">12</Badge>}>
Pipeline
</SidebarItem>
<SidebarItem icon={<Bell />}>Feedback</SidebarItem>
</SidebarSection>
</SidebarContent>
</Sidebar>
}
>
<PageHeader
title="Component showcase"
description="A compact dashboard exercising layout, data, forms, overlays, and theme switching."
actions={<Button leftIcon={<Plus className="size-4" />}>New mandate</Button>}
/>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="data">Data & forms</TabsTrigger>
<TabsTrigger value="feedback">Feedback</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-8 pt-6">
<StatGrid>
<StatCard label="Active mandates" value="24" icon={<Briefcase className="size-4" />} delta={8.1} description="vs prior quarter" />
<StatCard label="Weighted pipeline" value="£4.2M" icon={<Users className="size-4" />} delta={2.0} description="74 normalised" />
<StatCard label="Avg. cycle" value="38 days" delta={-5} description="Discovery → proposal" />
<StatCard label="Advisory fees YTD" value="£1.05M" icon={<DollarSign className="size-4" />} delta={5.0} inverse description="Recognised revenue" />
</StatGrid>
<Section title="Quarterly revenue" description="Bar chart with themed series colours.">
<Card>
<CardContent className="h-64" />
</Card>
</Section>
</TabsContent>
</Tabs>
</DashboardShell>
</SidebarProvider>
);
}Why every piece exists
| Primitive | What it gives you |
| --- | --- |
| DashboardShell | Composes AppShell + AppShellMain + AppShellHeader + AppShellContent in the right order so header/sidebar/children are always laid out correctly. |
| Sidebar + SidebarProvider | Persistent, collapsible left navigation with keyboard support. SidebarProvider is required so collapse state can be remembered across reloads. |
| PageHeader | Title + optional description + actions block. Hard-codes the right typography scale and bottom border so every page top looks the same. |
| StatGrid | Auto-fitting grid for KPI tiles. Drop <StatCard>s in and never think about gap-4 again — the spacing is automatic and responsive. |
| Section | Titled content region with consistent vertical rhythm. Use one per logical block (Engagements, Workspace access, Feedback…). |
| space-y-8 (default in DashboardShell) | Vertical spacing between top-level children (header, tabs, sections) is handled for you. |
Common mistakes (and what to use instead)
| Don't | Do |
| --- | --- |
| <div className="grid grid-cols-4">{cards}</div> (no gap) | <StatGrid>{cards}</StatGrid> |
| Roll your own <header><h1>…</h1></header> per page | <PageHeader title="…" description="…" actions={…} /> |
| Stack AppShell + Sidebar + <main> by hand | <DashboardShell header={…} sidebar={…}> |
| <h2 className="mb-4">…</h2> per section | <Section title="…" description="…">…</Section> |
| <div className="space-y-6"> between sections | Already provided by DashboardShell's default content rhythm. |
Open Recipes → Dashboard in Storybook for a full, runnable version of this page including a DataTable, banners, and a progress bar.
Installation
@unity-advisory/ui is published to the npm registry. Pick whichever package manager you use:
# pnpm (recommended)
pnpm add @unity-advisory/ui
# npm
npm install @unity-advisory/ui
# yarn
yarn add @unity-advisory/ui
# bun
bun add @unity-advisory/uiYou'll also need the peer dependencies if your project doesn't already have them:
pnpm add react react-dom tailwindcss| Peer | Supported versions |
| --- | --- |
| react | ^18.0.0 \|\| ^19.0.0 |
| react-dom | ^18.0.0 \|\| ^19.0.0 |
| tailwindcss | ^4.0.0 |
Note: Tailwind CSS v4 is required. v3 is not supported. If you're on v3, see the Tailwind v4 upgrade guide.
Framework setup
Next.js (App Router)
Next.js 15 with the App Router is the recommended setup.
1. Install:
pnpm add @unity-advisory/ui react react-dom tailwindcss @tailwindcss/postcss2. PostCSS config (postcss.config.mjs):
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};3. Global CSS (app/globals.css):
@import "tailwindcss";
@import "@unity-advisory/ui/preset";
@import "@unity-advisory/ui/styles.css";
@source "../node_modules/@unity-advisory/ui/dist";4. Root layout (app/layout.tsx):
import type { Metadata } from 'next';
import { ThemeProvider, Toaster } from '@unity-advisory/ui';
import './globals.css';
export const metadata: Metadata = {
title: 'My App',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider defaultTheme="system">
{children}
<Toaster />
</ThemeProvider>
</body>
</html>
);
}5. (Optional) next.config.mjs — opt into optimized package imports for slightly faster dev builds:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
optimizePackageImports: ['@unity-advisory/ui'],
},
};
export default nextConfig;Next.js (Pages Router)
Identical to App Router, except instead of app/layout.tsx you wrap pages/_app.tsx:
import type { AppProps } from 'next/app';
import { ThemeProvider, Toaster } from '@unity-advisory/ui';
import '../styles/globals.css';
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider defaultTheme="system">
<Component {...pageProps} />
<Toaster />
</ThemeProvider>
);
}Vite + React
1. Install:
pnpm add @unity-advisory/ui react react-dom tailwindcss @tailwindcss/vite2. Vite config (vite.config.ts):
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});3. Global CSS (src/index.css):
@import "tailwindcss";
@import "@unity-advisory/ui/preset";
@import "@unity-advisory/ui/styles.css";
@source "../node_modules/@unity-advisory/ui/dist";4. Mount it (src/main.tsx):
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider, Toaster } from '@unity-advisory/ui';
import App from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider defaultTheme="system">
<App />
<Toaster />
</ThemeProvider>
</StrictMode>,
);Remix
Remix uses PostCSS by default. Install @tailwindcss/postcss, mirror the Next.js Pages setup, and wrap your app/root.tsx <Outlet /> with ThemeProvider.
Project setup
If your framework isn't covered above, here's what you need in any setup.
1. Install peer dependencies
pnpm add @unity-advisory/ui react react-dom tailwindcss2. Configure CSS
Create (or edit) the global stylesheet that's loaded once at the root of your app — typically app/globals.css, src/index.css, or similar:
/* 1. Tailwind itself */
@import "tailwindcss";
/* 2. Our Tailwind theme preset (design tokens, colors, fonts, radii, etc.) */
@import "@unity-advisory/ui/preset";
/* 3. Our base styles (CSS variables, sensible defaults, dark-mode mapping) */
@import "@unity-advisory/ui/styles.css";
/* 4. Optional: your own overrides */
:root {
--radius: 0.625rem;
}The order matters: Tailwind first, then preset, then base styles. Your own customizations come last.
3. Tell Tailwind where to scan
Tailwind v4 only generates CSS for classes it can find in your source. Because our component class names live in node_modules, you must add an @source directive so Tailwind scans the library too:
@source "../node_modules/@unity-advisory/ui/dist";Adjust the relative path to match where your global CSS lives. The path must resolve to the
distfolder inside@unity-advisory/uifrom the location of the CSS file.
If you'd rather not use @source, you can instead use @source inline() in your CSS to include the classes manually, or pin a safelist via @theme inline { ... }. The @source approach is by far the easiest.
4. Add the theme provider
ThemeProvider toggles data-theme="light|dark" on <html> and persists the user's choice in localStorage. Wrap it as close to the root as possible.
import { ThemeProvider } from '@unity-advisory/ui';
<ThemeProvider defaultTheme="system" storageKey="ua-theme">
{children}
</ThemeProvider>| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| defaultTheme | 'light' \| 'dark' \| 'system' | 'system' | Initial theme when nothing is stored. |
| storageKey | string | 'ua-theme' | localStorage key. |
| enableSystem | boolean | true | Whether to track prefers-color-scheme. |
| disableTransitionOnChange | boolean | false | Disables CSS transitions during theme swap to avoid flicker. |
| attribute | 'class' \| 'data-theme' | 'data-theme' | How the theme is applied to <html>. |
If you also want toasts, mount the <Toaster /> once in the same place:
import { ThemeProvider, Toaster } from '@unity-advisory/ui';
<ThemeProvider>
{children}
<Toaster />
</ThemeProvider>5. Use a component
import { Button } from '@unity-advisory/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@unity-advisory/ui/card';
export function Welcome() {
return (
<Card>
<CardHeader>
<CardTitle>Welcome to Unity</CardTitle>
</CardHeader>
<CardContent>
<Button>Get started</Button>
</CardContent>
</Card>
);
}Import strategies
There are three ways to import — use whichever you prefer.
// 1. Per-component subpath (recommended)
// Best tree-shaking, smallest payloads, clearest stack traces.
import { Button } from '@unity-advisory/ui/button';
import { Dialog, DialogContent, DialogTrigger } from '@unity-advisory/ui/dialog';
import { DataTable } from '@unity-advisory/ui/data-table';
// 2. Root barrel
// Convenient, relies on bundler tree-shaking. Fine for most apps.
import { Button, Dialog, DialogContent, DataTable } from '@unity-advisory/ui';
// 3. Subpackages for non-component exports
import { cn } from '@unity-advisory/ui/lib';
import { useDisclosure, useHotkeys, useDebouncedValue } from '@unity-advisory/ui/hooks';
import { ChartContainer, BarChart, Bar } from '@unity-advisory/ui/charts';
import { ArrowRight, Check, Search } from '@unity-advisory/ui/icons';Recommendation: prefer subpaths in production code. They produce the smallest bundles and the clearest debugging experience.
Theming
Light, dark, system
Wrap your tree in ThemeProvider (see setup). Theme switches are instant and persist across reloads.
To read the current theme inside a client component:
'use client';
import { useTheme } from '@unity-advisory/ui';
function Settings() {
const { theme, setTheme, resolvedTheme } = useTheme();
return <button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>Toggle</button>;
}Theme toggle
Drop the prebuilt toggle anywhere:
import { ThemeToggle } from '@unity-advisory/ui';
<ThemeToggle />It's a polymorphic, accessible button that cycles light → dark → system and announces the change to screen readers.
Re-skinning the library
Every visual decision lives in CSS variables, so re-skinning the library is just a CSS override. There are three layers of tokens:
- Primitive tokens — raw HSL channel triplets (
--forest-700,--neutral-50,--amber-500). - Semantic tokens — role-based aliases (
--primary,--background,--ring,--destructive). - Component tokens — per-component overrides (
--sidebar-background,--chart-1,--card).
To rebrand, override the semantic layer in your own CSS. The rest of the system inherits automatically.
:root {
--primary: 158 64% 22%; /* deep forest green */
--primary-foreground: 60 9% 98%;
--ring: 158 64% 22%;
--radius: 0.625rem;
}
[data-theme='dark'] {
--primary: 158 50% 50%;
--background: 240 10% 4%;
--foreground: 0 0% 98%;
}Values are HSL channel triplets (no hsl(...) wrapper) so Tailwind's alpha modifier works:
<div className="bg-primary/50 ring-1 ring-ring/40">…</div>Adding a brand palette
Want a brand-blue palette? Add primitive tokens, then point semantic tokens at them:
:root {
--brand-50: 220 100% 97%;
--brand-500: 220 90% 56%;
--brand-600: 220 90% 48%;
--brand-700: 220 90% 40%;
--primary: var(--brand-600);
--primary-foreground: 0 0% 98%;
--ring: var(--brand-500);
}Tailwind utilities like bg-primary and ring-ring immediately reflect the new palette across every component.
For the full list of tokens (semantic + component), see src/styles/tokens.css.
Component catalog
Every component ships with a 'use client' boundary, polymorphic asChild where applicable, full keyboard support, and props/types autocompletion in your editor.
Building a page? Start from the Building a dashboard recipe —
DashboardShell+PageHeader+StatGrid+Sectionhandle every layout concern (gaps, padding, header placement) for you, so you can focus on content.
| Category | Components |
| --- | --- |
| Inputs | Button, ButtonGroup, IconButton, Toggle, ToggleGroup, Input, Textarea, Select, Combobox, MultiSelect, Autocomplete, Checkbox, Radio, Switch, Slider, RangeSlider, NumberInput, DatePicker, DateRangePicker, Calendar, TimePicker, ColorPicker, FileUpload, PinInput, TagsInput, Rating, PasswordInput, RichTextEditor |
| Layout | Container, Stack, HStack, VStack, Flex, Grid, Spacer, Separator, AspectRatio, ScrollArea, ResizablePanels, AppShell, DashboardShell, Sidebar, PageHeader, Section, StatGrid, Collapsible, Sortable |
| Navigation | Breadcrumbs, Tabs, NavigationMenu, Menubar, Pagination, SimplePagination, Stepper, Link, CommandPalette, ThemeToggle |
| Overlays | Dialog, AlertDialog, Drawer, Sheet, Popover, Tooltip, HoverCard, ContextMenu, DropdownMenu, Toast |
| Data display | Card, StatCard, Badge, Chip, Avatar, AvatarGroup, Table, DataTable (TanStack), VirtualDataTable, List, Tree, Accordion, Carousel, Timeline, EmptyState, Code, Kbd, Highlight, Indicator, Progress, CircularProgress, Skeleton, Spinner |
| Charts | ChartContainer, ChartTooltip, ChartLegend + Recharts re-exports (Bar, Line, Area, Pie, Radar, Scatter, RadialBar, …) |
| Feedback | Alert, Banner, Callout, Toast (via Toaster + toast()) |
| Typography | Heading, Text |
| Form primitives | Form, Field, FieldControl, FieldLabel, HelperText, ErrorMessage, Fieldset |
| Utility | Toolbar, ErrorBoundary, VisuallyHidden, Portal, DirectionProvider, ThemeProvider, ThemeToggle |
Browse every component live with full prop tables and runnable examples in Storybook.
Hooks
A small but useful collection. All exported from @unity-advisory/ui/hooks.
import {
useControllableState,
useDisclosure,
useHotkeys,
useMediaQuery,
useDebouncedValue,
useLocalStorage,
useClickOutside,
useEscapeKey,
useCopyToClipboard,
useId,
useCallbackRef,
useIsomorphicLayoutEffect,
} from '@unity-advisory/ui/hooks';| Hook | Purpose |
| --- | --- |
| useControllableState | Build controllable + uncontrolled props (value / defaultValue / onChange). |
| useDisclosure | Boolean state with open, close, toggle, setOpen — perfect for dialogs/menus. |
| useHotkeys | Bind global keyboard shortcuts (e.g. 'mod+k' for command palette). |
| useMediaQuery | Reactive media-query state, SSR-safe. |
| useDebouncedValue | Debounce any value over a delay. |
| useLocalStorage | Reactive localStorage with cross-tab sync. |
| useClickOutside | Run a callback when a click lands outside a ref. |
| useEscapeKey | Run a callback when the user presses Escape. |
| useCopyToClipboard | [copy, status] tuple with idle / copied / error state. |
| useId | Stable unique IDs for SSR. |
| useCallbackRef | Stable identity wrapper around an inline callback. |
| useIsomorphicLayoutEffect | useLayoutEffect on the client, useEffect on the server. |
Utilities
import { cn } from '@unity-advisory/ui/lib';cn is a clsx + tailwind-merge combination. It joins classnames and resolves Tailwind conflicts:
cn('p-4 text-sm', isActive && 'bg-primary p-6');
// -> 'text-sm bg-primary p-6' (later p-6 wins, conflict resolved)Use it whenever you forward className from a consumer to one of our components.
Polymorphism: the asChild pattern
Most primitives accept asChild to render as your own element. The component still applies its styling, behavior, and ARIA, but the underlying DOM is yours.
import Link from 'next/link';
import { Button } from '@unity-advisory/ui/button';
// Renders an <a> with Next.js client-side navigation, styled as our Button.
<Button asChild>
<Link href="/dashboard">Open dashboard</Link>
</Button>This works for Button, IconButton, Link, DropdownMenu.Item, Sidebar.Item, Toolbar.Button, and many others. It's the cleanest way to integrate with router-specific link components without losing your styling.
Forms
Composable, accessible form primitives that work with any form library — including React Hook Form, which is our recommended default.
import { useForm } from 'react-hook-form';
import { Button } from '@unity-advisory/ui/button';
import { Field, FieldLabel, FieldControl, HelperText, ErrorMessage } from '@unity-advisory/ui/form';
import { Input } from '@unity-advisory/ui/input';
type Values = { email: string };
export function SignIn() {
const { register, handleSubmit, formState: { errors } } = useForm<Values>();
return (
<form onSubmit={handleSubmit((v) => console.log(v))} className="space-y-4">
<Field invalid={!!errors.email}>
<FieldLabel>Email</FieldLabel>
<FieldControl>
<Input type="email" {...register('email', { required: 'Required' })} />
</FieldControl>
<HelperText>We'll never share your email.</HelperText>
{errors.email && <ErrorMessage>{errors.email.message}</ErrorMessage>}
</Field>
<Button type="submit">Sign in</Button>
</form>
);
}Field wires up the id, aria-invalid, aria-describedby, label association, and helper/error messaging automatically — so you don't have to.
Charts
Charts live in their own subpackage so consumers who don't need them don't pay the Recharts cost.
import { ChartContainer, ChartTooltip, BarChart, Bar, XAxis, YAxis, CartesianGrid } from '@unity-advisory/ui/charts';
const data = [
{ month: 'Jan', revenue: 1200 },
{ month: 'Feb', revenue: 1800 },
{ month: 'Mar', revenue: 2100 },
];
<ChartContainer
config={{
revenue: { label: 'Revenue', color: 'hsl(var(--chart-1))' },
}}
>
<BarChart data={data}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip />
<Bar dataKey="revenue" fill="var(--color-revenue)" radius={4} />
</BarChart>
</ChartContainer>ChartContainer accepts a config map keyed by series name. It generates CSS variables (--color-revenue in the example) that Recharts shapes can reference, so chart colors automatically follow your theme.
Five chart palette tokens (--chart-1 … --chart-5) ship by default and recolor in dark mode.
Icons
import { ArrowRight, Search, Check } from '@unity-advisory/ui/icons';
<button>
<Search className="size-4" />
Search
</button>The /icons subpath re-exports a curated subset of lucide-react so consumers don't pull in the entire icon set. Each icon is tree-shakeable.
If you need an icon that isn't re-exported, just import { IconName } from 'lucide-react' directly — it's already a transitive dependency.
Accessibility
Accessibility is non-negotiable. Every component:
- is built on Radix UI, React Aria, or another audited headless library
- meets the WCAG 2.2 AA target
- ships with proper roles, names, states, and keyboard support
- is audited by axe-core in Storybook (a11y addon)
- respects
prefers-reduced-motionandprefers-color-scheme - has visible, high-contrast focus rings
What's still your responsibility:
- Provide labels and accessible names for inputs (
<FieldLabel>makes this trivial) - Set a logical heading hierarchy
- Manage focus on route changes
- Test with a screen reader before shipping
The Storybook a11y panel will catch most issues during development — keep it open while you build.
Server Components & SSR
The library is designed to work seamlessly with React Server Components (Next.js App Router).
- Components that need the browser (state, refs, portals, event listeners) are marked
'use client'. The directive is preserved per-file at build time. - Components that don't (
Card,Container,Separator,Skeleton, layout primitives, …) are server-safe and run on the server.
You can import a server-safe component into a server component without forcing the surrounding page to become a client component. The bundler will only ship the client components that actually need to be hydrated.
// app/page.tsx — Server Component
import { Card, CardContent, CardHeader, CardTitle } from '@unity-advisory/ui/card';
import { Button } from '@unity-advisory/ui/button'; // 'use client' under the hood
export default async function Page() {
const data = await fetch('https://api.example.com').then((r) => r.json());
return (
<Card>
<CardHeader><CardTitle>{data.title}</CardTitle></CardHeader>
<CardContent><Button>Action</Button></CardContent>
</Card>
);
}TypeScript
The package is authored in TypeScript and ships full .d.ts types alongside both ESM (.d.mts) and CJS (.d.cts) outputs. node16 and bundler module resolution are both supported and validated by @arethetypeswrong/cli in CI.
- Polymorphic components (
asChild) infer the correct element props at the call site. - CVA variants are typed as discriminated string unions — autocomplete works for
variant,size, etc. - Hook return types are precise (no
any).
Recommended tsconfig.json settings:
{
"compilerOptions": {
"moduleResolution": "Bundler", // or "Node16"
"module": "ESNext",
"target": "ES2022",
"jsx": "preserve", // (or "react-jsx")
"strict": true
}
}Browser support
Modern evergreen browsers — the last 2 versions of:
- Chrome / Edge
- Firefox
- Safari (desktop and iOS)
The library uses CSS custom properties, container queries (sparingly), and the :has() selector. No legacy shims are shipped.
Versioning
We follow semver. Releases are managed with Changesets and published with npm provenance.
- Major — breaking API change.
- Minor — new components or non-breaking additions.
- Patch — bug fixes, internal refactors, type-only fixes.
Every release ships with a changelog entry detailing the changes. See CHANGELOG.md.
Documentation & playground
- Storybook — every component, every variant, every prop. Run locally with
pnpm storybookor browse the hosted version. - Playground — a Next.js 15 app that mirrors real Unity Advisory dashboards using the library end-to-end. Run with
pnpm --filter playground dev.
Both are great when you want to see how a component is really supposed to be used.
Contributing
Contributions are welcome — issues, PRs, design feedback, all of it.
# clone, install
git clone https://github.com/unity-advisory/ua-component-library.git
cd ua-component-library
pnpm install
# develop
pnpm storybook # docs + interactive playground
pnpm --filter playground dev # Next.js dogfood app
pnpm test:watch # vitest in watch mode
# quality gates (run before PR)
pnpm typecheck
pnpm lint
pnpm test
pnpm build
pnpm publint
pnpm attwWhen adding or changing public API, run pnpm changeset and commit the resulting markdown file alongside your PR. See CONTRIBUTING.md for the full guide, including component conventions and our review checklist.
License
Proprietary — All rights reserved. © 2026 Unity Advisory.
This package is published publicly to npm so that prospective users and Unity Advisory's own products can install it, but the source code is not open source. No licence to use, copy, modify, redistribute, or create derivative works of this software is granted by its mere availability on npm or GitHub.
To use @unity-advisory/ui in your product, you must obtain a written licence from Unity Advisory. See LICENSE for the full terms, or contact [email protected] for licensing enquiries.
