jem-ui-components

Complete reference for using @jem-open/jem-ui components — props, variants, design tokens, setup, and common mistakes to avoid.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "jem-ui-components" with this command: npx skills add jem-open/jem-agent-skills/jem-open-jem-agent-skills-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

ComponentDescription
ButtonPrimary action button with 9 variants (default, primary, secondary, destructive, approve, outline, subtle, ghost, link), loading state, and icon slots
IconButtonIcon-only button with square/circle shapes
InputBase text input
InputFieldInput with label, description, helper text, and error state
SearchInputSearch input with built-in clear button
CheckboxStandalone checkbox
CheckboxWithLabelCheckbox with label and optional description
CheckboxCardCard-style checkbox with label and description
RadioGroup, RadioGroupItemRadio button group with items
RadioGroupItemWithLabelRadio item with label and optional description
RadioGroupCardCard-style radio item
Select, SelectTrigger, SelectContent, SelectItem, SelectValueComposable dropdown select
SelectFieldSelect wrapper with label and description
DatePickerSingle date picker with calendar popover
DateRangePickerDate range picker with two-month calendar
CalendarStandalone calendar component
TextareaMulti-line text input
TextareaFieldTextarea with label, description, helper text, and error state
SwitchToggle switch
SwitchWithLabelSwitch with label and optional description
LabelForm label (Radix-based, associates with inputs)
UploadFile upload with progress states (default, uploading, uploaded)

Navigation

ComponentDescription
Accordion, AccordionItem, AccordionTrigger, AccordionContentCollapsible content sections
Tabs, TabsList, TabsTrigger, TabsContentTabbed content with default and line variants
Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparatorNavigation breadcrumbs with chevron/slash separator
Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsisPage navigation controls
LinkStyled anchor with variant (default, muted) and size (xs, sm, base) options

Data Display

ComponentDescription
DataTable, DataTableColumnHeaderFull-featured data table with TanStack Table integration, sorting, filtering, pagination
Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption, TableFooterPrimitive table building blocks
TagStatus tag with 13 variants (default, success, processing, pending, failed, drafted, outline, outline-navy, neutral, pink, pink-text, lime, purple)
DismissibleTagTag with dismiss (X) button
CountTagSmall count badge (pink circle with white number)
Avatar, AvatarImage, AvatarFallbackUser avatar with image and fallback
AvatarBadgeOnline/status indicator badge for Avatar
AvatarGroup, AvatarGroupCountStacked avatar group with overflow count
ProgressProgress bar
DividerHorizontal/vertical separator with label option and 5 style variants
SkeletonLoading placeholder with pulse animation

Feedback

ComponentDescription
Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogCloseModal dialog with close button
Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerBody, DrawerSection, DrawerFooter, DrawerTitle, DrawerCloseSide drawer panel (right/left/top/bottom)
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparatorContext/action dropdown menu
DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItemSelection items within dropdown
DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContentNested sub-menus
Tooltip, TooltipProvider, TooltipTrigger, TooltipContentHover tooltip with dark/light variants
Popover, PopoverTrigger, PopoverContentClick-triggered popover
Alert, AlertTitle, AlertDescriptionAlert message with 5 variants (default, success, warning, destructive, note) and auto icons
EmptyStateEmpty state placeholder with icon, title, description, and action buttons
EmptyStateNotFound, EmptyStateNoResults, EmptyStateNoDataPre-configured empty state variants
ToasterToast notifications — add once in root layout, trigger with toast() from sonner

Design Tokens

ComponentDescription
IconSized icon wrapper (xs/sm/md/lg/xl) for Lucide icons with accessibility label
PrimaryLogoPrimary JEM logo with 4 variant/color options and 4 sizes
SecondaryLogoRoundRound secondary logo with 2 variants and 4 sizes
SecondaryLogoSquareSquare secondary logo with 2 variants and 4 sizes

Utilities

