npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@unity-advisory/ui

v1.2.0

Published

Unity Advisory's React component library — accessible, themeable, Tailwind v4 powered.

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?

| | @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 &amp; 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/ui

You'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/postcss

2. 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/vite

2. 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 tailwindcss

2. 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 dist folder inside @unity-advisory/ui from 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:

  1. Primitive tokens — raw HSL channel triplets (--forest-700, --neutral-50, --amber-500).
  2. Semantic tokens — role-based aliases (--primary, --background, --ring, --destructive).
  3. 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 + Section handle 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-motion and prefers-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 storybook or 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 attw

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