@quplex/ui
v0.2.0
Published
Shared Quplex React UI primitives and semantic theme contract.
Readme
@quplex/ui
Quplex's shared React UI library: accessible, themeable primitives for React applications.
Install
npm install @quplex/uiQuick start
Import the stylesheet once in your application's global CSS or root entry point:
import "@quplex/ui/globals.css";Then use the primitives:
import { Button, Input, Label } from "@quplex/ui";
export function SignInForm() {
return (
<form>
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" autoComplete="email" required />
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" autoComplete="current-password" required />
<Button type="submit">Sign in</Button>
</form>
);
}Storybook
Run the component catalogue locally from the package directory:
npm run storybookA hosted Storybook is not configured yet. npm renders this README but cannot host interactive Storybook files; once deployed to a public static host, its URL belongs in this section.
What belongs here
@quplex/ui owns small, generic primitives and their accessible behavior:
- Button, Input, Label, Checkbox, Dialog, Tooltip, and similar building blocks
- shared semantic CSS tokens and safe light/dark defaults
- the
cnclass-merging utility
It does not own pages, product workflows, API calls, routing, permissions,
tenancy rules, or application-specific layouts. For example,
AdminUserSuspensionDialog belongs in admin, while a generic Dialog
belongs here.
Package contract
This package uses React, TypeScript, ESM, Tailwind CSS v4, shadcn/Base UI primitives, CSS variables, and Lucide icons. It is framework-neutral: it works with Vite applications and can be consumed by Next.js.
Imports
Import the package stylesheet once in the application's global stylesheet or root entry point:
import "@quplex/ui/globals.css";Use a root import for convenience:
import { Button } from "@quplex/ui";
export function SaveButton() {
return <Button>Save changes</Button>;
}For the most explicit tree-shakeable import, use the component subpath:
import { Button } from "@quplex/ui/components/button";The #components/*, #lib/*, and #hooks/* aliases are package-internal.
Consumer applications must not import them.
Component reference
All primitives are available from explicit, tree-shakeable component subpaths.
Button, Input, and Label are also available from the root package. For
example:
import { Dialog, DialogContent, DialogTrigger } from "@quplex/ui/components/dialog";
import { Button } from "@quplex/ui/components/button";Import the stylesheet once before using any component:
import "@quplex/ui/globals.css";Each primitive accepts its underlying HTML or Base UI props unless noted below,
as well as className. The key component-specific props and composition
exports are listed here; consult the linked source or the local Storybook
catalogue for the complete TypeScript signature and interactive examples.
| Component | Import | Main exports | Key props and usage |
| --- | --- | --- | --- |
| Accordion | components/accordion | Accordion, AccordionItem, AccordionTrigger, AccordionContent | Set an item value; use defaultValue/value on the root. Compose an item with a trigger and matching content. |
| Alert | components/alert | Alert, AlertTitle, AlertDescription, AlertAction | variant: default or destructive. Use for important, non-blocking status messages. |
| Alert dialog | components/alert-dialog | AlertDialog, AlertDialogTrigger, AlertDialogContent, header/title/description/footer, AlertDialogAction, AlertDialogCancel | A confirmation modal. Always provide a clear title, description, and safe cancel action. |
| Avatar | components/avatar | Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge | Use an image with meaningful alt plus initials or an icon fallback. Avatar supports size: sm, default, lg. |
| Badge | components/badge | Badge, badgeVariants | variant: default, secondary, destructive, outline, ghost, link. Short status or category labels only. |
| Breadcrumb | components/breadcrumb | Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis | Use BreadcrumbPage for the current location and links for ancestors. |
| Button | components/button or root | Button, buttonVariants | variant: default, outline, secondary, ghost, destructive, link; size: xs, sm, default, lg, icon, icon-xs, icon-sm, icon-lg. Icon-only buttons require aria-label. |
| Button group | components/button-group | ButtonGroup, ButtonGroupText, ButtonGroupSeparator | orientation: horizontal or vertical. Group adjacent buttons with related actions. |
| Calendar | components/calendar | Calendar, CalendarDayButton | Uses React Day Picker props, including mode, selected, onSelect, disabled, and numberOfMonths. Compose it into a date-picker with Popover in an application. |
| Card | components/card | Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter | A semantic surface for a self-contained summary, form, or setting. |
| Chart | components/chart | ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, ChartConfig | Wrap a Recharts chart in ChartContainer, passing a required config. Configure each series with label and either color or light/dark theme colors. |
| Checkbox | components/checkbox | Checkbox | Base UI checkbox props including checked, defaultChecked, onCheckedChange, disabled, and name. Pair it with a visible <label>. |
| Collapsible | components/collapsible | Collapsible, CollapsibleTrigger, CollapsibleContent | Supports controlled or uncontrolled open state: open, defaultOpen, onOpenChange. |
| Command | components/command | Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator | Build searchable command palettes. Set a stable value for each CommandItem; use CommandDialog for modal palettes. |
| Context menu | components/context-menu | ContextMenu, trigger/content, item, checkbox/radio/submenu/group/label/separator/shortcut exports | Wrap the target with ContextMenuTrigger, then provide ContextMenuContent and accessible actions. |
| Dialog | components/dialog | Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose | Supports open, defaultOpen, onOpenChange; use a title and description for every modal. |
| Drawer | components/drawer | Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose | A touch-friendly bottom sheet. Supports controlled open state; use for compact, task-focused mobile interactions. |
| Dropdown menu | components/dropdown-menu | DropdownMenu, trigger/content, item, checkbox/radio/submenu/group/label/separator/shortcut exports | Menu items accept standard event props. Use checkbox/radio items for persistent selection state. |
| Empty | components/empty | Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent | EmptyMedia variant: default, icon. Use for empty lists, searches, and unavailable content. |
| Field | components/field | Field, FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet, FieldLegend, FieldContent, FieldTitle, FieldSeparator | Use to structure form controls. Pass errors={[{ message }]} to FieldError, or provide error children directly. |
| Hover card | components/hover-card | HoverCard, HoverCardTrigger, HoverCardContent | Use concise supplemental preview content. Position with side, sideOffset, align, and alignOffset. |
| Input | components/input or root | Input | Standard input props (type, value, defaultValue, disabled, aria-invalid). Always provide a visible or ARIA label. |
| Input group | components/input-group | InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea | InputGroupAddon uses align: inline-start, inline-end, block-start, block-end; InputGroupButton supports button variant and compact sizes. |
| Keyboard hint | components/kbd | Kbd, KbdGroup | Display a keyboard shortcut such as <Kbd>⌘ K</Kbd>; it is presentational and not an interactive control. |
| Label | components/label or root | Label | Standard label props. Set htmlFor to the input id for pointer and screen-reader association. |
| Menubar | components/menubar | Menubar, menu/trigger/content/item/group/checkbox/radio/submenu/label/separator/shortcut exports | Use for desktop-style application menus; keep item labels concise and actions keyboard accessible. |
| Navigation menu | components/navigation-menu | NavigationMenu, list/item/trigger/content/link/indicator/positioner exports | Use links for navigation and triggers only for a disclosure containing related links. |
| Pagination | components/pagination | Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis | Set PaginationLink isActive for the current page. Provide real href/routing props for each link. |
| Popover | components/popover | Popover, PopoverTrigger, PopoverContent, PopoverHeader, PopoverTitle, PopoverDescription | Supports open, defaultOpen, onOpenChange; position content with side, sideOffset, align, and alignOffset. |
| Progress | components/progress | Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue | Set value, min, and max on the root. Include a label when the progress purpose is not obvious. |
| Radio group | components/radio-group | RadioGroup, RadioGroupItem | Use value, defaultValue, and onValueChange on the group. Each radio needs a label. |
| Resizable | components/resizable | ResizablePanelGroup, ResizablePanel, ResizableHandle | ResizablePanelGroup uses orientation: horizontal or vertical; panels support defaultSize, minSize, and maxSize; set withHandle for a visible drag grip. |
| Scroll area | components/scroll-area | ScrollArea, ScrollBar | Give the root a constrained height or width. The component provides its viewport and vertical scrollbar automatically. |
| Select | components/select | Select, SelectTrigger, SelectValue, SelectContent, SelectGroup, SelectLabel, SelectItem, SelectSeparator, scroll controls | Use value, defaultValue, and onValueChange; each item requires a unique value. Label the trigger. |
| Separator | components/separator | Separator | orientation: horizontal or vertical; decorative separators can use decorative. |
| Sheet | components/sheet | Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose | Side-panel dialog with controlled open state. SheetContent supports side: top, right, bottom, left. |
| Sidebar | components/sidebar | SidebarProvider, Sidebar, SidebarHeader, SidebarContent, SidebarFooter, groups/menu/input/trigger/rail/inset exports | Wrap an application shell in SidebarProvider; Sidebar supports side, variant, and collapsible. Use SidebarTrigger for the built-in keyboard-accessible toggle. |
| Skeleton | components/skeleton | Skeleton | A presentational loading placeholder; set dimensions using Tailwind className. |
| Slider | components/slider | Slider | Use value/defaultValue arrays, min, max, step, and change callbacks. Wrap in a <label> or otherwise provide an accessible name. |
| Sonner | components/sonner | Toaster | Render one <Toaster /> near the app root, then call toast.success, toast.error, etc. from sonner. The consumer must also provide a next-themes ThemeProvider. |
| Spinner | components/spinner | Spinner | A status icon with built-in accessible loading text. Use className for size or color. |
| Switch | components/switch | Switch | Supports checked, defaultChecked, onCheckedChange, disabled, and size: sm or default. Pair it with a label. |
| Table | components/table | Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption | A semantic table primitive. Sorting, filtering, pagination, and data fetching belong in an application-level composition. |
| Tabs | components/tabs | Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants | Use a unique value on every trigger/content pair. Root supports value, defaultValue, and onValueChange; list variant: default or line. |
| Textarea | components/textarea | Textarea | Standard textarea props. Use a visible/ARIA label and aria-invalid for invalid state. |
| Toggle | components/toggle | Toggle, toggleVariants | Supports pressed state props plus variant: default or outline, and size: sm, default, lg. Icon-only toggles need aria-label. |
| Toggle group | components/toggle-group | ToggleGroup, ToggleGroupItem | Set multiple for multi-select; supports value, defaultValue, onValueChange, orientation, spacing, plus toggle variant and size. |
| Tooltip | components/tooltip | TooltipProvider, Tooltip, TooltipTrigger, TooltipContent | Wrap each pair in Tooltip; position content with side, sideOffset, align, and alignOffset. Use tooltips to supplement—not replace—an icon button's aria-label. |
Complete examples
Confirm a destructive action
import { Button } from "@quplex/ui/components/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@quplex/ui/components/alert-dialog";
export function DeleteWorkspaceDialog() {
return (
<AlertDialog>
<AlertDialogTrigger render={<Button variant="destructive" />}>
Delete workspace
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this workspace?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}Controlled selection field
import { useState } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@quplex/ui/components/select";
export function RoleSelect() {
const [role, setRole] = useState("member");
return (
<Select value={role} onValueChange={setRole}>
<SelectTrigger aria-label="Member role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Administrator</SelectItem>
<SelectItem value="member">Member</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
);
}Next.js consumers
The initial package distribution exports TypeScript/TSX source. A Next.js consumer should transpile it:
// next.config.ts
const nextConfig = {
transpilePackages: ["@quplex/ui"],
};
export default nextConfig;Interactive primitives contain their own "use client" boundary. Do not add
that directive to static primitives unless they genuinely need client APIs.
Theming
Theme values resolve in this order:
- The complete defaults in
@quplex/ui/globals.css - The consuming application's theme CSS
- A component instance's
className
An application imports the global defaults, then overrides only the semantic tokens it needs:
@import "@quplex/ui/globals.css";
/* admin/src/styles/theme.css */
:root {
--primary: oklch(0.52 0.22 290);
--primary-foreground: oklch(0.98 0 0);
}If app or landing does not override a token, the library default remains.
For a one-off visual treatment, pass standard Tailwind utilities to the
component; cn merges them last:
<Button className="bg-amber-500 text-black hover:bg-amber-600">
Special landing action
</Button>Use semantic utilities such as bg-primary, text-foreground,
border-border, and ring-ring inside shared primitives. Do not hard-code a
brand colour into a primitive when a semantic token expresses its intent.
Changing the shared theme
Edit src/styles/globals.css to change a library
default. Keep both :root and .dark complete, preserve foreground pairs such
as --primary / --primary-foreground, and review every affected component in
Storybook. Never put an admin, app, or landing brand override in this package.
The complete theme-change procedure, including contrast and release checks, is in the UI library management guide.
Development
Run these commands from packages/ui/:
npm run storybook
npm run typecheck
npm run test
npm run build-storybookStorybook is the visual workbench. The Vitest addon runs stories in Playwright Chromium, including keyboard interactions and accessibility checks. Add a story for every exported component and keep accessibility failures blocking.
The current Storybook suite covers the complete component catalogue, including interactive overlays, navigation, form controls, data/layout primitives, keyboard interactions, and accessibility checks.
Adding a primitive
- Confirm that the component is generic rather than product-specific.
- Use the shared
components.jsonconfiguration (Base UI, Nova, Neutral, Lucide, CSS variables, RSC enabled). - Run the shadcn command yourself from this directory; it may install or alter package metadata, so AI agents must not run it.
- Keep package-internal imports on
#components/*,#lib/*, or#hooks/*. - Add an intentional public export and a Storybook story.
- Verify type checking, Chromium/a11y story tests, and the Storybook build.
Do not add the entire shadcn catalogue in advance. Add one primitive when approved product work requires it.
Managing changes
Use this change flow for every public component or token change:
- Decide whether it is a generic primitive change or application-specific composition. Keep the latter in its application.
- Update the component or global tokens, then its Storybook stories.
- Run the package checks and review both light and dark states.
- Decide the release impact: patch for a compatible fix, minor for an additive compatible API, major for a removal or breaking behavior/API change.
- Publish a new package version only after validating the packed artifact.
Detailed procedures—including public exports, component additions, theme work, and consumer migration—are in the UI library management guide.
Releases and consuming
@quplex/ui is a public npm package. Each consuming application installs a
published version through its own lockfile; applications never import from this
repository's packages/ui folder directly.
Before a new release:
- Increment the package version deliberately.
- Run
npm pack --dry-runand inspect the package contents. - Run the package checks and test the tarball in a disposable consumer fixture when exports or build behavior change.
- Publish the verified version with
npm publish --access public. - Upgrade each consuming application deliberately.
The person releasing the package runs the final publish command. AI agents may prepare the change and provide commands, but do not publish on the user's behalf. The detailed release checklist is in the UI library management guide.
This preserves independent lockfiles and deployments: upgrading the UI library
in admin does not silently upgrade app or landing. Avoid file:,
npm link, root workspaces, and Turborepo for this architecture.
Browser and bundle policy
Tailwind CSS v4 requires modern browsers. Test shared primitives in Chromium, Firefox, and WebKit before declaring a cross-browser release. The package marks only CSS as side-effectful and exposes component subpaths so consumers can tree-shake unused primitives. Keep root exports deliberate and verify them with a consumer bundle fixture before expanding the barrel.
Related documentation
The design and implementation rationale lives in
global_ai/guides/custom-theme-integration.md.
Package-specific contribution rules live in AGENTS.md.