ExportDescription
cnClass 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">

PropTypeDefaultDescription
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
leftIconReact.ReactNodeIcon before label
rightIconReact.ReactNodeIcon after label
loadingbooleanfalseShows loading spinner, disables button
asChildbooleanfalseRender 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">

PropTypeDefaultDescription
size"default" | "small" | "medium" | "large""default"Button size
shape"square" | "circle""square"Button shape
iconReact.ReactNodeIcon 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

PropTypeDefaultDescription
labelstringLabel text above input
descriptionstringDescription below label
helperTextstringHelper text below input
errorbooleanfalseError state (red border, helper text turns red)
iconReact.ReactNodeIcon inside input
buttonReact.ReactNodeButton 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

PropTypeDefaultDescription
onClear() => voidCalled 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"
PropTypeDefaultDescription
labelstringrequiredLabel text
descriptionstringDescription 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"
PropTypeDefaultDescription
dateDateSelected date
onDateChange(date: Date | undefined) => voidDate change handler
placeholderstringPlaceholder text
disabledbooleanfalseDisabled state
labelstringLabel above picker

Example:

<DatePicker
  label="Start date"
  date={startDate}
  onDateChange={setStartDate}
  placeholder="Pick a date"
/>

DateRangePicker

import { DateRangePicker } from "@jem-open/jem-ui"
PropTypeDefaultDescription
dateRangeDateRangeSelected range { from?: Date, to?: Date }
onDateRangeChange(range: DateRange | undefined) => voidRange change handler
placeholderstringPlaceholder text
disabledbooleanfalseDisabled state
labelstringLabel 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:

PropTypeDefaultDescription
labelstringLabel text
descriptionstringDescription below label
helperTextstringHelper text below textarea
errorbooleanfalseError 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"
PropTypeDefaultDescription
state"default" | "uploading" | "uploaded""default"Current upload state
progressnumberUpload progress (0-100), shown during "uploading"
fileNamestringName of uploaded file
titlestringTitle text in upload area
descriptionstringDescription text in upload area
maxSizestringMax file size text (e.g. "10MB")
onSelectFile() => voidCalled when user clicks to select file
onRemoveFile() => voidCalled when user removes uploaded file
onSubmit() => voidCalled 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:

VariantDescription
defaultPink-100 background, white active tab
lineBorder-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"
PropTypeDefaultDescription
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.

PropTypeDefaultDescription
columnsColumnDef<TData, TValue>[]requiredColumn definitions
dataTData[]requiredData array
filterColumnstringColumn key to filter on
filterPlaceholderstringFilter input placeholder
showPaginationbooleantrueShow pagination controls
showToolbarbooleantrueShow toolbar with filter
showRowsSelectedbooleantrueShow selected row count
toolbarChildrenReact.ReactNodeExtra 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:

VariantDescription
defaultNavy-900 background
successGreen tones
processingBlue-50 background
pendingYellow-50 background
failedRed-50 background
draftedGrey tones
outlineBorder only
outline-navyNavy border
neutralNeutral background
pinkPink background
pink-textPink text, no background
limeLime background
purplePurple 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:

SizeDimensions
sm32px
md40px (default)
lg48px

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"
PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Direction
variant"default" | "subtle" | "strong" | "primary" | "secondary""default"Color style
spacing"none" | "sm" | "md" | "lg""none"Margin around divider
labelstringText 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:

PropTypeDefaultDescription
showCloseButtonbooleantrueShow X button in top-right

Max width: max-w-md. Border radius: rounded-2xl.

DialogTitle variants:

VariantDescription
defaultgreyscale-text-title color
errorsecondary-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:

PropTypeDefaultDescription
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:

PropTypeDefaultDescription
insetbooleanfalseAdd 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:

VariantDescription
darkNavy-900 background, white text (default)
lightNeutral-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):

