jem-ui-components
jem-ui-components
Complete reference for AI agents consuming @jem-open/jem-ui in application projects. Covers every component's props, variants, and design tokens so you use the library correctly instead of recreating what already exists.
Step 1 — Check prerequisites
Verify the consuming project is set up to use jem-ui.
Check 1: Package installed
Look for @jem-open/jem-ui in package.json dependencies or devDependencies.
If not found, install it:
npm install @jem-open/jem-ui
Peer dependencies required: react@^18 | ^19, react-dom@^18 | ^19, tailwindcss@^3.4.
Check 2: Styles imported
The app entry point (e.g. layout.tsx, _app.tsx, or main.tsx) must import jem-ui styles:
import "@jem-open/jem-ui/styles.css"
If missing, add it.
Check 3: Tailwind preset configured
tailwind.config.js (or .ts) must include the jem-ui preset and content path:
const jemPreset = require("@jem-open/jem-ui/tailwind-preset");
module.exports = {
presets: [jemPreset],
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/@jem-open/jem-ui/dist/**/*.{js,mjs}",
],
};
If missing, add both the preset and the content path.
Halt if any check fails and cannot be auto-fixed.
Step 2 — Identify needed components
Before writing any UI code, scan this catalog to find existing components that match your needs. Do not recreate components that already exist in the library.
All components are imported from @jem-open/jem-ui:
import { ComponentName } from "@jem-open/jem-ui"
Forms
| Component | Description |
|---|---|
Button |
Primary action button with 9 variants (default, primary, secondary, destructive, approve, outline, subtle, ghost, link), loading state, and icon slots |
IconButton |
Icon-only button with square/circle shapes |
Input |
Base text input |
InputField |
Input with label, description, helper text, and error state |
SearchInput |
Search input with built-in clear button |
Checkbox |
Standalone checkbox |
CheckboxWithLabel |
Checkbox with label and optional description |
CheckboxCard |
Card-style checkbox with label and description |
RadioGroup, RadioGroupItem |
Radio button group with items |
RadioGroupItemWithLabel |
Radio item with label and optional description |
RadioGroupCard |
Card-style radio item |
Select, SelectTrigger, SelectContent, SelectItem, SelectValue |
Composable dropdown select |
SelectField |
Select wrapper with label and description |
DatePicker |
Single date picker with calendar popover |
DateRangePicker |
Date range picker with two-month calendar |
Calendar |
Standalone calendar component |
Textarea |
Multi-line text input |
TextareaField |
Textarea with label, description, helper text, and error state |
Switch |
Toggle switch |
SwitchWithLabel |
Switch with label and optional description |
Label |
Form label (Radix-based, associates with inputs) |
Upload |
File upload with progress states (default, uploading, uploaded) |
Navigation
| Component | Description |
|---|---|
Accordion, AccordionItem, AccordionTrigger, AccordionContent |
Collapsible content sections |
Tabs, TabsList, TabsTrigger, TabsContent |
Tabbed content with default and line variants |
Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator |
Navigation breadcrumbs with chevron/slash separator |
Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis |
Page navigation controls |
Link |
Styled anchor with variant (default, muted) and size (xs, sm, base) options |
Data Display
| Component | Description |
|---|---|
DataTable, DataTableColumnHeader |
Full-featured data table with TanStack Table integration, sorting, filtering, pagination |
Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption, TableFooter |
Primitive table building blocks |
Tag |
Status tag with 13 variants (default, success, processing, pending, failed, drafted, outline, outline-navy, neutral, pink, pink-text, lime, purple) |
DismissibleTag |
Tag with dismiss (X) button |
CountTag |
Small count badge (pink circle with white number) |
Avatar, AvatarImage, AvatarFallback |
User avatar with image and fallback |
AvatarBadge |
Online/status indicator badge for Avatar |
AvatarGroup, AvatarGroupCount |
Stacked avatar group with overflow count |
Progress |
Progress bar |
Divider |
Horizontal/vertical separator with label option and 5 style variants |
Skeleton |
Loading placeholder with pulse animation |
Feedback
| Component | Description |
|---|---|
Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose |
Modal dialog with close button |
Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerBody, DrawerSection, DrawerFooter, DrawerTitle, DrawerClose |
Side drawer panel (right/left/top/bottom) |
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator |
Context/action dropdown menu |
DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem |
Selection items within dropdown |
DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent |
Nested sub-menus |
Tooltip, TooltipProvider, TooltipTrigger, TooltipContent |
Hover tooltip with dark/light variants |
Popover, PopoverTrigger, PopoverContent |
Click-triggered popover |
Alert, AlertTitle, AlertDescription |
Alert message with 5 variants (default, success, warning, destructive, note) and auto icons |
EmptyState |
Empty state placeholder with icon, title, description, and action buttons |
EmptyStateNotFound, EmptyStateNoResults, EmptyStateNoData |
Pre-configured empty state variants |
Toaster |
Toast notifications — add once in root layout, trigger with toast() from sonner |
Design Tokens
| Component | Description |
|---|---|
Icon |
Sized icon wrapper (xs/sm/md/lg/xl) for Lucide icons with accessibility label |
PrimaryLogo |
Primary JEM logo with 4 variant/color options and 4 sizes |
SecondaryLogoRound |
Round secondary logo with 2 variants and 4 sizes |
SecondaryLogoSquare |
Square secondary logo with 2 variants and 4 sizes |
Utilities
| Export | Description |
|---|---|
cn |
Class name merger (clsx + tailwind-merge). Use this for ALL class composition. Never use template literals to combine Tailwind classes. |
Step 3 — Use components correctly
Detailed reference for each component. Organized by category.
Forms
Button
import { Button } from "@jem-open/jem-ui"
Props: extends React.ComponentProps<"button">
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "primary" | "secondary" | "destructive" | "approve" | "outline" | "subtle" | "ghost" | "link" |
"default" |
Visual style |
size |
"default" | "xs" | "sm" | "small" | "medium" | "lg" | "large" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" |
"default" |
Button size |
leftIcon |
React.ReactNode |
— | Icon before label |
rightIcon |
React.ReactNode |
— | Icon after label |
loading |
boolean |
false |
Shows loading spinner, disables button |
asChild |
boolean |
false |
Render as child element (use with links) |
Example:
<Button variant="primary" size="medium" leftIcon={<Plus />}>
Add item
</Button>
asChild example (Button as link):
<Button asChild variant="outline">
<a href="/settings">Settings</a>
</Button>
IconButton
import { IconButton } from "@jem-open/jem-ui"
Props: extends React.ComponentProps<"button">
| Prop | Type | Default | Description |
|---|---|---|---|
size |
"default" | "small" | "medium" | "large" |
"default" |
Button size |
shape |
"square" | "circle" |
"square" |
Button shape |
icon |
React.ReactNode |
— | Icon to display |
Example:
<IconButton icon={<Trash2 />} shape="circle" aria-label="Delete item" />
Note: Always provide aria-label for accessibility since there is no visible text.
Input
import { Input } from "@jem-open/jem-ui"
Props: extends React.ComponentProps<"input">
No additional props beyond standard HTML input attributes.
Example:
<Input type="email" placeholder="Enter email" />
InputField
import { InputField } from "@jem-open/jem-ui"
Props: extends Input props
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
— | Label text above input |
description |
string |
— | Description below label |
helperText |
string |
— | Helper text below input |
error |
boolean |
false |
Error state (red border, helper text turns red) |
icon |
React.ReactNode |
— | Icon inside input |
button |
React.ReactNode |
— | Button element inside input |
Example:
<InputField
label="Email"
description="We'll never share your email"
helperText={errors.email}
error={!!errors.email}
type="email"
placeholder="name@example.com"
/>
SearchInput
import { SearchInput } from "@jem-open/jem-ui"
Props: extends Input props
| Prop | Type | Default | Description |
|---|---|---|---|
onClear |
() => void |
— | Called when clear (X) button is clicked |
Shows a search icon on the left and a clear (X) button when there is a value.
Example:
const [query, setQuery] = useState("")
<SearchInput
value={query}
onChange={(e) => setQuery(e.target.value)}
onClear={() => setQuery("")}
placeholder="Search..."
/>
Checkbox
import { Checkbox } from "@jem-open/jem-ui"
Props: extends React.ComponentProps<typeof CheckboxPrimitive.Root> (Radix)
Standard Radix checkbox props: checked, onCheckedChange, disabled, name, value.
Example:
<Checkbox checked={agreed} onCheckedChange={setAgreed} />
CheckboxWithLabel
import { CheckboxWithLabel } from "@jem-open/jem-ui"
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
required | Label text |
description |
string |
— | Description below label |
Plus all Checkbox props.
Example:
<CheckboxWithLabel
label="Accept terms"
description="You agree to our terms of service"
checked={agreed}
onCheckedChange={setAgreed}
/>
CheckboxCard
import { CheckboxCard } from "@jem-open/jem-ui"
Same props as CheckboxWithLabel but renders as a card-style container.
Example:
<CheckboxCard
label="Email notifications"
description="Receive email updates about your account"
checked={emailNotifs}
onCheckedChange={setEmailNotifs}
/>
RadioGroup / RadioGroupItem
import { RadioGroup, RadioGroupItem } from "@jem-open/jem-ui"
RadioGroup props: extends React.ComponentProps<typeof RadioGroupPrimitive.Root> (Radix)
value,onValueChange,disabled,name- Layout: grid with gap-3
RadioGroupItem props: extends Radix RadioGroup.Item
value(required)
Example:
<RadioGroup value={plan} onValueChange={setPlan}>
<RadioGroupItem value="free" />
<RadioGroupItem value="pro" />
<RadioGroupItem value="enterprise" />
</RadioGroup>
Also available: RadioGroupItemWithLabel (adds label and description props) and RadioGroupCard (card-style).
RadioGroupItemWithLabel example:
<RadioGroup value={plan} onValueChange={setPlan}>
<RadioGroupItemWithLabel value="free" label="Free" description="Basic features" />
<RadioGroupItemWithLabel value="pro" label="Pro" description="All features" />
</RadioGroup>
Select (compound)
import {
Select, SelectTrigger, SelectContent, SelectItem, SelectValue
} from "@jem-open/jem-ui"
Composable compound component. Must be assembled from parts:
Example:
<Select value={role} onValueChange={setRole}>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
Also available: SelectField wraps Select with label and description:
<SelectField label="Role" description="User's permission level">
<Select value={role} onValueChange={setRole}>
{/* ... same inner content */}
</Select>
</SelectField>
Additional parts: SelectGroup, SelectLabel, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton.
DatePicker
import { DatePicker } from "@jem-open/jem-ui"
| Prop | Type | Default | Description |
|---|---|---|---|
date |
Date |
— | Selected date |
onDateChange |
(date: Date | undefined) => void |
— | Date change handler |
placeholder |
string |
— | Placeholder text |
disabled |
boolean |
false |
Disabled state |
label |
string |
— | Label above picker |
Example:
<DatePicker
label="Start date"
date={startDate}
onDateChange={setStartDate}
placeholder="Pick a date"
/>
DateRangePicker
import { DateRangePicker } from "@jem-open/jem-ui"
| Prop | Type | Default | Description |
|---|---|---|---|
dateRange |
DateRange |
— | Selected range { from?: Date, to?: Date } |
onDateRangeChange |
(range: DateRange | undefined) => void |
— | Range change handler |
placeholder |
string |
— | Placeholder text |
disabled |
boolean |
false |
Disabled state |
label |
string |
— | Label above picker |
Shows 2 months side by side. Format: "LLL dd, y" (e.g. "Mar 09, 2026").
Example:
<DateRangePicker
label="Date range"
dateRange={range}
onDateRangeChange={setRange}
placeholder="Select range"
/>
Calendar
import { Calendar } from "@jem-open/jem-ui"
Standalone calendar component. Props extend React DayPicker. Usually used inside DatePicker — use DatePicker instead unless you need a custom calendar layout.
Example:
<Calendar mode="single" selected={date} onSelect={setDate} />
Textarea / TextareaField
import { Textarea } from "@jem-open/jem-ui"
Textarea props: extends React.ComponentProps<"textarea">. Min height: 115px. Resize disabled.
TextareaField adds form wrapper props:
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
— | Label text |
description |
string |
— | Description below label |
helperText |
string |
— | Helper text below textarea |
error |
boolean |
false |
Error state |
Example:
<TextareaField
label="Notes"
helperText="Max 500 characters"
placeholder="Enter your notes..."
/>
Switch / SwitchWithLabel
import { Switch } from "@jem-open/jem-ui"
Switch props: extends Radix Switch. Key props: checked, onCheckedChange, disabled.
Checked: pink-900 background. Unchecked: slate-200.
SwitchWithLabel adds label and description props:
<SwitchWithLabel
label="Dark mode"
description="Use dark theme across the app"
checked={darkMode}
onCheckedChange={setDarkMode}
/>
Label
import { Label } from "@jem-open/jem-ui"
Radix-based label that properly associates with form inputs via htmlFor.
Example:
<Label htmlFor="email">Email address</Label>
<Input id="email" type="email" />
Note: InputField, TextareaField, SelectField, CheckboxWithLabel, and SwitchWithLabel handle labeling automatically. Use Label only when building custom form layouts.
Upload
import { Upload } from "@jem-open/jem-ui"
| Prop | Type | Default | Description |
|---|---|---|---|
state |
"default" | "uploading" | "uploaded" |
"default" |
Current upload state |
progress |
number |
— | Upload progress (0-100), shown during "uploading" |
fileName |
string |
— | Name of uploaded file |
title |
string |
— | Title text in upload area |
description |
string |
— | Description text in upload area |
maxSize |
string |
— | Max file size text (e.g. "10MB") |
onSelectFile |
() => void |
— | Called when user clicks to select file |
onRemoveFile |
() => void |
— | Called when user removes uploaded file |
onSubmit |
() => void |
— | Called when user submits uploaded file |
Example:
<Upload
state={uploadState}
progress={uploadProgress}
fileName={file?.name}
title="Upload document"
description="PDF, DOC, or DOCX"
maxSize="10MB"
onSelectFile={handleSelectFile}
onRemoveFile={handleRemoveFile}
onSubmit={handleSubmit}
/>
Navigation
Accordion (compound)
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@jem-open/jem-ui"
Accordion props: extends Radix Accordion.Root. Key props: type ("single" | "multiple"), collapsible, value, onValueChange.
Example:
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Section 1</AccordionTrigger>
<AccordionContent>Content for section 1</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Section 2</AccordionTrigger>
<AccordionContent>Content for section 2</AccordionContent>
</AccordionItem>
</Accordion>
Trigger text turns pink on hover and when open. ChevronDown icon rotates.
Tabs (compound)
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@jem-open/jem-ui"
TabsList variants:
| Variant | Description |
|---|---|
default |
Pink-100 background, white active tab |
line |
Border-bottom style, no background |
TabsTrigger variants: matches TabsList — use variant="line" on both for line style.
Example (default):
<Tabs defaultValue="general">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
</TabsList>
<TabsContent value="general">General settings</TabsContent>
<TabsContent value="security">Security settings</TabsContent>
</Tabs>
Example (line variant):
<Tabs defaultValue="overview">
<TabsList variant="line">
<TabsTrigger variant="line" value="overview">Overview</TabsTrigger>
<TabsTrigger variant="line" value="details">Details</TabsTrigger>
</TabsList>
<TabsContent value="overview">Overview content</TabsContent>
<TabsContent value="details">Details content</TabsContent>
</Tabs>
Breadcrumb (compound)
import {
Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink,
BreadcrumbPage, BreadcrumbSeparator
} from "@jem-open/jem-ui"
BreadcrumbSeparator accepts variant: "chevron" (default) or "slash".
BreadcrumbLink supports asChild for use with router links.
Example:
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/settings">Settings</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Profile</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
Pagination (compound)
import {
Pagination, PaginationContent, PaginationItem,
PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis
} from "@jem-open/jem-ui"
PaginationLink props: isActive (boolean) — active page gets outline variant with border. size from Button.
Example:
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
Link
import { Link } from "@jem-open/jem-ui"
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "muted" |
"default" |
default is pink-900, muted is greyscale-text-caption |
size |
"xs" | "sm" | "base" |
"xs" |
Font size: 12px, 14px, 16px |
Plus standard anchor attributes.
Example:
<Link href="/privacy" variant="muted" size="sm">Privacy Policy</Link>
Data Display
DataTable (compound)
import { DataTable, DataTableColumnHeader } from "@jem-open/jem-ui"
Uses TanStack React Table. Requires defining columns with ColumnDef.
| Prop | Type | Default | Description |
|---|---|---|---|
columns |
ColumnDef<TData, TValue>[] |
required | Column definitions |
data |
TData[] |
required | Data array |
filterColumn |
string |
— | Column key to filter on |
filterPlaceholder |
string |
— | Filter input placeholder |
showPagination |
boolean |
true |
Show pagination controls |
showToolbar |
boolean |
true |
Show toolbar with filter |
showRowsSelected |
boolean |
true |
Show selected row count |
toolbarChildren |
React.ReactNode |
— | Extra toolbar content |
DataTableColumnHeader enables sorting. Use it in column header definitions.
Example:
import { ColumnDef } from "@tanstack/react-table"
type User = { name: string; email: string; status: string }
const columns: ColumnDef<User>[] = [
{
accessorKey: "name",
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
},
{
accessorKey: "email",
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => <Tag variant="success">{row.getValue("status")}</Tag>,
},
]
<DataTable columns={columns} data={users} filterColumn="name" filterPlaceholder="Filter by name..." />
Table (compound)
import {
Table, TableHeader, TableBody, TableRow, TableHead, TableCell
} from "@jem-open/jem-ui"
Primitive table elements for custom table layouts. Use DataTable for most cases.
Example:
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Alice</TableCell>
<TableCell>Active</TableCell>
</TableRow>
</TableBody>
</Table>
Additional parts: TableCaption, TableFooter.
Tag / DismissibleTag / CountTag
import { Tag, DismissibleTag, CountTag } from "@jem-open/jem-ui"
Tag variants:
| Variant | Description |
|---|---|
default |
Navy-900 background |
success |
Green tones |
processing |
Blue-50 background |
pending |
Yellow-50 background |
failed |
Red-50 background |
drafted |
Grey tones |
outline |
Border only |
outline-navy |
Navy border |
neutral |
Neutral background |
pink |
Pink background |
pink-text |
Pink text, no background |
lime |
Lime background |
purple |
Purple background |
Tag example:
<Tag variant="success">Active</Tag>
<Tag variant="pending">Pending review</Tag>
DismissibleTag adds onDismiss callback:
<DismissibleTag variant="default" onDismiss={() => removeTag(id)}>
Filter: Active
</DismissibleTag>
CountTag — small pink circle with white number:
<CountTag>3</CountTag>
Avatar (compound)
import { Avatar, AvatarImage, AvatarFallback } from "@jem-open/jem-ui"
Avatar sizes:
| Size | Dimensions |
|---|---|
sm |
32px |
md |
40px (default) |
lg |
48px |
Example:
<Avatar size="lg">
<AvatarImage src="/user.jpg" alt="Jane Doe" />
<AvatarFallback size="lg">JD</AvatarFallback>
</Avatar>
Note: Pass size to both Avatar and AvatarFallback to keep dimensions consistent.
AvatarBadge — online/status indicator (green dot, absolute positioned):
<Avatar>
<AvatarImage src="/user.jpg" alt="Jane Doe" />
<AvatarFallback>JD</AvatarFallback>
<AvatarBadge />
</Avatar>
AvatarGroup / AvatarGroupCount — stacked avatars:
<AvatarGroup>
<Avatar><AvatarFallback>AB</AvatarFallback></Avatar>
<Avatar><AvatarFallback>CD</AvatarFallback></Avatar>
<Avatar><AvatarFallback>EF</AvatarFallback></Avatar>
<AvatarGroupCount>+5</AvatarGroupCount>
</AvatarGroup>
Progress
import { Progress } from "@jem-open/jem-ui"
Props: extends Radix Progress. Key prop: value (0-100).
Height: 6px. Indicator: secondary-pink-900.
Example:
<Progress value={65} />
Divider
import { Divider } from "@jem-open/jem-ui"
| Prop | Type | Default | Description |
|---|---|---|---|
orientation |
"horizontal" | "vertical" |
"horizontal" |
Direction |
variant |
"default" | "subtle" | "strong" | "primary" | "secondary" |
"default" |
Color style |
spacing |
"none" | "sm" | "md" | "lg" |
"none" |
Margin around divider |
label |
string |
— | Text label in center (horizontal only) |
Example:
<Divider spacing="md" />
<Divider label="OR" variant="subtle" spacing="lg" />
Skeleton
import { Skeleton } from "@jem-open/jem-ui"
Loading placeholder. Apply width/height via className.
Example:
<Skeleton className="h-12 w-full" />
<Skeleton className="h-4 w-3/4" />
Feedback
Dialog (compound)
import {
Dialog, DialogTrigger, DialogContent, DialogHeader,
DialogTitle, DialogDescription, DialogFooter, DialogClose
} from "@jem-open/jem-ui"
DialogContent props:
| Prop | Type | Default | Description |
|---|---|---|---|
showCloseButton |
boolean |
true |
Show X button in top-right |
Max width: max-w-md. Border radius: rounded-2xl.
DialogTitle variants:
| Variant | Description |
|---|---|
default |
greyscale-text-title color |
error |
secondary-pink-900 color (use for destructive actions) |
Example:
<Dialog>
<DialogTrigger asChild>
<Button>Open dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Make changes to your profile here.</DialogDescription>
</DialogHeader>
{/* Form content */}
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="primary">Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Also available: DialogContact (shows email link, default "support@example.com").
Drawer (compound)
import {
Drawer, DrawerTrigger, DrawerContent, DrawerHeader,
DrawerBody, DrawerFooter, DrawerTitle, DrawerClose
} from "@jem-open/jem-ui"
Drawer props:
| Prop | Type | Default | Description |
|---|---|---|---|
direction |
"right" | "left" | "top" | "bottom" |
"right" |
Side to open from |
DrawerHeader props: showCloseButton (default: true). Background: secondary-pink-50.
DrawerContent: max-w-[455px] for left/right, max-h-[80vh] for top/bottom.
Example:
<Drawer direction="right">
<DrawerTrigger asChild>
<Button variant="outline">View details</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>User Details</DrawerTitle>
</DrawerHeader>
<DrawerBody>
{/* Detail content */}
</DrawerBody>
<DrawerFooter>
<Button variant="primary">Edit</Button>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
Also available: DrawerSection (groups content within DrawerBody), DrawerDescription.
DropdownMenu (compound)
import {
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
DropdownMenuItem, DropdownMenuSeparator
} from "@jem-open/jem-ui"
DropdownMenuItem props:
| Prop | Type | Default | Description |
|---|---|---|---|
inset |
boolean |
false |
Add left padding (align with items that have icons) |
variant |
"default" | "destructive" |
"default" |
destructive shows red text |
Example:
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton icon={<MoreHorizontal />} aria-label="Actions" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Additional parts for selection: DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem.
Nested menus: DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent.
Grouping: DropdownMenuGroup, DropdownMenuLabel.
Keyboard shortcuts: DropdownMenuShortcut.
Tooltip (compound)
import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from "@jem-open/jem-ui"
TooltipContent variants:
| Variant | Description |
|---|---|
dark |
Navy-900 background, white text (default) |
light |
Neutral-100 background, title text |
Important: Wrap your app or layout in <TooltipProvider> once. Tooltips won't render without it.
Example:
// In layout.tsx — add once
<TooltipProvider>
{children}
</TooltipProvider>
// In any component
<Tooltip>
<TooltipTrigger asChild>
<IconButton icon={<Info />} aria-label="More info" />
</TooltipTrigger>
<TooltipContent variant="dark">
Additional information here
</TooltipContent>
</Tooltip>
Popover (compound)
import { Popover, PopoverTrigger, PopoverContent } from "@jem-open/jem-ui"
PopoverContent props: align (default: "center"), sideOffset (default: 4).
Example:
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Options</Button>
</PopoverTrigger>
<PopoverContent align="start">
{/* Popover content */}
</PopoverContent>
</Popover>
Also available: PopoverAnchor, PopoverHeader, PopoverTitle, PopoverDescription.
Alert (compound)
import { Alert, AlertTitle, AlertDescription } from "@jem-open/jem-ui"
Alert variants (each has an automatic icon):
| Variant | Icon | Description |
|---|---|---|
default |
Info | General information |
success |
CheckCircle2 | Success message |
warning |
AlertTriangle | Warning message |
destructive |
AlertCircle | Error message |
note |
Lightbulb | Note/tip |
| Prop | Type | Default | Description |
|---|---|---|---|
hideIcon |
boolean |
false |
Hide the automatic icon |
Example:
<Alert variant="warning">
<AlertTitle>Warning</AlertTitle>
<AlertDescription>Your session will expire in 5 minutes.</AlertDescription>
</Alert>
EmptyState
import { EmptyState } from "@jem-open/jem-ui"
Variants:
| Variant | Description |
|---|---|
default |
No background |
card |
greyscale-surface-subtle background |
bordered |
Border style |
Sizes: sm (max-w-sm), md (max-w-md, default), lg (max-w-lg)
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
icon |
"folder" | "alert" | "search" | "file" | "inbox" | "users" | React.ReactNode |
— | Icon to display |
title |
string |
required | Title text |
description |
string |
— | Description text |
primaryAction |
{ label: string; onClick?: () => void; href?: string } |
— | Primary action button |
secondaryAction |
{ label: string; onClick?: () => void; href?: string } |
— | Secondary action link |
footer |
React.ReactNode |
— | Custom footer content |
Example:
<EmptyState
icon="search"
title="No results found"
description="Try adjusting your search or filters"
primaryAction={{ label: "Clear filters", onClick: clearFilters }}
variant="card"
/>
Pre-configured variants: EmptyStateNotFound, EmptyStateNoResults, EmptyStateNoData.
Toaster
import { Toaster } from "@jem-open/jem-ui"
Add <Toaster /> once in your root layout. Trigger toasts using toast() from the sonner package:
// layout.tsx
import { Toaster } from "@jem-open/jem-ui"
export default function Layout({ children }) {
return (
<html>
<body>
{children}
<Toaster />
</body>
</html>
)
}
// any component
import { toast } from "sonner"
toast.success("Changes saved")
toast.error("Something went wrong")
toast.info("New update available")
toast.warning("Disk space low")
Icons are pre-configured: success (PartyPopper), error (TriangleAlert), info (Bell), warning (TriangleAlert), loading (Loader2 spinning).
Design Tokens (components)
Icon
import { Icon } from "@jem-open/jem-ui"
Sizes:
| Size | Dimensions |
|---|---|
xs |
12px |
sm |
16px |
md |
20px (default) |
lg |
24px |
xl |
32px |
| Prop | Type | Default | Description |
|---|---|---|---|
icon |
LucideIcon |
required | Lucide icon component |
label |
string |
— | Accessibility label |
Example:
import { Icon } from "@jem-open/jem-ui"
import { Settings } from "lucide-react"
<Icon icon={Settings} size="lg" label="Settings" />
Re-exported Lucide icons available from @jem-open/jem-ui: X, Menu, Check, ChevronDown, ChevronUp, ChevronLeft, ChevronRight, Home, Settings, Calendar, Users, Info, CircleAlert, AlertTriangle, Bell, Edit, Trash2, Download, Upload, Copy, Plus, Minus, Search, Filter, Eye, EyeOff, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, ExternalLink, MoreHorizontal, MoreVertical, RefreshCw, Loader2.
PrimaryLogo
import { PrimaryLogo } from "@jem-open/jem-ui"
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"bg-pink" | "bg-white" | "pink" | "white" |
"bg-pink" |
Color scheme |
size |
"sm" | "md" | "lg" | "xl" |
"md" |
sm=24px, md=40px, lg=64px, xl=96px |
Example:
<PrimaryLogo variant="bg-white" size="lg" />
SecondaryLogoRound
import { SecondaryLogoRound } from "@jem-open/jem-ui"
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"bg-pink" | "bg-white" |
"bg-pink" |
Color scheme |
size |
"sm" | "md" | "lg" | "xl" |
"md" |
sm=32px, md=48px, lg=64px, xl=96px |
SecondaryLogoSquare
import { SecondaryLogoSquare } from "@jem-open/jem-ui"
Same props as SecondaryLogoRound.
Step 4 — Apply design tokens
Use these design tokens for consistent styling. Always prefer semantic tokens over raw color values.
Colors — Semantic tokens
Use semantic tokens for all UI colors. These adapt correctly across themes.
| Category | CSS variable | Tailwind class | Purpose |
|---|---|---|---|
| Primary surface | --primary-surface-default |
bg-primary-surface-default |
Primary backgrounds (navy) |
| Primary surface | --primary-surface-lighter |
bg-primary-surface-lighter |
Lighter primary bg |
| Primary surface | --primary-surface-subtle |
bg-primary-surface-subtle |
Subtle primary bg |
| Primary surface | --primary-surface-darker |
bg-primary-surface-darker |
Darker primary bg |
| Primary border | --primary-border-default |
border-primary-border-default |
Primary borders |
| Primary border | --primary-border-lighter |
border-primary-border-lighter |
Lighter primary border |
| Primary border | --primary-border-subtle |
border-primary-border-subtle |
Subtle primary border |
| Primary border | --primary-border-darker |
border-primary-border-darker |
Darker primary border |
| Primary text | --primary-text-label |
text-primary-text-label |
Primary label text |
| Secondary surface | --secondary-surface-default |
bg-secondary-surface-default |
Secondary backgrounds (pink) |
| Secondary surface | --secondary-surface-lighter |
bg-secondary-surface-lighter |
Lighter secondary bg |
| Secondary surface | --secondary-surface-subtle |
bg-secondary-surface-subtle |
Subtle secondary bg |
| Secondary surface | --secondary-surface-darker |
bg-secondary-surface-darker |
Darker secondary bg |
| Secondary border | --secondary-border-default |
border-secondary-border-default |
Secondary borders |
| Secondary text | --secondary-text-label |
text-secondary-text-label |
Secondary label text |
| Error surface | --error-surface-default |
bg-error-surface-default |
Error backgrounds (red) |
| Error border | --error-border-default |
border-error-border-default |
Error borders |
| Error text | --error-text-label |
text-error-text-label |
Error label text |
| Warning surface | --warning-surface-default |
bg-warning-surface-default |
Warning backgrounds (orange) |
| Warning border | --warning-border-default |
border-warning-border-default |
Warning borders |
| Warning text | --warning-text-label |
text-warning-text-label |
Warning label text |
| Success surface | --success-surface-default |
bg-success-surface-default |
Success backgrounds (green) |
| Success border | --success-border-default |
border-success-border-default |
Success borders |
| Success text | --success-text-label |
text-success-text-label |
Success label text |
| Greyscale surface | --greyscale-surface-default |
bg-greyscale-surface-default |
Default page background |
| Greyscale surface | --greyscale-surface-subtle |
bg-greyscale-surface-subtle |
Subtle background |
| Greyscale surface | --greyscale-surface-disabled |
bg-greyscale-surface-disabled |
Disabled background |
| Greyscale border | --greyscale-border-default |
border-greyscale-border-default |
Default borders |
| Greyscale border | --greyscale-border-disabled |
border-greyscale-border-disabled |
Disabled borders |
| Greyscale border | --greyscale-border-darker |
border-greyscale-border-darker |
Darker borders |
| Greyscale text | --greyscale-text-title |
text-greyscale-text-title |
Page titles |
| Greyscale text | --greyscale-text-body |
text-greyscale-text-body |
Body text |
| Greyscale text | --greyscale-text-subtitle |
text-greyscale-text-subtitle |
Subtitles |
| Greyscale text | --greyscale-text-caption |
text-greyscale-text-caption |
Captions, hints |
| Greyscale text | --greyscale-text-negative |
text-greyscale-text-negative |
Inverted text (white) |
| Greyscale text | --greyscale-text-disabled |
text-greyscale-text-disabled |
Disabled text |
Colors — Base palette
Use sparingly. Prefer semantic tokens above. Available for decorative/custom elements only.
| Palette | Tailwind prefix | Shades |
|---|---|---|
| Navy | navy- |
50, 100, 200, 300, 400, 500, 600, 700, 800, 900 |
| Pink | pink- |
50, 100, 200, 300, 400, 500, 600, 700, 800, 900 |
| Lime | lime- |
50–900 |
| Purple | purple- |
50–900 |
| Violet | violet- |
50–900 |
| Blue | blue- |
50–900 |
| Green | green- |
50–900 |
| Orange | orange- |
50–900 |
| Yellow | yellow- |
50–900 |
| Red | red- |
50–900 |
| Neutral | neutral- |
50–900, white, cream, black |
Usage: bg-navy-100, text-pink-900, border-green-500, etc.
Spacing
| Token | Tailwind class | Value |
|---|---|---|
| none | p-none, m-none, gap-none |
0px |
| xxxxs | p-xxxxs, m-xxxxs, gap-xxxxs |
2px |
| xxxs | p-xxxs, m-xxxs, gap-xxxs |
4px |
| xxs | p-xxs, m-xxs, gap-xxs |
8px |
| xs | p-xs, m-xs, gap-xs |
12px |
| sm | p-sm, m-sm, gap-sm |
16px |
| md | p-md, m-md, gap-md |
24px |
| lg | p-lg, m-lg, gap-lg |
32px |
| xl | p-xl, m-xl, gap-xl |
48px |
| xxl | p-xxl, m-xxl, gap-xxl |
64px |
| xxxl | p-xxxl, m-xxxl, gap-xxxl |
96px |
| xxxxl | p-xxxxl, m-xxxxl, gap-xxxxl |
128px |
These tokens work with all Tailwind spacing utilities: p-, px-, py-, m-, mx-, my-, gap-, space-x-, space-y-, w-, h-, etc.
Typography
Font families:
font-heading— heading font (from--font-family-heading)font-body— body font (from--font-family-body)
Font sizes:
| Token | Tailwind class | Value |
|---|---|---|
| xxs | text-xxs |
10px |
| xs | text-xs |
12px |
| sm | text-sm |
14px |
| base | text-base |
16px |
| lg | text-lg |
18px |
| xl | text-xl |
20px |
| 2xl | text-2xl |
24px |
| 3xl | text-3xl |
30px |
| 4xl | text-4xl |
36px |
| 5xl | text-5xl |
48px |
| 6xl | text-6xl |
60px |
| 7xl | text-7xl |
72px |
| 8xl | text-8xl |
96px |
| 9xl | text-9xl |
128px |
Font weights:
| Weight | Tailwind class | Value |
|---|---|---|
| light | font-light |
300 |
| regular | font-regular |
400 |
| medium | font-medium |
500 |
| semibold | font-semibold |
600 |
| bold | font-bold |
700 |
| extrabold | font-extrabold |
800 |
| black | font-black |
900 |
Border radius
| Token | Tailwind class | Value |
|---|---|---|
| none | rounded-none |
0px |
| xs | rounded-xs |
2px |
| sm | rounded-sm |
4px |
| md | rounded-md |
6px |
| lg | rounded-lg |
8px |
| xl | rounded-xl |
12px |
| 2xl | rounded-2xl |
16px |
| 3xl | rounded-3xl |
24px |
| 4xl | rounded-4xl |
32px |
| full | rounded-full |
100px |
Step 5 — Avoid common mistakes
| Mistake | Why it's wrong | Correction |
|---|---|---|
| Building a custom button, input, or select component | jem-ui already provides these with consistent styling and accessibility | Check the catalog in Step 2 — the component likely exists |
Using raw hex colors like bg-[#062133] or text-[#ff697f] |
Bypasses the design system; breaks if tokens change | Use semantic tokens: bg-primary-surface-default, text-secondary-text-label |
Merging classes with template literals: `${base} ${conditional}` |
Tailwind classes can conflict (e.g. p-4 and p-2); template literals don't resolve conflicts |
Use cn() from @jem-open/jem-ui: cn("p-4", isCompact && "p-2") |
Wrapping a Button in an anchor: <a><Button>Click</Button></a> |
Nested interactive elements — accessibility violation, unexpected behavior | Use asChild: <Button asChild><a href="/path">Click</a></Button> |
Writing style={{ padding: '16px' }} or className="p-4" |
Bypasses design tokens; p-4 is Tailwind default (16px) not jem-ui token |
Use spacing tokens: className="p-sm" (16px via jem-ui token) |
Hardcoding #ff697f for pink or #062133 for navy |
Raw hex breaks if the palette changes | Use text-pink-500 or semantic text-secondary-text-label |
Forgetting TooltipProvider wrapper |
Tooltips silently won't render | Wrap your app or layout in <TooltipProvider> once |
Adding Toaster in every page component |
Creates duplicate toasts | Add <Toaster /> once in the root layout |
Using <div onClick> for clickable elements |
Not keyboard accessible, no focus management, no semantic meaning | Use <Button variant="ghost"> or the appropriate interactive component |