Hard-edged design system for nin.dev.
> Import from
01
One mark: the NiN monogram. Monochrome ink, hairline seams, folded bends. Do not redraw it, recolor it, or set it in a typeface.
import { Mark } from '@nin/ui'
Symbol
Where the full mark does not fit — favicons, avatars, app icons — use the square symbol: the monogram on an ink square. Never crop the monogram itself.
Clear space
Keep a safety area around the mark equal to the height of the diamond dot. Nothing enters it — no type, no borders, no badges.
Spelling
Written, the name is always lowercase `nin` — headings, links, URLs, and social tags included. `NiN` exists only inside the drawn mark. The domain is `nin.dev`.
02
Tailwind v4 scales, 50–950, absolute. Unused hues are tree-shaken. Light lifts by a 12% edge, dark lifts by a raised fill. One gray ladder runs muted → secondary → accent. Light is `:root`, dark is `html.dark`.
Palette
Eight hues, 50–950, absolute. Dark remaps aliases, not the ramp. Click a cell to copy `var(--color-gray-400)`.
50
100
200
300
400
500
600
700
800
900
950
gray
blue
red
amber
green
teal
purple
pink
03
Geist for the voice. Google Sans Code for indexes, code, and controls.
Geist · Display
text-5xl tracking-tight
Hard surfaces.
Geist · Title
text-3xl tracking-tight
Hard surfaces.
Geist · Body
text-base leading-relaxed
The quick brown fox jumps over the lazy dog.
Google Sans Code · Label
text-[11px] uppercase tracking-wider
api / nin / ui
Google Sans Code · Code
font-mono text-[13px]
import { Button } from '@nin/ui'
04
A material is a recipe: radius, fill, stroke, shadow. Surface sits on the page; Floating sits above it. Every corner is 0 and nothing casts a shadow, so light lifts by a 1px edge and dark lifts by a raised fill.
Surface · on the page
page
The page, cut out of a well.
background
The canvas. No fill of its own, no stroke — visible only against another material.
well
pnpm dev start
secondary
Inset rest fill: code, wells, filled controls. Hover washes are muted, one step lighter.
frame
email@nin.dev
background + border
Specimen, table, field. Fill stays page; the 1px edge does the work.
invert
Deploy
primary
Inverted ink. One high-contrast action per view.
muted
gray-100 / 900
Hover wash: ghost and outline hover, avatar shell.
secondary
gray-200 / 800
Control at rest. The well.
accent
gray-300 / 700
Selected: menu item, expanded trigger.
One gray axis carries state: rest wash → control fill → selected. The well is secondary — one step firmer than a muted hover, so rest never reads as hover.
Floating · above the page
Delete Project
Real copy sits here so the overlay has ink to press on.
frame · still on the page
modal
popover + 1px ring over overlay — black 50% in both themes.
page
popover
Light: popover keeps the page fill — the halves merge and the 1px ring carries all lift. Dark: popover rises to 0.19, so the fill itself is the lift.
menu
Open Project
Copy Link
Delete Project
Same recipe as modal, no overlay. The held row is accent.
tooltip
Inverted ink. The only floating surface that flips.
radius
Always 0. Geist rounds 6 / 12 / 16 — we do not. Icon buttons stay square.
fill
page at rest, secondary inset, popover floating. muted → secondary → accent is state, not depth.
stroke
`border` is the only edge. `ring` is focus, never a second border. `overlay` dims — it is not a stroke.
shadow
None. Light lifts by the 1px edge; dark lifts by the raised fill.
05
Motion is not a material. It explains state; it does not fake depth. Three springs on a doubling ladder — 150 / 300 / 600 — all bounce 0, from `@nin/motion`. Click a track to play. Apps wrap `MotionRoot` once.
import { snap, ui, gentle, theme } from '@nin/motion'
snap
150ms
Taps and toggles.
ui
300ms
Panels, dialogs, in-page jumps.
gentle
600ms
Fold and long travel.
stagger.base
40ms
Incoming groups.
travel.enter
8px
fadeUp. Two steps on the 4px grid.
travel.section
16px
Section in-view. Four grid steps.
reducedMotion
user / calm
MotionRoot follows the OS. Theme calm drops travel.
08
import { Toggle, ToggleGroup, ToggleGroupItem } from '@nin/ui'
Toggle
One pressed state. Toolbar, not a setting.
<Toggle>Bold</Toggle> <Toggle variant="outline" defaultPressed>Italic</Toggle>
Toggle Group
Two or three exclusive views. This is Geist Switch (Source / Output).
<ToggleGroup defaultValue={['source']}> <ToggleGroupItem value="source">Source</ToggleGroupItem> <ToggleGroupItem value="output">Output</ToggleGroupItem> </ToggleGroup>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| Toggle.variant | "default" | "outline" | "default" | |
| Toggle.pressed | boolean | — | Controlled. Uncontrolled uses defaultPressed. |
| ToggleGroup.value | string[] | — | Exclusive views. Past three, use Tabs. |
09
import { Input } from '@nin/ui'
Default
One line. 32px. font-mono. Pair with Field.
<Input placeholder="Email Address" />
Disabled
<Input disabled placeholder="Email Address" />
Invalid
Mark the control with aria-invalid. Copy lives on FieldError.
<Input aria-invalid defaultValue="not-an-email" />
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| type | string | "text" | Native input type. |
| disabled | boolean | false | |
| aria-invalid | boolean | — | Field copy stays on FieldError. |
10
import { Field, FieldLabel, FieldDescription, FieldError, Input } from '@nin/ui'
Default
Label, control, help. Do not use Label on its own.
We will never share your email.
<Field> <FieldLabel htmlFor="email">Email Address</FieldLabel> <Input id="email" placeholder="you@example.com" /> <FieldDescription>We will never share your email.</FieldDescription> </Field>
Disabled
Account email cannot be changed.
<Field data-disabled="true"> <FieldLabel htmlFor="email">Email Address</FieldLabel> <Input id="email" disabled /> </Field>
Invalid
<Field data-invalid="true"> <FieldLabel htmlFor="email">Email Address</FieldLabel> <Input id="email" aria-invalid defaultValue="not-an-email" /> <FieldError>Enter a valid email address.</FieldError> </Field>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| orientation | "vertical" | "horizontal" | "responsive" | "vertical" | Checkbox / Radio / Switch use horizontal. |
| data-disabled | "true" | — | Dims the label with the control. |
| data-invalid | "true" | — | Pairs with FieldError. |
11
import { Textarea } from '@nin/ui'
Default
Use the moment the value can wrap.
<Textarea placeholder="Describe Your Project" />
Disabled
<Textarea disabled placeholder="Describe Your Project" />
Invalid
<Textarea aria-invalid defaultValue="Too short." />
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| disabled | boolean | false | |
| aria-invalid | boolean | — | Copy lives on FieldError. |
12
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '@nin/ui'
Prefix
Units, search, clear. The field is still one Input.
<InputGroup> <InputGroupAddon> <InputGroupText>https://</InputGroupText> </InputGroupAddon> <InputGroupInput placeholder="nin.dev" /> </InputGroup>
Suffix
<InputGroup> <InputGroupInput placeholder="80" /> <InputGroupAddon align="inline-end"> <InputGroupText>ms</InputGroupText> </InputGroupAddon> </InputGroup>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| InputGroupAddon.align | "inline-start" | "inline-end" | "inline-start" | |
| InputGroupInput | part | — | The field is still one Input. |
13
import { Checkbox, Field, FieldContent, FieldDescription, FieldLabel } from '@nin/ui'
Default
Square. Pair with Field orientation=horizontal.
Read the agreement before continuing.
<Field orientation="horizontal"> <Checkbox id="terms" /> <FieldContent> <FieldLabel htmlFor="terms">Accept Terms</FieldLabel> <FieldDescription>Read the agreement before continuing.</FieldDescription> </FieldContent> </Field>
Disabled
This option is unavailable.
<Field orientation="horizontal" data-disabled="true"> <Checkbox id="terms" disabled /> <FieldLabel htmlFor="terms">Accept Terms</FieldLabel> </Field>
Invalid
You must accept before continuing.
<Field orientation="horizontal" data-invalid="true"> <Checkbox id="terms" aria-invalid /> <FieldLabel htmlFor="terms">Accept Terms</FieldLabel> </Field>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| checked | boolean | — | Controlled. Uncontrolled uses defaultChecked. |
| disabled | boolean | false | |
| aria-invalid | boolean | — |
14
import { RadioGroup, RadioGroupItem, Field, FieldLabel } from '@nin/ui'
Default
Two to five exclusive options. Square, like Checkbox.
<RadioGroup defaultValue="email"> <Field orientation="horizontal"> <RadioGroupItem value="email" id="email" /> <FieldLabel htmlFor="email">Email</FieldLabel> </Field> </RadioGroup>
Disabled
<RadioGroup defaultValue="email" disabled>…</RadioGroup>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| value | string | — | Controlled. Uncontrolled uses defaultValue. |
| disabled | boolean | false | |
| RadioGroupItem.value | string | — | Required on each option. |
15
import { Switch, Field, FieldLabel } from '@nin/ui'
Default
Boolean, immediate. This is Geist Toggle. Segmented views use Toggle Group.
Send a digest when a deploy finishes.
<Field orientation="horizontal"> <Switch id="mail" defaultChecked /> <FieldContent> <FieldLabel htmlFor="mail">Email Notifications</FieldLabel> </FieldContent> </Field>
Size
<Switch size="sm" defaultChecked /> <Switch defaultChecked />
Disabled
<Switch disabled defaultChecked />
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| size | "sm" | "default" | "default" | |
| checked | boolean | — | Controlled. Uncontrolled uses defaultChecked. |
| disabled | boolean | false |
16
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@nin/ui'
Default
Fixed lists under about ten items. Longer or searchable lists use Combobox.
<Select defaultValue="prod"> <SelectTrigger> <SelectValue placeholder="Environment" /> </SelectTrigger> <SelectContent> <SelectItem value="prod">Production</SelectItem> <SelectItem value="preview">Preview</SelectItem> </SelectContent> </Select>
Disabled
<Select defaultValue="prod" disabled>…</Select>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| value | string | — | Controlled. Uncontrolled uses defaultValue. |
| disabled | boolean | false | |
| SelectTrigger.size | "sm" | "default" | "default" | 28px or 32px. |
17
import { NativeSelect, NativeSelectOption } from '@nin/ui'
Default
The platform select. Styled lists use Select. Searchable lists use Combobox.
<NativeSelect defaultValue="prod"> <NativeSelectOption value="prod">Production</NativeSelectOption> <NativeSelectOption value="preview">Preview</NativeSelectOption> </NativeSelect>
Disabled
<NativeSelect defaultValue="prod" disabled>…</NativeSelect>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| size | "sm" | "default" | "default" | |
| disabled | boolean | false |
18
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem } from '@nin/ui'
Default
Type to filter a known list. Free-form search is still Input.
<Combobox> <ComboboxInput placeholder="Region" /> <ComboboxContent> <ComboboxList> <ComboboxItem value="iad">Washington, D.C.</ComboboxItem> <ComboboxItem value="sfo">San Francisco</ComboboxItem> </ComboboxList> </ComboboxContent> </Combobox>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| value | string | — | Controlled. Pair with onValueChange. |
| ComboboxItem.value | string | — | The stored value, not the label. |
19
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@nin/ui'
Default
Six slots. Do not fake this with six Inputs.
<InputOTP maxLength={6}> <InputOTPGroup> <InputOTPSlot index={0} /> {/* … index 1–5 */} </InputOTPGroup> </InputOTP>
Disabled
<InputOTP maxLength={6} disabled>…</InputOTP>
Invalid
<InputOTP maxLength={6} aria-invalid>…</InputOTP>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| maxLength | number | 6 | Always six for email codes. |
| disabled | boolean | false | |
| aria-invalid | boolean | — |
20
import { Slider } from '@nin/ui'
Default
Continuous values. Discrete choices stay on Radio or Select.
<Slider defaultValue={[40]} />
Disabled
<Slider defaultValue={[40]} disabled />
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| value | number[] | — | Controlled. Uncontrolled uses defaultValue. |
| disabled | boolean | false |
21
import { Calendar } from '@nin/ui'
Default
Square days. A field that picks a date uses Date Picker.
<Calendar mode="single" selected={date} onSelect={setDate} />
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| mode | "single" | "multiple" | "range" | "single" | |
| selected | Date | — | Controlled. Pair with onSelect. |
| captionLayout | "label" | "dropdown" | "label" |
22
import { DatePicker } from '@nin/ui'
Default
Calendar inside Popover. Not a registry item — the composition is the component.
<DatePicker value={date} onValueChange={setDate} placeholder="Pick a date" />
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| value | Date | — | Controlled. Uncontrolled uses defaultValue. |
| onValueChange | (date?: Date) => void | — | |
| placeholder | string | "Pick a date" | |
| disabled | boolean | false |
26
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@nin/ui'
Default
Explain an icon button. Inverted ink — the only floating surface that flips.
<TooltipProvider> <Tooltip> <TooltipTrigger render={<Button size="icon" aria-label="Copy Link" />} /> <TooltipContent>Copy Link</TooltipContent> </Tooltip> </TooltipProvider>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| TooltipContent.side | "top" | "right" | "bottom" | "left" | "top" | |
| TooltipProvider.delay | number | 0 | Milliseconds before the tip opens. |
27
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@nin/ui'
Default
Hover preview. A click still uses Popover.
<HoverCard> <HoverCardTrigger render={<Button variant="ghost" />}>@nin</HoverCardTrigger> <HoverCardContent>Product UI for nin.dev</HoverCardContent> </HoverCard>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| HoverCardContent.side | "top" | "right" | "bottom" | "left" | "bottom" | |
| HoverCardContent.align | "start" | "center" | "end" | "center" |
28
import { Popover, PopoverTrigger, PopoverContent, PopoverHeader, PopoverTitle } from '@nin/ui'
Default
A small panel from a visible trigger. Confirmations stay in Dialog.
<Popover> <PopoverTrigger render={<Button variant="outline" />}>Open Popover</PopoverTrigger> <PopoverContent> <PopoverHeader> <PopoverTitle>Deploy Hook</PopoverTitle> </PopoverHeader> </PopoverContent> </Popover>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| open | boolean | — | Controlled. Pair with onOpenChange. |
| PopoverContent.side | "top" | "right" | "bottom" | "left" | "bottom" | |
| PopoverContent.align | "start" | "center" | "end" | "center" |
29
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@nin/ui'
Confirm
1px ring. Overlay is overlay, not inverted ink.
<Dialog> <DialogTrigger render={<Button variant="outline" />}>Open Dialog</DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Delete Project</DialogTitle> <DialogDescription>This action cannot be undone.</DialogDescription> </DialogHeader> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose> <Button variant="destructive">Delete Project</Button> </DialogFooter> </DialogContent> </Dialog>
Email code
Preview only. Six digits. No password.
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| open | boolean | — | Controlled. Pair with onOpenChange. |
| overlay | token | overlay | Built in. Do not stack your own dim. |
30
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogAction } from '@nin/ui'
Confirm
Must answer. Overlay is overlay.
<AlertDialog> <AlertDialogTrigger render={<Button variant="destructive" />}>Delete Project</AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Delete Project</AlertDialogTitle> <AlertDialogDescription>This cannot be undone.</AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction variant="destructive">Delete Project</AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| open | boolean | — | Controlled. Pair with onOpenChange. |
| AlertDialogAction.variant | Button variant | "default" | Destructive confirms. |
31
import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from '@nin/ui'
Default
A panel from the edge. Centered confirmations stay Dialog.
<Sheet> <SheetTrigger render={<Button variant="outline" />}>Open Sheet</SheetTrigger> <SheetContent side="right"> <SheetHeader> <SheetTitle>Project</SheetTitle> </SheetHeader> <SheetFooter> <Button>Save</Button> </SheetFooter> </SheetContent> </Sheet>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| open | boolean | — | Controlled. Pair with onOpenChange. |
| SheetContent.side | "top" | "right" | "bottom" | "left" | "right" |
32
import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle } from '@nin/ui'
Default
A panel you can swipe. Overlay is overlay. Everyday side panels still use Sheet.
<Drawer> <DrawerTrigger render={<Button variant="outline" />}>Open Drawer</DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Filters</DrawerTitle> </DrawerHeader> </DrawerContent> </Drawer>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| open | boolean | — | Controlled. Pair with onOpenChange. |
| swipeDirection | "up" | "down" | "left" | "right" | "down" |
33
import { CommandDialog, CommandInput, CommandList, CommandItem } from '@nin/ui'
Palette
Global ⌘K. A menu on one resource is still Dropdown Menu.
<CommandDialog open={open} onOpenChange={setOpen}> <CommandInput placeholder="Search projects…" /> <CommandList> <CommandEmpty>No results.</CommandEmpty> <CommandGroup heading="Projects"> <CommandItem>nin</CommandItem> </CommandGroup> </CommandList> </CommandDialog>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| open | boolean | — | On CommandDialog. Pair with onOpenChange. |
| CommandGroup.heading | string | — | Section label in the list. |
34
import { Alert, AlertDescription, AlertTitle } from '@nin/ui'
Default
Page-level message. Field errors stay on FieldError.
<Alert> <AlertTitle>Update Available</AlertTitle> <AlertDescription>A new version is ready to install.</AlertDescription> </Alert>
Destructive
<Alert variant="destructive"> <AlertTitle>Delete Failed</AlertTitle> <AlertDescription>The project could not be removed. Try again.</AlertDescription> </Alert>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| variant | "default" | "destructive" | "default" | Destructive is a failure, not a confirm. |
35
import { Button, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from '@nin/ui'
Default
No data. Loading a known layout is Skeleton, not Empty.
<Empty> <EmptyHeader> <EmptyTitle>No Projects Yet</EmptyTitle> <EmptyDescription>Push a repository to create your first project.</EmptyDescription> </EmptyHeader> <EmptyContent> <Button>Create Project</Button> </EmptyContent> </Empty>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| EmptyTitle | part | — | Name the missing thing. |
| EmptyContent | part | — | One action, or nothing. |
36
import { toast, Toaster } from '@nin/ui'
Default
Async receipt. The page root already mounts Toaster.
<Button variant="outline" onClick={() => toast('Deploy queued')}> Show Toast </Button>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| toast | (message: string) => void | — | Import toast from @nin/ui. Mount Toaster once at the root. |
37
import { Skeleton } from '@nin/ui'
Default
Known layout, data still coming. No data is Empty. A tap in flight stays on the Button.
<Skeleton className="h-4 w-40" /> <Skeleton className="h-4 w-64" /> <Skeleton className="h-8 w-full" />
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| className | string | — | Set the final width and height so the reveal does not reflow. |
38
import { Progress, ProgressLabel, ProgressValue } from '@nin/ui'
Default
Known percent. Do not invent a standalone spinner.
<Progress value={64}> <ProgressLabel>Upload</ProgressLabel> <ProgressValue /> </Progress>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| value | number | — | 0–100. Known percent only. |
39
import { Spinner } from '@nin/ui'
Default
An independent wait. A tap in flight stays on the Button — do not swap the button for this.
<Spinner /> <Spinner className="size-6" />
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| className | string | — | Size only. Do not restyle the icon into a pill. |
41
import { Pagination, PaginationContent, PaginationItem, PaginationLink } from '@nin/ui'
Default
<Pagination> <PaginationContent> <PaginationItem> <PaginationPrevious href="?page=1" /> </PaginationItem> <PaginationItem> <PaginationLink href="?page=1" isActive>1</PaginationLink> </PaginationItem> <PaginationItem> <PaginationNext href="?page=2" /> </PaginationItem> </PaginationContent> </Pagination>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| PaginationLink.isActive | boolean | false | The current page. |
42
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@nin/ui'
Default
Sibling views on one page. Unrelated pages are not tabs.
<Tabs defaultValue="overview"> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger> </TabsList> <TabsContent value="overview">Deploy 14 finished in 38s.</TabsContent> </Tabs>
Line
<Tabs defaultValue="overview"> <TabsList variant="line"> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger> </TabsList> </Tabs>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| defaultValue | string | — | Or value + onValueChange. |
| TabsList.variant | "default" | "line" | "default" |
44
import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount } from '@nin/ui'
Letter
Image first, then 1–2 uppercase letters. Placeholder is only the loading shell.
<Avatar> <AvatarFallback>AL</AvatarFallback> </Avatar>
Placeholder
Reserve the empty shell for loading. Never as a permanent fallback.
<Avatar> <AvatarFallback className="animate-pulse" /> </Avatar>
Size
24 next to compact type. 32 default. 40 in headers.
<Avatar size="sm">…</Avatar> <Avatar>…</Avatar> <Avatar size="lg">…</Avatar>
Group
Stack members. Overflow is AvatarGroupCount, not a fourth face.
<AvatarGroup> <Avatar><AvatarFallback>AL</AvatarFallback></Avatar> <Avatar><AvatarFallback>JD</AvatarFallback></Avatar> <AvatarGroupCount>+3</AvatarGroupCount> </AvatarGroup>
Badge
Status only. Not a decorative icon tile.
<Avatar> <AvatarFallback>AL</AvatarFallback> <AvatarBadge /> </Avatar>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| size | "sm" | "default" | "lg" | "default" | 24 / 32 / 40. |
| AvatarImage | part | — | Prefer a photo when you have one. |
| AvatarFallback | part | — | 1–2 letters, or the loading shell. |
45
import { Badge } from '@nin/ui'
Variants
Indexes, counts, plugin chips. Not decoration.
<Badge>12</Badge> <Badge variant="secondary">Preview</Badge> <Badge variant="outline">api / nin</Badge> <Badge variant="destructive">Failed</Badge>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| variant | "default" | "secondary" | "outline" | "destructive" | "default" |
46
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@nin/ui'
Frame
One bordered surface. Not a wall of cards, and not a card inside a card.
<Card> <CardHeader> <CardTitle>Production</CardTitle> <CardDescription>iad1 · ready</CardDescription> </CardHeader> <CardContent>Deploy 14 finished in 38s.</CardContent> <CardFooter> <Button>Open</Button> </CardFooter> </Card>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| size | "default" | "sm" | "default" | Tightens spacing. |
| CardAction | part | — | Sits on the header’s right. |
47
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@nin/ui'
Default
A frame of rows. Not a nested card.
| Project | Status | Updated |
|---|---|---|
| nin | Ready | 2m |
| design | Building | 14s |
<Table> <TableHeader> <TableRow> <TableHead>Project</TableHead> <TableHead>Status</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow> <TableCell>nin</TableCell> <TableCell>Ready</TableCell> </TableRow> </TableBody> </Table>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| TableHead | part | — | Column title. |
| TableCell | part | — | A cell. Numbers can be font-mono. |
48
import { Item, ItemContent, ItemTitle, ItemDescription } from '@nin/ui'
Default
One row in a list. Tables stay tables.
iad1 · ready
sfo1 · building
<Item> <ItemContent> <ItemTitle>Production</ItemTitle> <ItemDescription>iad1 · ready</ItemDescription> </ItemContent> </Item>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| ItemTitle | part | — | |
| ItemDescription | part | — | Secondary line. |
49
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@nin/ui'
Bar
Recharts inside ChartContainer. Color is --chart-1, not a hex.
<ChartContainer config={{ deploy: { label: 'Deploys', color: 'var(--chart-1)' } }}> <BarChart data={data}> <XAxis dataKey="month" /> <ChartTooltip content={<ChartTooltipContent />} /> <Bar dataKey="deploy" fill="var(--color-deploy)" /> </BarChart> </ChartContainer>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| config | ChartConfig | — | label + color per series. Color is --chart-*. |
| children | Recharts | — | BarChart, LineChart, and so on. |
50
import { Kbd, KbdGroup } from '@nin/ui'
Combination
Shortcut hints in menus and prose. font-mono.
<KbdGroup> <Kbd>⌘</Kbd> <Kbd>K</Kbd> </KbdGroup>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| Kbd | part | — | One key per Kbd. |
| KbdGroup | part | — | Wraps a combination. |
51
import { Separator } from '@nin/ui'
Default
Full-width hairline. Do not fake a rule with an empty div.
Overview
Logs
<p>Overview</p> <Separator /> <p>Logs</p>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" |
52
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@nin/ui'
Default
A stack of headings that reveal a section.
<Accordion defaultValue={['billing']}> <AccordionItem value="billing"> <AccordionTrigger>Billing</AccordionTrigger> <AccordionContent>Invoices close on the first of the month.</AccordionContent> </AccordionItem> </Accordion>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| defaultValue | string[] | — | Open items. Or value + onValueChange. |
| AccordionItem.value | string | — | Required. |
53
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@nin/ui'
Default
One block. A list of headings is Accordion.
<Collapsible> <CollapsibleTrigger render={<Button variant="outline" />}>Show Details</CollapsibleTrigger> <CollapsibleContent>Commit 4530d29. Ready in 38s.</CollapsibleContent> </Collapsible>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| open | boolean | — | Controlled. Pair with onOpenChange. |
54
import { ScrollArea } from '@nin/ui'
Default
<ScrollArea className="h-32"> {/* overflowing content */} </ScrollArea>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| className | string | — | Give it a height. The bar is built in. |
55
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@nin/ui'
Default
Adjacent panes. The handle is a 1px rule.
<ResizablePanelGroup> <ResizablePanel defaultSize={50}>Source</ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize={50}>Output</ResizablePanel> </ResizablePanelGroup>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" | On the group. |
| ResizablePanel.defaultSize | number | — | Percent. |
| ResizableHandle.withHandle | boolean | false |
56
import { AspectRatio } from '@nin/ui'
16 / 9
<AspectRatio ratio={16 / 9}> <img src="/cover.png" alt="" className="absolute inset-0 size-full object-cover" /> </AspectRatio>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| ratio | number | — | 16 / 9, 1, 4 / 3. Children are absolutely filled. |
57
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@nin/ui'
Default
Square slides. No dots.
<Carousel> <CarouselContent> <CarouselItem>Overview</CarouselItem> <CarouselItem>Logs</CarouselItem> </CarouselContent> <CarouselPrevious /> <CarouselNext /> </Carousel>
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" | |
| setApi | (api) => void | — | Embla API if you need it. |