VariantIconDescription
defaultInfoGeneral information
successCheckCircle2Success message
warningAlertTriangleWarning message
destructiveAlertCircleError message
noteLightbulbNote/tip
PropTypeDefaultDescription
hideIconbooleanfalseHide 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:

VariantDescription
defaultNo background
cardgreyscale-surface-subtle background
borderedBorder style

Sizes: sm (max-w-sm), md (max-w-md, default), lg (max-w-lg)

Props:

PropTypeDefaultDescription
icon"folder" | "alert" | "search" | "file" | "inbox" | "users" | React.ReactNodeIcon to display
titlestringrequiredTitle text
descriptionstringDescription text
primaryAction{ label: string; onClick?: () => void; href?: string }Primary action button
secondaryAction{ label: string; onClick?: () => void; href?: string }Secondary action link
footerReact.ReactNodeCustom 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:

SizeDimensions
xs12px
sm16px
md20px (default)
lg24px
xl32px
PropTypeDefaultDescription
iconLucideIconrequiredLucide icon component
labelstringAccessibility 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"
PropTypeDefaultDescription
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"
PropTypeDefaultDescription
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.

CategoryCSS variableTailwind classPurpose
Primary surface--primary-surface-defaultbg-primary-surface-defaultPrimary backgrounds (navy)
Primary surface--primary-surface-lighterbg-primary-surface-lighterLighter primary bg
Primary surface--primary-surface-subtlebg-primary-surface-subtleSubtle primary bg
Primary surface--primary-surface-darkerbg-primary-surface-darkerDarker primary bg
Primary border--primary-border-defaultborder-primary-border-defaultPrimary borders
Primary border--primary-border-lighterborder-primary-border-lighterLighter primary border
Primary border--primary-border-subtleborder-primary-border-subtleSubtle primary border
Primary border--primary-border-darkerborder-primary-border-darkerDarker primary border
Primary text--primary-text-labeltext-primary-text-labelPrimary label text
Secondary surface--secondary-surface-defaultbg-secondary-surface-defaultSecondary backgrounds (pink)
Secondary surface--secondary-surface-lighterbg-secondary-surface-lighterLighter secondary bg
Secondary surface--secondary-surface-subtlebg-secondary-surface-subtleSubtle secondary bg
Secondary surface--secondary-surface-darkerbg-secondary-surface-darkerDarker secondary bg
Secondary border--secondary-border-defaultborder-secondary-border-defaultSecondary borders
Secondary text--secondary-text-labeltext-secondary-text-labelSecondary label text
Error surface--error-surface-defaultbg-error-surface-defaultError backgrounds (red)
Error border--error-border-defaultborder-error-border-defaultError borders
Error text--error-text-labeltext-error-text-labelError label text
Warning surface--warning-surface-defaultbg-warning-surface-defaultWarning backgrounds (orange)
Warning border--warning-border-defaultborder-warning-border-defaultWarning borders
Warning text--warning-text-labeltext-warning-text-labelWarning label text
Success surface--success-surface-defaultbg-success-surface-defaultSuccess backgrounds (green)
Success border--success-border-defaultborder-success-border-defaultSuccess borders
Success text--success-text-labeltext-success-text-labelSuccess label text
Greyscale surface--greyscale-surface-defaultbg-greyscale-surface-defaultDefault page background
Greyscale surface--greyscale-surface-subtlebg-greyscale-surface-subtleSubtle background
Greyscale surface--greyscale-surface-disabledbg-greyscale-surface-disabledDisabled background
Greyscale border--greyscale-border-defaultborder-greyscale-border-defaultDefault borders
Greyscale border--greyscale-border-disabledborder-greyscale-border-disabledDisabled borders
Greyscale border--greyscale-border-darkerborder-greyscale-border-darkerDarker borders
Greyscale text--greyscale-text-titletext-greyscale-text-titlePage titles
Greyscale text--greyscale-text-bodytext-greyscale-text-bodyBody text
Greyscale text--greyscale-text-subtitletext-greyscale-text-subtitleSubtitles
Greyscale text--greyscale-text-captiontext-greyscale-text-captionCaptions, hints
Greyscale text--greyscale-text-negativetext-greyscale-text-negativeInverted text (white)
Greyscale text--greyscale-text-disabledtext-greyscale-text-disabledDisabled text

