UIKit is a React component library designed for agentic code workflows. 45+ typed components, 5 OKLCH themes, and 6 production layouts under one canonical pattern — with machine-readable specs (llms.txt + AGENTS.md) so AI coding agents generate correct TSX, and developers review work they actually understand.
npm install, every componentnpm install @bloomneo/uikit
This page is written for humans. Agents should fetch /uikit/llms.txt and /uikit/AGENTS.md as the authoritative specs — every component's exact prop shape, canonical setup, and what NOT to do. Same files ship inside the published npm package under node_modules/@bloomneo/uikit/ and are regenerated on every release.
UIKit is the React library teams reach for when they need to ship — not evaluate five headless kits and style them by hand.
PermissionGate + role-aware dropdown menus, gated buttons, protected routes.MobileLayout, TabBar, SafeArea with touch-optimised components and platform detection.isTauri() / isNative() helpers for platform-specific tweaks.PopupLayout with constrained viewports and extension-safe CSP configuration.cn() helper, semantic tokens — fork the design system without forking the components.These are the only ways to silently break UIKit code. Memorise them — or point your AI agent at AGENTS.md where they're spelled out line by line.
onChange on <Select> or <Combobox>. Both use onValueChange(newValue). Using onChange fails silently — no error, no update. onChange is reserved for native input wrappers (Input, Textarea, PasswordInput).undefined to <DataTable data>. Always fall back to [] during loading: data={users ?? []}. Passing undefined throws UIKitError.<ToastProvider> or <ConfirmProvider> twice. Duplicate mounts produce doubled behaviour in prod and fire a dev-only warning. Exactly one per app.<FormField>. Bare <Label> + <Input> misses error display and a11y wiring. <FormField label="..." error={...}> handles both.bg-primary, text-muted-foreground, border-border). Raw Tailwind colours (bg-blue-500) break the moment a user switches themes.Four handler families. Picking the right one is the single biggest thing agents and humans get wrong. Memorise this table.
| Component family | Value prop | Change handler |
|---|---|---|
Native inputs — Input, Textarea, PasswordInput | value | onChange(e) — ChangeEvent |
Radix pickers — Select, Combobox, Slider, Tabs, Accordion | value | onValueChange(newValue) |
Radix checkables — Checkbox, Switch, RadioGroup | checked | onCheckedChange(checked) |
Overlays — Dialog, Sheet, Popover | open | onOpenChange(open) |
In 2.0, <Combobox> switched from onChange to onValueChange to match <Select>. No alias kept — this is drift-checked in CI.
// 1. Import the core stylesheet ONCE at your app entry (e.g. main.tsx)
import '@bloomneo/uikit/styles';
// Optional: import theme fonts (Elegant, Metro, Studio, Vivid)
import '@bloomneo/uikit/styles/fonts';
// 2. Wrap your app with providers
import { ThemeProvider, ToastProvider, ConfirmProvider } from '@bloomneo/uikit';
<ThemeProvider theme="base" mode="light">
<ToastProvider position="bottom-right" />
<ConfirmProvider>
<App />
</ConfirmProvider>
</ThemeProvider>
// 3. Add FOUC prevention in index.html <head> — prevents theme flash before React mounts
import { foucScript } from '@bloomneo/uikit/fouc';
<script dangerouslySetInnerHTML={{ __html: foucScript() }} />
There is exactly ONE supported import path for normal use. Always use the flat import:
import {
Button, Input, Card, CardHeader, CardTitle, CardContent,
Alert, AlertDescription, Badge, DataTable, Dialog,
DialogContent, DialogHeader, DialogTitle, DialogFooter,
Select, Combobox, DatePicker, Skeleton, EmptyState,
PageHeader, PermissionGate, PermissionProvider,
FormField, PasswordInput, toast, useToast, useConfirm,
useTheme, useApi, usePagination, useBreakpoint,
formatBytes, formatCurrency, formatDate, timeAgo,
} from '@bloomneo/uikit';
// Deep imports like '@bloomneo/uikit/button' exist for tree-shaking
// but are NOT the canonical form. Don't mix styles in one file.
OKLCH color space ensures colors look consistent across devices and scale predictably — no muddy mid-tones or clipped highs. Each theme ships in light and dark mode.
Clean neutral foundation. Good default for any product.
Refined, subdued palette. Professional and polished.
High contrast, bold. Inspired by transit design systems.
Creative, warm. For design tools and media apps.
Saturated, energetic. For consumer and marketing apps.
const { theme, mode, availableThemes, setTheme, toggleMode } = useTheme();
setTheme('elegant'); // switch theme at runtime
toggleMode(); // light ↔ dark
// All available themes:
// 'base' | 'elegant' | 'metro' | 'studio' | 'vivid' | (your custom theme)
import { Button } from '@bloomneo/uikit';
// Variants
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
// States
<Button disabled>Disabled</Button>
<Button loading>Saving...</Button>
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Button } from '@bloomneo/uikit';
<Card>
<CardHeader>
<CardTitle>User Settings</CardTitle>
<CardDescription>Manage your account preferences</CardDescription>
</CardHeader>
<CardContent>
<p>Content goes here</p>
</CardContent>
<CardFooter>
<Button>Save changes</Button>
</CardFooter>
</Card>
Sortable, filterable, paginated table with row actions. Define columns once and let the table handle the rest.
import { DataTable, type DataTableColumn, type RowAction } from '@bloomneo/uikit';
import { Pencil, Trash2 } from 'lucide-react';
type User = { id: string; name: string; email: string; role: 'admin' | 'user'; createdAt: string; };
const columns: DataTableColumn<User>[] = [
{ id: 'name', header: 'Name', accessorKey: 'name', sortable: true },
{ id: 'email', header: 'Email', accessorKey: 'email' },
{ id: 'role', header: 'Role', accessorKey: 'role', sortable: true },
{ id: 'createdAt', header: 'Joined', accessorKey: 'createdAt', sortable: true, dataType: 'date' },
];
const actions: RowAction<User>[] = [
{ id: 'edit', label: 'Edit', icon: <Pencil size={14} />, onClick: (row) => handleEdit(row) },
{ id: 'delete', label: 'Delete', icon: <Trash2 size={14} />, onClick: (row) => handleDelete(row), destructive: true },
];
<DataTable<User>
data={users}
columns={columns}
rowActions={actions}
searchable
pagination
pageSize={10}
getRowId={(row) => row.id}
/>
import { FormField, Input, PasswordInput, Select, Combobox, Button } from '@bloomneo/uikit';
// FormField wraps any input with label, error, and helper text
<FormField label="Email" required error={emailError} helper="We'll never share this">
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</FormField>
<FormField label="Password" required>
<PasswordInput value={password} onChange={(e) => setPassword(e.target.value)} />
</FormField>
// Searchable select with async loading
<Combobox
value={country}
onValueChange={setCountry}
options={countries}
placeholder="Select a country"
searchPlaceholder="Search countries..."
clearable
/>
Mount <ToastProvider /> once at the root. Call toast.* from anywhere — no hooks, no context drilling.
import { toast } from '@bloomneo/uikit';
// Simple toasts
toast.success('Changes saved');
toast.error('Something went wrong');
toast.info('Heads up — deployment is running');
toast.warning('Your session expires in 5 minutes');
// With description and action
toast('Saved', {
description: 'Your changes are now live',
action: {
label: 'Undo',
onClick: () => toast.info('Undone'),
},
});
useConfirm() returns a promise — true if confirmed, false if cancelled. No boilerplate modal state needed.
import { useConfirm } from '@bloomneo/uikit';
function DeleteButton() {
const confirm = useConfirm();
async function handleDelete() {
const ok = await confirm({
title: 'Delete this record?',
description: 'This cannot be undone.',
confirmLabel: 'Delete',
tone: 'destructive',
});
if (!ok) return;
await deleteRecord();
toast.success('Deleted');
}
// High-stakes: user must type a phrase before confirm enables
async function handleHardDelete() {
const ok = await confirm.destructive({
title: 'Delete account',
description: 'This will permanently delete the account and all its data.',
verifyText: 'delete my account',
});
if (ok) await nukeAccount();
}
return <Button variant="destructive" onClick={handleDelete}>Delete</Button>;
}
import { useState } from 'react';
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@bloomneo/uikit';
export function EditProfileDialog() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Edit profile</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Make changes and save when you're done.</DialogDescription>
</DialogHeader>
<p className="text-sm text-muted-foreground">Form fields go here.</p>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button onClick={() => setOpen(false)}>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
import { Users } from 'lucide-react';
import { Button, PageHeader } from '@bloomneo/uikit';
<PageHeader
icon={<Users />}
title="User management"
description="View and manage all users in your workspace"
breadcrumbs={[
{ label: 'Admin', href: '/admin' },
{ label: 'Users' },
]}
actions={<Button>Add user</Button>}
/>
import { Inbox } from 'lucide-react';
import { Button, EmptyState } from '@bloomneo/uikit';
<EmptyState
icon={<Inbox />}
title="No invoices yet"
description="Create your first invoice to get started."
action={<Button onClick={handleCreate}>Create invoice</Button>}
/>
import { PermissionGate, PermissionProvider, Button } from '@bloomneo/uikit';
// Bring your own auth source — just provide a check function
const check = (perm: string) => currentUser.roles.includes(perm);
<PermissionProvider check={check}>
{/* Single permission */}
<PermissionGate when="admin">
<Button variant="destructive">Delete user (admin only)</Button>
</PermissionGate>
{/* OR logic across multiple roles */}
<PermissionGate when={['admin', 'moderator']} fallback={<span>Restricted</span>}>
<Button>Moderate content</Button>
</PermissionGate>
{/* Custom predicate */}
<PermissionGate when={() => currentUser.plan === 'pro'}>
<ProFeature />
</PermissionGate>
</PermissionProvider>
import { formatBytes, formatCurrency, formatDate, timeAgo, Time } from '@bloomneo/uikit';
formatCurrency(1234.56, { currency: 'USD' }) // '$1,234.56'
formatCurrency(1234.56, { currency: 'INR', locale: 'en-IN' }) // '₹1,234.56'
formatDate(new Date(), { preset: 'long' }) // 'April 12, 2026'
timeAgo(new Date(Date.now() - 10 * 60 * 1000)) // '10 minutes ago'
formatBytes(1_572_864) // '1.5 MB'
// Auto-updating relative time component
<Time date={publishedAt} />
import { useApi, useTheme, usePagination, useBreakpoint, useActiveBreakpoint, useMediaQuery } from '@bloomneo/uikit';
// API hook — managed loading/data/error state
const api = useApi({ baseURL: 'http://localhost:3000' });
await api.get('/api/users');
// api.data, api.loading, api.error available reactively
// Theme hook
const { theme, mode, setTheme, toggleMode, availableThemes } = useTheme();
// Pagination hook — works with any data source
const pagination = usePagination({ total: 234, pageSize: 10 });
const visible = allItems.slice(pagination.startIndex, pagination.endIndex);
// pagination.page, pagination.pageCount, pagination.hasPrev, pagination.hasNext
// pagination.prev(), pagination.next(), pagination.goTo(n), pagination.pages
// Responsive breakpoints
const isAtLeastMd = useBreakpoint('md'); // true when ≥ 768px
const isMobile = useBreakpoint('md', 'down'); // true when < 768px
const active = useActiveBreakpoint(); // 'sm' | 'md' | 'lg' | 'xl' | '2xl'
const reduced = useMediaQuery('(prefers-reduced-motion: reduce)');
Header / HeaderLogo / HeaderNavSticky top nav with logo and navigation linksFooterPage footer with configurable column layoutSidebar / SidebarNavCollapsible sidebar with nested navigation supportPageLayoutFull-page wrapper with header, sidebar, content, and footer slotsSection / ContainerContent containers with spacing variants and max-width constraintsMobileLayout / TabBar / SafeAreaMobile-specific layouts for Capacitor appsInput / TextareaStandard text inputs with consistent stylingPasswordInputPassword input with show/hide toggleSelectDropdown select for static option listsComboboxSearchable select with async option loading and clearableDatePicker / DateRangePickerCalendar-based date and date-range selectionCheckbox / Radio / SwitchBoolean and choice inputsFileUploadDrag-and-drop file input with preview and progressFormFieldWrapper providing label, required marker, error, and helper textDataTableSortable, filterable, paginated table with row actions and column definitionsDataListKey-value display for detail viewsStatCardMetric card with label, value, change indicator, and optional sparklineBadgeStatus / label chip in multiple color variantsAlert / AlertDescriptionInline status messages with info, warning, error, success variantsSkeletonLoading placeholder that matches the shape of real contentA complete user list with search, sort, row actions, and delete-with-confirm:
import { useState } from 'react';
import { Pencil, Trash2, Users } from 'lucide-react';
import {
Button, ConfirmProvider, DataTable, PageHeader,
ThemeProvider, ToastProvider, toast, useConfirm,
type DataTableColumn, type RowAction,
} from '@bloomneo/uikit';
type User = { id: string; name: string; email: string; role: 'admin' | 'user' };
function UserListPage() {
const [users, setUsers] = useState<User[]>(initialUsers);
const confirm = useConfirm();
const columns: DataTableColumn<User>[] = [
{ id: 'name', header: 'Name', accessorKey: 'name', sortable: true },
{ id: 'email', header: 'Email', accessorKey: 'email' },
{ id: 'role', header: 'Role', accessorKey: 'role', sortable: true },
];
const actions: RowAction<User>[] = [
{ id: 'edit', label: 'Edit', icon: <Pencil size={14} />, onClick: handleEdit },
{
id: 'delete', label: 'Delete', icon: <Trash2 size={14} />, destructive: true,
onClick: async (user) => {
const ok = await confirm({ title: `Delete ${user.name}?`, tone: 'destructive' });
if (!ok) return;
setUsers(prev => prev.filter(u => u.id !== user.id));
toast.success(`${user.name} deleted`);
},
},
];
return (
<>
<PageHeader icon={<Users />} title="Users" actions={<Button>Add user</Button>} />
<DataTable data={users} columns={columns} rowActions={actions} searchable pagination getRowId={r => r.id} />
</>
);
}
export default function App() {
return (
<ThemeProvider theme="base" mode="light">
<ToastProvider position="bottom-right" />
<ConfirmProvider>
<UserListPage />
</ConfirmProvider>
</ThemeProvider>
);
}
Vite + React. Full component library with all themes. SSR-compatible.
Electron renderer process. Same components, same themes, same API.
Capacitor + React. Touch-optimised with MobileLayout, TabBar, SafeArea.
Browser extension popup. Constrained layout variants for small viewports.
llms.txt and AGENTS.md machine-readable specs in every release so agents generate correct JSX on the first try.
@llm-rule JSDoc so AI agents pick the correct API on the first try.
src/components/ui/*.tsx ship the "use client" directive and work in the App Router. Server Components can import non-interactive primitives like Card, Badge, Alert directly. SSR and FOUC are handled via the foucScript() helper for <head>.
tailwind.config.js in your app (the bundled stylesheet is self-contained), but you do need to import @bloomneo/uikit/styles.
https://dev.bloomneo.com/uikit/llms.txt and /uikit/AGENTS.md — authoritative machine-readable specs. (2) Install the package; the same files ship inside node_modules/@bloomneo/uikit/. (3) Claude Code users: copy skills/bloomneo-uikit/ into your repo's .claude/skills/ for auto-triggered per-component guidance.
any in the public surface — DataTable<User>, RowAction<User>, formatters and hooks all infer correctly so agent autocomplete actually works. A drift-checked public-surface test in CI prevents accidental API changes.
base, elegant, metro, studio, vivid — each in light and dark mode. Switch at runtime with useTheme().setTheme('elegant'). Custom themes are first-class: run npx uikit generate theme <name> to scaffold one with OKLCH tokens and a dark-mode variant.
isTauri(), isNative(), isExtension()) and mobile-specific layouts (MobileLayout, TabBar, SafeArea). Use the same components for web, Electron desktop, Capacitor mobile, and browser extension popups from one codebase.
UIKitError subclasses — DataTableError, FormFieldError, ThemeError, etc.), docs-URL links in error messages, and a drift-checked public surface enforced in CI. The unified controlled-prop convention means agents never silently generate broken handlers.