admin-ui-starter-kit
v0.9.1
Published
Opinionated React component kit for admin panels and SaaS dashboards, built on top of shadcn/ui + Tailwind CSS v4. Callback-driven, framework-agnostic, i18n-ready.
Maintainers
Readme
Admin UI Starter Kit
Opinionated React components for admin panels, SaaS interiors, and B2B back-offices. It sits above shadcn/ui and Tailwind CSS v4: shadcn provides primitives; this package provides the reusable admin layer those primitives usually become.
npm install admin-ui-starter-kit react react-dom tailwindcss lucide-reactimport 'admin-ui-starter-kit/style.css';
import { Badge } from 'admin-ui-starter-kit/base/badge';
import { Button } from 'admin-ui-starter-kit/base/buttons';
import { Item, ItemActions, ItemContent, ItemMedia, ItemTitle } from 'admin-ui-starter-kit/base/item';
import { Mail } from 'lucide-react';
export function UserRow() {
return (
<Item>
<ItemMedia variant="icon"><Mail /></ItemMedia>
<ItemContent>
<ItemTitle>[email protected]</ItemTitle>
</ItemContent>
<ItemActions>
<Badge variant="success">Active</Badge>
<Button size="small">Open</Button>
</ItemActions>
</Item>
);
}What You Get
- Typography primitives:
Text,Heading,DisplayLabel,Label, andTextLinkwith a dense admin scale. - Semantic display primitives:
Value,Name,Initials,MonoValue,Link,Email,Phone,Url,Money,Number,Percent,FileSize,Duration,Dimensions,Date,Time,DateTime,DateRange, andRelativeTimefor metadata, cards, tables, previews, and activity surfaces. - Base wrappers: buttons, badges, cards, item rows, form fields, comboboxes, tables, navigation menus, popovers, display states, toaster, maps, dates, and more.
- Composed surfaces: admin settings rows, AI surfaces, metrics, commerce cards, data-display blocks, dark surfaces, navigation widgets, and timelines.
- Feature shells: filters, data-view, comments, activities, async preview, media library, schema form, product editor, global search, overlays, Kanban, rich text, sync, and resource pages.
- Layout chrome: page/header/sidebar/auth/settings/workspace shells that stay framework-agnostic.
- Agentic docs: shipped Codex/Claude skills for maintainers and consumer migrations, plus generated component references from the showcase.
- Canonical recipes: live operational index, resource detail, activity,
and AI-assisted compositions under
#/docs/composition-recipes. - Parity tooling: a scoped presentation lab under
#/docs/parity-labplus API, keyboard/focus, performance, accessibility, and visual regression gates.
Current Canonical Primitives
These are the defaults agents and humans should reach for before writing local JSX:
| Need | Use |
| --- | --- |
| Metadata keys, grouped menu labels, column groups | DisplayLabel from admin-ui-starter-kit/typography |
| Displayed values, identities, contact links, money, numbers, durations, dimensions, file sizes, dates | Value, Name, Initials, MonoValue, Link, Email, Phone, Url, Money (including unit="minor" and secondary currency display), Number, Percent, FileSize, Duration, Dimensions, Date, Time, DateTime, DateRange, RelativeTime from admin-ui-starter-kit/primitives |
| Plain table markup | Table, TableHeader, TableBody, TableRow, TableHead, TableCell from admin-ui-starter-kit/base/table |
| Data table behavior and primary/secondary stacked cells | DataTable, CellValue, CellStack from admin-ui-starter-kit/features/table |
| Overflow/page/card/table row actions | ActionMenu / ActionButtons from admin-ui-starter-kit/base/navigation |
| Status chips, tags, small semantic labels | Badge from admin-ui-starter-kit/base/badge |
| Icon/avatar/title/description/action rows | Item from admin-ui-starter-kit/base/item |
| Empty/loading/error surfaces | LoadingState, ErrorState from admin-ui-starter-kit/base/display; EmptyState from admin-ui-starter-kit/composed/data-display |
| Label/value facts | MetadataList, MetadataValue from admin-ui-starter-kit/composed/data-display |
| Relationship card content scaffolds | RelationshipCardFrame from admin-ui-starter-kit/composed/data-display, framed by SmartCard at the page call site |
| Card primary/body action rows | CardPrimaryAction, CardActionStrip from admin-ui-starter-kit/base/cards |
| Editable label/control/error rows | FormField / ControlledFormField from admin-ui-starter-kit/base/forms |
| Editable amount plus currency | MoneyInput from admin-ui-starter-kit/base/forms (optional form chrome, one { amount, currency } callback) |
| Filtered table chrome | DataViewShell and features/filters beside features/table |
| Row/cell/inline preview | AsyncPreview, PreviewTriggerCell from admin-ui-starter-kit/features/async-preview (data for existing payloads, onShow for lazy loading) |
| Mixed comments, events, and audit logs | ActivityLog, toActivityLogEntry, createActivityEventAdapter from admin-ui-starter-kit/features/activities |
DisplayLabel is intentionally one fixed style: text-sm, foreground,
medium, tight leading. Caller typography overrides are ignored so metadata
labels, grouped menu labels, and column-group labels stay visually identical.
Value is intentionally limited to type="main" or type="secondary":
main is the default foreground display-data tone, and secondary is the muted
supporting-value tone. Use Text for broader prose typography roles.
For tables, use CellValue when a column displays one primitive-backed value
and CellStack when a column needs primary plus secondary values such as name,
email, phone, file size, date, person name, or a custom badge. CellStack skips null,
undefined, and blank string values, promotes the first visible item as the
normal value, renders the rest smaller/secondary by default, and caps output at
four rows.
For preview cells, put PreviewTriggerCell inside AsyncPreview.Trigger.
It renders a button and caret only when preview interaction exists; empty,
disabled, or non-previewable values keep the same row footprint without
pretending to be interactive.
For metadata, prefer typed MetadataValue descriptors when a value arrives as
data (email, phone, url, link, date, datetime, money, badge,
mono, empty) and reserve custom React nodes for genuinely custom layout.
For relationship cards, the app owns the domain data and actions while
RelationshipCardFrame owns the consistent region order.
For activity, map raw events with toActivityLogEntry or
createActivityEventAdapter before rendering ActivityLog; user-facing rows
should never expose raw backend event keys.
Why This Exists
Every admin product eventually rebuilds the same layer:
- rows with media, titles, descriptions, and actions;
- form rows with label, control, hint, and validation state;
- dense typography and metadata rules;
- provider-level display defaults;
- table, filter, empty, loading, and error states;
- reusable action menus and overlay actions;
- features that work in Vite, Next.js, Remix, Inertia, Tanstack Router, or any React app without importing those frameworks.
Admin UI Starter Kit centralizes that layer. Consumers wire routing, data,
permissions, translations, uploads, and cache invalidation through callbacks,
fetchers, accessors, render props, and strings props.
Design Contract
- Layered architecture.
ui/is internal shadcn output and is never edited directly. Public code flows throughtypography/,primitives/,base/,layout/,composed/, andfeatures/.npm run lint:architectureenforces the boundaries. - Framework-agnostic behavior. The package imports no app routers, query clients, i18n runtimes, or backend SDKs.
- Exact entrypoints. Import only the family you use:
admin-ui-starter-kit/base/item,admin-ui-starter-kit/features/comments, etc. Broadbase,composed, andfeaturesbarrels are intentionally not public. - Provider + tokens.
<UIProvider>owns JS-side display defaults. CSS variables undersrc/css/tokens/own scale, density, states, controls, rows, surfaces, media, and shell dimensions. Default surface insets use the unsuffixedp-surfacefamily; compact and spacious insets are explicit variants, never alternate defaults. - Typography is structural. Every library-owned title, description,
label, helper/error message, menu label, and accessible-only string renders
through
Text,Heading,DisplayLabel,Label,TextLink, or a semantic display primitive. Arbitrary consumerReactNodeslots stay unwrapped. - Strings are overridable. User-facing text flows through
useStrings(defaults, override). - Slots and render props before forks. Complex features expose partials, slots, hooks, and render props so apps can reshape without copying source.
- Stable DOM hooks. Public components expose
{kebab-name}--componentand named region classes for theming, testing, analytics, and DevTools.
Maintainer Confidence Gates
The package treats public types and presentation behavior as versioned contracts:
npm run api:check # exact public TypeScript API snapshot
npm run test:interaction-contracts # keyboard, Escape, and focus return
npm run test:visual:parity # theme/density/font/text/radius matrix
npm run build:lib
npm run test:performance-budget # CSS, entrypoint, format, and dist budgetsnpm run verify:release includes these checks alongside package smoke,
registered-route browser coverage, axe, feature visual baselines, and the
normal architecture/token/type/lint/test gates.
What This Is Not
- Not a public mirror of
src/components/ui/*; those shadcn files are an internal maintainer layer. - Not a marketing-site kit; it is built for app interiors.
- Not a backend framework; routing, data, auth, permissions, uploads, and i18n stay in the consuming app.
Install
For a full framework and bundler setup walkthrough, see INTEGRATION.md.
Migrating an app that already copied an older local UI tree? Start with MIGRATION.md, then use COMPONENT_SELECTION.md to choose exact package entrypoints.
1. Add the package
npm install admin-ui-starter-kit2. Required peers
Always required:
npm install react react-dom tailwindcss lucide-reactlucide-react is intentionally a v1 peer. Apps still on lucide v0 should
plan that icon-package upgrade as part of adoption.
3. Optional peers — install only what you use
| Feature you import | Install peer |
| --- | --- |
| features/rich-text-editor | @tiptap/pm @tiptap/react @tiptap/starter-kit |
| base/map | leaflet leaflet-draw leaflet.fullscreen leaflet.markercluster react-leaflet react-leaflet-markercluster |
| base/carousel | embla-carousel-react |
| base/display/chart | recharts |
| composed/analytics (charts) | recharts |
| features/data-view, features/products, features/table | @tanstack/react-table |
| base/forms sortable repeaters/media gallery; features/kanban | @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities |
| base/date-pickers; date facets in features/filters | react-day-picker@^10 |
| base/display/qr-code | qrcode culori |
| RHF-backed exports in base/forms; features/sync; features/rich-text-editor | react-hook-form |
| base/toaster, base/copyable, composed/admin | sonner |
| base/command, base/combobox, base/popover-menu; features/filters, features/global-search, features/mentions, features/suggestions | cmdk |
Skip the ones you don't import — npm will warn but not fail. Date picker
calendarProps are forwarded to DayPicker v10, so use v10 prop names
(startMonth / endMonth) rather than deprecated v9 names. The kit-level
yearSelection.fromYear / toYear option remains supported and is mapped
internally.
4. Mount the stylesheet
// in your root layout
import 'admin-ui-starter-kit/style.css';The shipped stylesheet is a pre-compiled Tailwind v4 build (~360 KB minified). If you already run Tailwind in your app, see the Build your own section below to skip the pre-built CSS.
5. Mount the provider (optional but recommended)
// app.tsx
import { UIProvider } from 'admin-ui-starter-kit/ui-provider';
<UIProvider config={{
money: { defaultCurrency: 'USD', locale: 'en-US' },
dates: { weekStartsOn: 1, format: 'dd MMM yyyy' },
forms: { defaultControlSize: 'default', defaultLabelSize: 'default' },
table: { defaultSize: 'small' },
button: { defaultVariant: 'primary', defaultButtonStyle: 'solid' },
badge: { defaultVariant: 'primary', defaultSize: 'xs' },
item: { defaultSize: 'default' },
card: { defaultSurface: 'framed', defaultTitleSize: 'small' },
toast: { position: 'bottom-center', theme: 'dark' },
}}>
<App />
</UIProvider>The provider is locked at first mount; pass everything once at the root.
For sizing and spacing, prefer the calibrated package scale (default /
small) plus structural CSS variables such as --height-control,
--height-action, --control-x, --row-y,
--surface-x, and --space-scale over repeated
per-component size props.
Structural scale is intentionally small. Tune typography with direct text
tokens such as --text-xs, --text-pxs, --text-sm, --text-base, and
their paired --text-*--line-height values when a theme needs exact sizing.
Tune global UI shape with --space-scale and --density-scale first; use the public --space-*,
--control-*, --row-*, --surface-*, --icon*, --avatar*, and shell
variables when a product needs a precise override. Do not recreate old
component-family aliases such as action, card, table, header, or app-shell
spacing tokens in consuming apps.
Buttons and clickable triggers consume the action ladder (--height-action-*);
inputs, selects, and editable fields consume the control ladder
(--height-control-*). Their defaults align by design, but the semantic
families are independent so a scoped product theme can resize actions without
silently resizing form controls. Every public package-owned root token is
documented beside its declaration, and npm run lint:tokens rejects
undocumented tokens or raw interactive geometry that bypasses these families.
The surface ladder is semantic: default cards, dialogs, sheets, panels, and
content/card/multi-section popovers use p-surface (or the matching
px-surface / py-surface split). Item/option/action collections use the
0.5rem p-surface-xs inset—on the outer shell for pure menus and on the list
region for mixed pickers with separately padded chrome. Explicit compact
surfaces may also use *-surface-xs; *-surface-lg is the explicit spacious
tier. *-surface-sm is a deprecated compatibility spelling, not a
package-source or new-consumer default.
Visual theme values stay in CSS, not in <UIProvider>. Override
--link-color for link-like inline values, --success / --info /
--warning for package semantic states, and --inverse-* for explicit dark or inverse
toast surfaces from the theme/defaults layer. --success and --info are
independent semantic state colors, not aliases to primary or chart colors;
their solid fills use the paired foreground token for WCAG AA contrast.
6. Optional action runtime
Use services/actions when the same action should appear in several places
(page header, table row menu, card menu, command palette) and share one
loading/error/overlay lifecycle. Keep one-off dialogs local with
features/overlays.
import {
ActionOverlayOutlet,
ActionProvider,
useLocalAction,
type ActionRequestRunner,
type ActionRuntimeGlue,
} from 'admin-ui-starter-kit/services/actions';
const requestRunner: ActionRequestRunner = async (request) => {
const method = request.method;
const target = request.target;
if (target?.type === 'named') {
return appVisit(appRoute(target.name, target.params), {
...request.options,
method,
data: request.data,
headers: request.headers,
});
}
if (target?.type === 'url') {
return appHttp(target.url, {
...request.options,
method,
data: request.data,
headers: request.headers,
});
}
throw new Error('Action request target is missing.');
};
const actionRuntime: ActionRuntimeGlue = {
requestRunner,
feedback: {
onSuccess: (event) => event.message && toast.success(event.message),
onError: (event) => event.message && toast.error(event.message),
},
guards: {
can: (permission) => auth.can(permission),
hasAnyRole: (roles) => auth.hasAnyRole(roles),
},
parseErrors: (error) => {
const response = error as {
errors?: Record<string, string | string[]>;
message?: string;
};
return {
fieldErrors: response.errors,
message: response.message ?? 'Action failed.',
};
},
onOverlayOpenChange: (open) => polling.setPaused(open),
onRunningChange: (running) => activity.setBusy(running),
};
<ActionProvider runtime={actionRuntime}>
<App />
<ActionOverlayOutlet />
</ActionProvider>Actions can be callback-driven with run, or data-driven through request
plus a provider/action requestRunner. Provider guards keep permission and
role checks app-owned, while parseErrors normalizes backend validation
payloads into action.errors and failed lifecycle messages. Use
useLocalAction for incremental page-owned migrations, and use
<ActionScope> + useActionSurface when one definition needs to power page,
table, card, command, or overlay surfaces.
const confirmBooking = useLocalAction<Booking, { note: string }, Booking>({
id: 'booking.confirm',
label: 'Confirm booking',
permission: 'bookings.confirm',
modality: {
type: 'dialog',
title: ({ payload }) => `Confirm ${payload?.reference}`,
formId: 'confirm-booking-form',
render: ({ getFormProps, isRunning }) => (
<form {...getFormProps()} id="confirm-booking-form">
<textarea name="note" disabled={isRunning} />
</form>
),
},
request: {
method: 'post',
target: ({ payload }) => ({
type: 'named',
name: 'bookings.confirm',
params: payload?.id,
}),
data: ({ values }) => values,
}}
}, { payload: booking });Pages register contextual actions with useRegisterActions or
<ActionScope>. Routing, permissions, translations, cache invalidation, and
toasts stay in the consuming app through callbacks, provider adapters, and
lifecycle hooks. Resolved actions expose status, isRunning, isSuccess,
isError, errors, message, result, and timestamps; overlay form actions
can use modality.formId with getFormProps(...) in the render context.
7. Install the AI agent skills (optional)
This package ships AI skills for Claude Code and other agents:
component-library-rules— maintainer rules for editing this library.admin-ui-components— component selection, layer, and reference workflow.token-system— shadcn theme + structural token workflow.admin-ui-consumer-migration— consumer-app migration rules and guardrails.component-quality-pass— route, visual, API, token, and test audit workflow.
After installing the package:
npx admin-ui-starter-kit-install-skillCopies the skills into your project's .claude/skills/ and
.agents/skills/. Run again to update after a package upgrade.
Flags:
--skill=all|component-library-rules|admin-ui-components|token-system|admin-ui-consumer-migration|component-quality-pass(defaultall)--target=claude|agents|both(defaultboth) — pick which directory to install into.--force— overwrite without prompting.
For migration checks in a consuming app:
npx admin-ui-starter-kit-audit
npx admin-ui-starter-kit-audit --fixThe audit rejects stale copied imports, invalid package subpaths, and local files that only re-export package components.
Browse the component showcase
Every component shipped with this package has a docs page (preview + code tab + props table). To browse the full showcase locally:
npx admin-ui-starter-kit-showcaseOpens http://127.0.0.1:7654 with sidebar navigation across UI / Base / Composed / Features / Layout, search, and per-component examples + APIs.
For agent or text-first discovery, search
.agents/skills/component-library-rules/references/components/INDEX.json.
Each entry exposes the route, status, primary exact publicImport, documented
components in api, example names, and a doc path. Open only the matching
doc: its generated Public API section contains exact named imports and props,
followed by consumer-normalized examples. Empty API metadata on ui/** pages
is intentional because raw shadcn primitives are private.
Auth layouts live under the exact layout subpath:
import { AuthShell } from 'admin-ui-starter-kit/layout/auth';Use them for login, register, invite, reset, magic-link, verification, and two-factor page chrome. The shell covers logo/brand, social provider buttons, policy links, language links or switchers, and full-bleed top media strips. Forms, submit handlers, translations, locale state, and framework links stay in the consuming app.
Media picking and product gallery flows live under the exact feature subpath:
import { MediaLibrary, MediaLibraryDialog } from 'admin-ui-starter-kit/features/media-library';
import { SchemaForm } from 'admin-ui-starter-kit/features/schema-form';Use MediaLibrary for embedded asset management and MediaLibraryDialog when
the picker should open in the shared overlay Dialog. Fetching, uploads,
metadata edits, storage URLs, and framework routing stay in the consuming app
through direct props.
Use SchemaForm for backend-provided settings, tenant configuration,
integration setup, or other admin schemas that should render as one cohesive
form or as section cards. Keep validation libraries, server errors, route
changes, translations, async option fetching, and app-specific controls in the
consuming app through direct props.
Flags:
--port <n>— pick a port (default 7654)--host <h>— bind to a different host (default 127.0.0.1)--no-open— do not auto-open a browser
The bundle is shipped pre-built inside the package; no build step required.
Architecture
Layer order
ui/ → shadcn primitives (DO NOT EDIT)
typography/→ Text, Heading, DisplayLabel, Label, TextLink
primitives/→ Value, Name, Initials, Email, Phone, Money, Number, FileSize, Duration, Dimensions, Date, DateRange, RelativeTime
base/ → typography-aware wrappers (Button, Item, FormField, ...)
composed/ → domain rows + cards (TeamMemberRow, ContactCard, MetricGrid, ...)
features/ → provider-driven feature surfaces (Comments, Filters, Overlays, ...)
layout/ → app-shell scaffolds (PageHeader, Sidebar, ...)Each layer can only import from the layer below it — lint:architecture
enforces this on every PR.
Three pillars
<UIProvider> — library-wide defaults. One zustand store, slice-scoped
subscriptions, locked-once at boot:
const { defaultCurrency } = useMoneyConfig();
const currency = props.currency ?? defaultCurrency;useStrings() — overridable internal copy. Every component owns a
<Name>Strings interface + default<Name>Strings:
<ContactCard strings={{ contact: t('actions.contact') }} /><Item>, <MetadataList>, and <FormField> — canonical row decisions.
Any "media + title + description + actions" row goes through Item.
Any label/value fact summary goes through MetadataList. Any editable
"label + control + error" row goes through FormField/ControlledFormField.
<Item>
<ItemMedia variant="avatar"><Avatar src={user.avatar} /></ItemMedia>
<ItemContent>
<ItemTitle>{user.name}</ItemTitle>
<ItemDescription>{user.role}</ItemDescription>
</ItemContent>
<ItemActions><Badge variant="success">Active</Badge></ItemActions>
</Item>
<ControlledFormField name="email" control={control} label="Email" required>
{(field, _err, invalid) => (
<Input value={field.value} onChange={field.onChange} invalid={invalid} />
)}
</ControlledFormField>Entry-points
Import per component family. This keeps optional peers isolated: importing
base/item should not require react-day-picker, and importing
features/overlays should not require TipTap.
import { Text } from 'admin-ui-starter-kit/typography';
import { Button } from 'admin-ui-starter-kit/base/buttons';
import { SmartCard } from 'admin-ui-starter-kit/base/cards';
import { Item, ItemGroup, ItemMedia, ItemContent, ItemTitle } from 'admin-ui-starter-kit/base/item';
import { ControlledFormField, FormSection, Input, PasswordInput } from 'admin-ui-starter-kit/base/forms';
import { DataViewShell, DataViewToolbar } from 'admin-ui-starter-kit/features/data-view';
import { AsyncResourceCombobox, EnhancedCombobox } from 'admin-ui-starter-kit/base/combobox';
import { DataTable, ResourceCell, StatusClusterCell } from 'admin-ui-starter-kit/features/table';
import { Badge } from 'admin-ui-starter-kit/base/badge';
import { Alert } from 'admin-ui-starter-kit/base/display';
import { Carousel, CarouselContent, CarouselItem } from 'admin-ui-starter-kit/base/carousel';
import { ChartContainer } from 'admin-ui-starter-kit/base/display/chart';
import { ActionList, ApiTokenManager } from 'admin-ui-starter-kit/composed/admin';
import { ContactCard } from 'admin-ui-starter-kit/composed/cards';
import { BookingCalendarEventCard, SeoListingCard } from 'admin-ui-starter-kit/composed/commerce';
import { RelationshipCardFrame } from 'admin-ui-starter-kit/composed/data-display';
import { ChartCard } from 'admin-ui-starter-kit/composed/analytics';
import { Comments } from 'admin-ui-starter-kit/features/comments';
import { SharedResourceCard } from 'admin-ui-starter-kit/features/resource-assignment';
import { parseFiltersFromQuery, serializeFiltersToQuery } from 'admin-ui-starter-kit/features/filters';
import { ProductVariantsManager } from 'admin-ui-starter-kit/features/products';
import { Container, Section } from 'admin-ui-starter-kit/layout/containers';
import { Dialog, Drawer } from 'admin-ui-starter-kit/features/overlays';
import { SchemaForm } from 'admin-ui-starter-kit/features/schema-form';
import { HeaderToolButton, HeaderToolPopover } from 'admin-ui-starter-kit/layout/header';
import { ResourceIndexShell, ResourceShowShell, TabbedResourceShell } from 'admin-ui-starter-kit/features/resource';
import { SettingsShell } from 'admin-ui-starter-kit/layout/settings';
import { ActionProvider, ActionOverlayOutlet } from 'admin-ui-starter-kit/services/actions';
import { useStrings } from 'admin-ui-starter-kit/lib/strings';Use documented exact entrypoints only. MetadataList has its own public
subfamily entrypoint because it is often used on its own. Chart primitives also
have an isolated entrypoint so consumers do not pull Recharts through unrelated
display helpers:
import { MetadataList } from 'admin-ui-starter-kit/composed/data-display';
import { ChartContainer } from 'admin-ui-starter-kit/base/display/chart';Safe public entrypoint shapes:
admin-ui-starter-kit— provider, strings helpers, andcnonly.admin-ui-starter-kit/ui-provideradmin-ui-starter-kit/typographyadmin-ui-starter-kit/base/buttons,admin-ui-starter-kit/base/item, etc.admin-ui-starter-kit/features/data-viewadmin-ui-starter-kit/base/tableadmin-ui-starter-kit/features/tableadmin-ui-starter-kit/base/comboboxadmin-ui-starter-kit/composed/data-displayadmin-ui-starter-kit/base/display/chartadmin-ui-starter-kit/composed/cards,admin-ui-starter-kit/composed/analytics,admin-ui-starter-kit/composed/admin,admin-ui-starter-kit/composed/commerce, etc.admin-ui-starter-kit/features/comments,admin-ui-starter-kit/features/filters,admin-ui-starter-kit/features/schema-form, etc.admin-ui-starter-kit/features/productsadmin-ui-starter-kit/layoutand exact layout families such asadmin-ui-starter-kit/layout/pageandadmin-ui-starter-kit/layout/settingsadmin-ui-starter-kit/features/resourcefor state-aware resource workspaces, not ordinary page chromeadmin-ui-starter-kit/services/actionsadmin-ui-starter-kit/lib/stringsadmin-ui-starter-kit/lib/utils
Raw shadcn primitives under src/components/ui/* are intentionally not public
package exports. If a consumer needs Tabs, import the library wrapper surface
that owns that pattern, such as admin-ui-starter-kit/base/navigation, rather
than admin-ui-starter-kit/ui/tabs.
Avoid broad component imports such as admin-ui-starter-kit/base,
admin-ui-starter-kit/composed, or admin-ui-starter-kit/features; they are
intentionally not exported. Use the exact family subpath so missing optional
peers fail only when you import the feature that needs them. The package
exports map lists each public family as an exact key; wildcard exports and
null wildcard blockers are intentionally avoided because bundlers handle them
inconsistently.
Internal shadcn primitive inventory
Maintainers can add shadcn primitives with
npx shadcn@latest add <primitive>, which writes to src/components/ui/.
Those files are reproducible shadcn output and should be wrapped in base/,
layout/, or features/ before becoming a public package API.
Installed internal primitives include:
accordion, alert-dialog, alert, aspect-ratio, avatar, badge,
breadcrumb, button, button-group, calendar, card, carousel,
chart, checkbox, collapsible, combobox, command, context-menu,
dialog, direction, drawer, dropdown-menu, empty, field,
hover-card, input, input-group, input-otp, item, kbd, label,
menubar, native-select, navigation-menu, pagination, popover,
progress, radio-group, resizable, scroll-area, select,
separator, sheet, sidebar, skeleton, slider, slot, sonner,
spinner, switch, table, tabs, textarea, toggle, toggle-group,
and tooltip.
Dropdown menus use Base UI's render prop for custom triggers, not Radix's
asChild prop:
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'admin-ui-starter-kit/base/navigation';
<DropdownMenu>
<DropdownMenuTrigger render={(props) => <Button {...props}>Open</Button>} />
<DropdownMenuContent />
</DropdownMenu>For repeated overflow actions, prefer ActionMenu with an actions definition
array from the same base/navigation entrypoint. Use ActionButtons when the
same definitions need to render inline buttons. Data table row actions,
SmartCard actions, and table column controls share that shell. Reach for
low-level DropdownMenu parts only when the menu needs radio, submenu, or
bespoke content. The shared dropdown surface inherits the active theme; only
use inverse styling when the surrounding product surface is explicitly
inverse. Fix row padding and item density in
base/navigation/dropdown-menu, not with caller-specific overrides.
For card-owned action rows, use CardPrimaryAction for the single app-primary
body action and CardActionStrip for repeated body/footer actions. Both live
in admin-ui-starter-kit/base/cards and render through the centralized button
and action definitions.
Reusable composed/ and features/ components are card-free content blocks by
default. Frame display-only relationships with SmartCard and
RelationshipCardFrame at the page, drawer, dialog, or expansion call site:
import { SmartCard } from 'admin-ui-starter-kit/base/cards';
import { RelationshipCardFrame } from 'admin-ui-starter-kit/composed/data-display';
<SmartCard title="Owner">
<RelationshipCardFrame title={owner.name} metadata={ownerMetadata} />
</SmartCard>For module-owned relationship cards, compose RelationshipCardFrame inside
that SmartCard. The app passes title, badge, selector, metadata, primary
action, overflow action, and details slots; the package owns the spacing and
region order.
For stateful SharedResourceCard assignment flows, keep suggestion shapes app-owned.
When a suggestion uses name, title, or another field instead of label,
pass selector.getSelectionLabel so the package default selection summary can
render the chosen resource without assuming the consumer domain model.
resource is the only selected/empty source of truth; pass null when the
relationship is unassigned and omit the deprecated hasResource flag. Supply
viewAction when navigation must render through the consumer's router.
Do not nest a second card inside an already framed section just because a
component name contains Card.
Build your own stylesheet
The pre-built style.css is the recommended default. To publish editable local
copies of the package theme/default token layers:
npx admin-ui-starter-kit-publish-themeThis writes src/styles/admin-ui/theme.css, defaults.css, overrides.css,
admin-ui.css, and source.css. theme.css is the swappable shadcn adapter;
defaults.css is a flattened editable copy of the package token layers.
Import src/styles/admin-ui/admin-ui.css when you want package CSS plus
checked-in token overrides.
If you intentionally skip the pre-built CSS and rebuild Tailwind in your app:
Don't import
admin-ui-starter-kit/style.css.Add the kit's source to your Tailwind
content:// tailwind config (or @source in your CSS) content: ['./node_modules/admin-ui-starter-kit/dist/**/*.{js,cjs}'],Start from the generated
src/styles/admin-ui/source.css. Keep shadcn color, radius, shadow, dark/light, chart, and sidebar values in the theme layer; keep package states,--link-color, inverse tokens, structural typography, spacing, density, action/control heights, rows, surfaces, media, shell, and component knobs in the defaults layer.
CommonJS support
Both ESM (.js) and CJS (.cjs) are emitted. The exports map resolves
automatically:
// ESM
import { Item } from 'admin-ui-starter-kit/base/item';
// CJS
const { Item } = require('admin-ui-starter-kit/base/item');For maintainers
This package is governed by a single rules file:
.agents/skills/component-library-rules/SKILL.md.
It is the contract — everything from the layer order to typography to
the strings pattern to the Item/FormField primitives is documented there.
Companion docs:
AGENTS.md— what AI agents should read first.CLAUDE.md— Claude Code entry point.references/— per-task deep guides (new feature, strings, ui-provider, base wrapper, preview pages, item pattern, form-field pattern, visual evaluation).
Common scripts
npm run dev # start the preview app
npm run build:lib # compile the publishable artifact (dist/)
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run lint:architecture # check layer-import boundaries
npm run test:exports # check the exports map matches the file tree
npm run api:check # detect public TypeScript API drift
npm run test:dist-assets # check style.css referenced assets exist after build:lib
npm run test:performance-budget # enforce CSS, entrypoint, format, and dist budgets
npm run test:package-smoke # pack + temporary consumer build for exact subpaths
npm run test:browser:features # Playwright feature route/workflow sweep
npm run test:interaction-contracts # keyboard, Escape, and focus return
npm run test:a11y:features # axe pass over feature showcase routes
npm run test:visual:features # committed feature screenshot baselines
npm run test:visual:parity # light/dark, density, font, text, and radius matrix
npm run test # vitest run
npm run verify # architecture + tokens + smells + exports + docs + typecheck + lint + tests
npm run verify:release # full maintainer release gate
npm run publish:skill # copy repo skills into ~/.claude/skills and ~/.agents/skillsThe browser gate is registry-driven rather than a hand-maintained route list.
At both desktop and mobile widths it visits every ready showcase route, scrolls
through the full page, exercises targeted workflows, and validates each
.example--component for render errors, collapsed geometry, header collisions,
minimum readable text, and contained horizontal overflow. Accessibility and
visual commands use the same canonical feature-route registry; committed
desktop/mobile baselines must map one-to-one to those routes. The focused
parity suite uses #/docs/parity-lab instead of multiplying every route across
every presentation scenario.
Project layout
src/
├── components/
│ ├── ui/ shadcn primitives (do not edit)
│ ├── typography/ Text, Heading, DisplayLabel, Label, TextLink
│ ├── primitives/ Value, Name, Initials, Link, Email, Phone, Url, Money, Number, FileSize, Duration, Dimensions, Date, DateRange
│ ├── base/ typography-aware wrappers + base/item + base/forms/form-field
│ ├── composed/ domain rows + cards (admin, cards, commerce, dark-surfaces, …)
│ ├── features/ feature surfaces (activities, comments, filters, overlays, …)
│ └── layout/ app-shell scaffolds
├── lib/
│ ├── ui-provider/ zustand store + slice hooks
│ ├── strings.ts useStrings + StringsProp + DeepPartial helpers
│ └── utils.ts cn() etc.
├── hooks/ cross-cutting hooks (useDebounce, useMobile, …)
└── preview/ in-repo showcase app source (excluded from publish; dist/showcase ships prebuilt)License
MIT © Nicolas Vlachos. See LICENSE.