Colors — Base palette

Use sparingly. Prefer semantic tokens above. Available for decorative/custom elements only.

PaletteTailwind prefixShades
Navynavy-50, 100, 200, 300, 400, 500, 600, 700, 800, 900
Pinkpink-50, 100, 200, 300, 400, 500, 600, 700, 800, 900
Limelime-50–900
Purplepurple-50–900
Violetviolet-50–900
Blueblue-50–900
Greengreen-50–900
Orangeorange-50–900
Yellowyellow-50–900
Redred-50–900
Neutralneutral-50–900, white, cream, black

Usage: bg-navy-100, text-pink-900, border-green-500, etc.

Spacing

TokenTailwind classValue
nonep-none, m-none, gap-none0px
xxxxsp-xxxxs, m-xxxxs, gap-xxxxs2px
xxxsp-xxxs, m-xxxs, gap-xxxs4px
xxsp-xxs, m-xxs, gap-xxs8px
xsp-xs, m-xs, gap-xs12px
smp-sm, m-sm, gap-sm16px
mdp-md, m-md, gap-md24px
lgp-lg, m-lg, gap-lg32px
xlp-xl, m-xl, gap-xl48px
xxlp-xxl, m-xxl, gap-xxl64px
xxxlp-xxxl, m-xxxl, gap-xxxl96px
xxxxlp-xxxxl, m-xxxxl, gap-xxxxl128px

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:

TokenTailwind classValue
xxstext-xxs10px
xstext-xs12px
smtext-sm14px
basetext-base16px
lgtext-lg18px
xltext-xl20px
2xltext-2xl24px
3xltext-3xl30px
4xltext-4xl36px
5xltext-5xl48px
6xltext-6xl60px
7xltext-7xl72px
8xltext-8xl96px
9xltext-9xl128px

Font weights:

WeightTailwind classValue
lightfont-light300
regularfont-regular400
mediumfont-medium500
semiboldfont-semibold600
boldfont-bold700
extraboldfont-extrabold800
blackfont-black900

Border radius

TokenTailwind classValue
nonerounded-none0px
xsrounded-xs2px
smrounded-sm4px
mdrounded-md6px
lgrounded-lg8px
xlrounded-xl12px
2xlrounded-2xl16px
3xlrounded-3xl24px
4xlrounded-4xl32px
fullrounded-full100px

Step 5 — Avoid common mistakes

MistakeWhy it's wrongCorrection
Building a custom button, input, or select componentjem-ui already provides these with consistent styling and accessibilityCheck 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 changeUse 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 conflictsUse 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 behaviorUse 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 tokenUse spacing tokens: className="p-sm" (16px via jem-ui token)
Hardcoding #ff697f for pink or #062133 for navyRaw hex breaks if the palette changesUse text-pink-500 or semantic text-secondary-text-label
Forgetting TooltipProvider wrapperTooltips silently won't renderWrap your app or layout in <TooltipProvider> once
Adding Toaster in every page componentCreates duplicate toastsAdd <Toaster /> once in the root layout
Using <div onClick> for clickable elementsNot keyboard accessible, no focus management, no semantic meaningUse <Button variant="ghost"> or the appropriate interactive component

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

review-fix-loop

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

jem-ui-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

jem-ui-recipes

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

clinic-visit-prep

帮助患者整理就诊前问题、既往记录、检查清单与时间线,不提供诊断。;use for healthcare, intake, prep workflows;do not use for 给诊断结论, 替代医生意见.

Archived SourceRecently Updated