@app/ui
@app/uiis the platform’s component parts bin — shadcn/ui-style copy-in components built on Radix primitives and themed exclusively through platform CSS tokens. Every surface in a generated app (core runtime primitives, forged components, human-written components) composes from here. Nobody hand-rolls a control that already exists in the barrel.
@app/uiautomatically — the barrel is preloaded, the Tailwind tokens are threaded through the project’s tailwind.config.ts, and every primitive ships wired. Install manually only if you’re consuming the kit outside a generated app.Installation
# inside a generated Appbricx app: nothing to do — already imported
# consuming standalone:
pnpm add @app/uiStandalone consumers also need Tailwind CSS and the platform CSS token layer (the --background, --foreground, --primary, --muted, --border, --ring, --destructivevariables the classes expect). Copy the token block from any generated app’s globals.css.
Import shape
import {
Button, Input, Textarea, Label,
Card, CardHeader, CardTitle, CardContent, CardFooter,
Dialog, DialogTrigger, DialogContent, DialogTitle,
Table, TableHeader, TableBody, TableRow, TableCell,
Select, SelectTrigger, SelectContent, SelectItem,
Tabs, TabsList, TabsTrigger, TabsContent,
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
Badge, Separator, Skeleton, Spinner, EmptyState,
cn,
} from "@app/ui";The primitives
| Primitive | What it is |
|---|---|
Button | CVA-driven button with variant (default, destructive, outline, secondary, ghost, link) and size (sm, default, lg, icon). asChild slots into any element. |
Input | Themed <input> with focus ring, disabled styling, and file-input tweaks. |
Textarea | Matching themed multi-line input. |
Label | Radix Label — pairs with any input for accessibility. |
Card | Surface container with sub-parts: CardHeader, CardTitle, CardDescription, CardContent, CardFooter. |
Table | Semantic table set: TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption. |
Badge | Pill for status / meta. badgeVariants is exported for composition. |
Skeleton | Placeholder shimmer while data loads. |
Separator | Radix separator — horizontal or vertical rule. |
Dialog | Radix modal: Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, DialogClose. Ships with an overlay, portal, and built-in close affordance. |
Select | Radix select: Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectItem. |
Tabs | Radix tabs: Tabs, TabsList, TabsTrigger, TabsContent. |
DropdownMenu | Radix dropdown menu: DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator. |
EmptyState | Consistent zero-data slot: icon, title, description, optional action. |
Spinner | Small inline loading indicator. |
cn | Class-name merge helper (clsx + tailwind-merge). Use it whenever you accept a className prop. |
A quick example
import {
Card, CardHeader, CardTitle, CardContent,
Button, Input, Label,
} from "@app/ui";
export function NewTodoCard({ onCreate }: { onCreate: (t: string) => void }) {
return (
<Card>
<CardHeader>
<CardTitle>New todo</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Label htmlFor="title">Title</Label>
<Input id="title" placeholder="Buy milk" />
<Button onClick={() => onCreate("Buy milk")}>Add</Button>
</CardContent>
</Card>
);
}Theming
Colors, radii, and rings come from CSS variables in the generated app’s globals.css (e.g. --background, --primary, --muted-foreground, --ring, --destructive). Tailwind is wired to read those variables in tailwind.config.ts— swap the token block to reskin the entire app in one place. No primitive hardcodes a hex value; if you find one, that’s a bug.
Dark mode: the token block defines both palettes under :root and .dark. Toggling the .dark class on <html> switches every primitive.
Adding your own primitive
Add new components alongside the kit only when nothing in the barrel fits — reach for composition first.
- Create
packages/app-ui/src/my-thing.tsx. Importcnfrom./cnand, if you need variants,cvafromclass-variance-authority. - Use CSS tokens (
bg-primary,text-foreground,border-border) — never hex. - Accept
classNameand forward it throughcn(...)so consumers can override. - Export from
packages/app-ui/src/index.tsso it shows up under@app/ui.
// packages/app-ui/src/callout.tsx
import * as React from "react";
import { cn } from "./cn";
export function Callout({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
"rounded-md border border-border bg-muted/40 p-4 text-sm text-foreground",
className
)}
{...props}
/>
);
}House rules
- No duplicates.If it’s in the barrel, import it — don’t reimplement.
- Tokens, not hex. Use Tailwind classes that resolve to CSS variables. This is what makes theming and dark mode work.
- Forward refs and props. Every primitive here does; new ones should too.
- Radix under the hood. Reach for the Radix primitive before rolling your own accessibility layer.
Related
- @app/sdk — the data hooks and
<AppRoot>these primitives render inside. - For developers — how to customize a generated app end to end.