The FlyCommerce design system for React: accessible components, design tokens and ready-made patterns that give every FlyCommerce surface (the merchant dashboard, the hub and third-party apps) the same look and behaviour.
Browse the component gallery →
Built on shadcn/ui, Radix UI and Tailwind CSS v4, restyled to FlyCommerce's design.
- 90+ components. The complete shadcn set plus commerce patterns: data tables with filters, media library, category picker, rich-text editor, save bar, stat cards and more.
- One source of truth for the look. Colour, radius, shadow and type tokens with light and dark themes. Screens compose components; they never restyle them.
- Accessible by default. Keyboard support throughout, focus rings for keyboard users only, labelled controls, screen-reader announcements for drag and drop and uploads.
- Ready for every market. RTL layouts, and every user-visible string can be translated through props.
- Mobile first. Every component is checked at 375px.
- Lightweight. Tree-shakeable ESM with TypeScript types: an app pays only for what it imports (a
Buttonadds about 14 KB gzipped). Heavy extras like charts and the code editor are opt-in.
- Requirements
- Installation
- Setup
- Quick start
- Components
- Recipes
- Design tokens
- Icons
- Accessibility and localisation
- Building with Claude Code
- Versioning
- Security
- Contributing
- License
| React | 19 |
| Styling | Tailwind CSS 4 (recommended), or the precompiled stylesheet for apps without Tailwind |
| Module format | ESM only |
| Toasts | sonner 2, a peer dependency: install it with the library |
| Charts | Optional: install recharts 3 only if you use @flycommerce/ui/chart |
| Code editor | Optional: install CodeMirror 6 only if you use @flycommerce/ui/code-editor |
| Browsers | Those Tailwind CSS 4 supports: Chrome 111+, Safari 16.4+, Firefox 128+ |
pnpm add @flycommerce/ui sonner
# or
npm install @flycommerce/ui sonner
# or
yarn add @flycommerce/ui sonnersonner (2.x) is a peer dependency, so toast() from the library and your app's <Toaster> share one copy. npm and pnpm install it for you; Yarn doesn't.
Apps using Tailwind CSS 4. Add the library after Tailwind in your main stylesheet. It registers the design tokens and tells Tailwind to scan the library's components:
@import "tailwindcss";
@import "@flycommerce/ui/tailwind.css";
@import "@flycommerce/ui/fonts.css"; /* Inter, if your app doesn't load it already */Adopting the library page by page? Import @flycommerce/ui/tailwind-core.css instead of tailwind.css. It leaves out the global base layer (page background and text colour on body, border and focus-outline colours on every element, Inter on html), so pages you haven't migrated keep their look. Switch to tailwind.css once the whole app uses the library.
Apps without Tailwind. Import the precompiled stylesheet once at the app root. It contains no CSS reset, so it won't restyle the rest of your page:
import "@flycommerce/ui/styles.css"
import "@flycommerce/ui/fonts.css"Wrap the app once:
import { Toaster, TooltipProvider } from "@flycommerce/ui"
export function Root({ children }: { children: React.ReactNode }) {
return (
<TooltipProvider>
{children}
<Toaster />
</TooltipProvider>
)
}- Dark mode: set
data-theme="dark"(or thedarkclass) on<html>. - Right-to-left: set
dir="rtl"on<html>and wrap the app in<DirectionProvider dir="rtl">.
import { useState } from "react"
import {
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Field,
FieldLabel,
Input,
SwitchField,
toast,
} from "@flycommerce/ui"
export function StoreSettings() {
const [name, setName] = useState("Trendy Store")
return (
<Card>
<CardHeader className="border-b">
<CardTitle>Store details</CardTitle>
<CardDescription>
Shown on your storefront and invoices.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-5">
<Field>
<FieldLabel htmlFor="store-name" required>
Store name
</FieldLabel>
<Input
id="store-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</Field>
<SwitchField
title="Accept reviews"
description="Let customers rate products they bought."
/>
<Button
className="justify-self-start"
onClick={() => toast.success("Saved")}
>
Save changes
</Button>
</CardContent>
</Card>
)
}The base components can also be imported one by one, for example import { Button } from "@flycommerce/ui/button". Either way, bundlers only include what you use.
The live gallery shows every component, variant and state in light, dark, RTL and mobile layouts, with a Code tab to copy each example.
| Category | Components |
|---|---|
| Actions | Button, ButtonArrow, ButtonGroup, Toggle, ToggleGroup, CopyButton |
| Forms | Field, Input, InputGroup, Textarea, PasswordInput, SearchInput, CopyField, ColorInput, Select, Combobox, AsyncCombobox, RichSelect, TreeSelect, TagInput, OptionListEditor, Checkbox, RadioGroup, RadioCard, CheckboxCard, Switch, SwitchField, Slider, InputOTP, DatePicker, DateRangePicker, RichTextEditor, Dropzone, ImageUploadField, Rating |
| Data display | DataTable, Table, TableFilters, ActiveFilters, MediaCell, StatCard, StatGroup, Badge, StatusBadge, Avatar, ImageWithFallback, BrandMark, CardBrandMark, Item, Kbd |
| Media | MediaPickerDialog, MediaGrid, MediaTile, MediaDetailsPanel, UploadQueue, VideoUrlList |
| Feedback | Alert, toast, Progress, Skeleton, Spinner, Empty, EmptyState, LoadingOverlay |
| Overlays | Dialog, ConfirmDialog, AlertDialog, Sheet, Drawer, Popover, Tooltip, InfoTooltip, HoverCard, DropdownMenu, ContextMenu |
| Navigation and layout | Sidebar, PageHeader, NavTabs, Tabs, Breadcrumb, Pagination, Card, Tray, Accordion, Collapsible, Separator, ScrollArea, ResizablePanelGroup, SaveBar |
| Foundations | Icon, Text, design tokens, type-* typography utilities |
Charts live in their own entry point so apps that don't chart never install or bundle a chart library:
pnpm add rechartsimport {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@flycommerce/ui/chart"The code editor ships the same way, as @flycommerce/ui/code-editor, with CodeMirror 6 as optional peer dependencies:
pnpm add @codemirror/commands @codemirror/lang-css @codemirror/lang-html @codemirror/lang-javascript @codemirror/lang-json @codemirror/language @codemirror/state @codemirror/viewimport { CodeEditor } from "@flycommerce/ui/code-editor"import { useState } from "react"
import {
ActiveFilters,
DataTable,
MediaCell,
SearchInput,
StatusBadge,
TableFilters,
type ColumnDef,
type FilterField,
} from "@flycommerce/ui"
type Product = {
id: string
name: string
image: string
vendor: string
status: string
}
const columns: ColumnDef<Product>[] = [
{
accessorKey: "name",
header: "Product",
cell: ({ row }) => (
<MediaCell
src={row.original.image}
title={row.original.name}
description={`by ${row.original.vendor}`}
/>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ getValue }) => <StatusBadge status={String(getValue())} />,
},
]
const filterFields: FilterField[] = [
{
key: "status",
label: "Status",
type: "select",
options: [
{ value: "published", label: "Published" },
{ value: "draft", label: "Draft" },
],
},
]
export function ProductList({
products,
total,
}: {
products: Product[]
total: number
}) {
const [page, setPage] = useState(1)
const [filters, setFilters] = useState<Record<string, unknown>>({})
return (
<DataTable
columns={columns}
data={products}
getRowId={(row) => row.id}
enableRowSelection
toolbar={
<>
<SearchInput
containerClassName="w-full sm:w-72"
placeholder="Search products"
onSearch={() => setPage(1)}
/>
<div className="ms-auto">
<TableFilters
fields={filterFields}
value={filters}
onValueChange={setFilters}
/>
</div>
</>
}
subToolbar={
<ActiveFilters
fields={filterFields}
value={filters}
onValueChange={setFilters}
/>
}
pagination={{ page, pageSize: 10, total, onPageChange: setPage }}
/>
)
}DataTable also supports sorting, bulk actions, cursor pagination, tree rows (getSubRows) and drag-to-reorder (onReorder).
MediaPickerDialog is the complete "Add Media" flow: upload with progress, a searchable media library, image details and alt text. It never calls an API itself. Your app passes the items and an upload function, so it works with any storage backend.
import { useState } from "react"
import {
Button,
MediaPickerDialog,
type MediaItem,
type UploadFn,
} from "@flycommerce/ui"
const upload: UploadFn = async (file, { signal, onProgress }) => {
// Call your API here; report progress (0–100) and honour `signal` for cancel.
const stored = await myApi.upload(file, { signal, onProgress })
return {
id: stored.id,
url: stored.url,
name: file.name,
mimeType: file.type,
size: file.size,
}
}
export function ProductImages({ library }: { library: MediaItem[] }) {
const [open, setOpen] = useState(false)
const [images, setImages] = useState<MediaItem[]>([])
return (
<>
<Button onClick={() => setOpen(true)}>Add images</Button>
<MediaPickerDialog
open={open}
onOpenChange={setOpen}
multiple
max={5}
value={images}
onConfirm={setImages}
upload={upload}
items={library}
/>
</>
)
}
declare const myApi: {
upload(
file: File,
options: { signal: AbortSignal; onProgress: (percent: number) => void }
): Promise<{ id: string; url: string }>
}All colours, radii, shadows and type sizes are CSS variables defined once in the theme, with light and dark values. Use them through Tailwind utilities, never raw hex values:
| Purpose | Utilities |
|---|---|
| Surfaces | bg-page, bg-background, bg-card, bg-card-header, bg-muted |
| Text | text-foreground, text-foreground-secondary, text-muted-foreground |
| Brand and status | bg-primary (fills), text-primary-ink (blue text and links), bg-primary-subtle, text-success-strong, text-warning-strong, text-destructive |
| Lines | border-border, border-border-subtle |
| Shape and depth | rounded-control, rounded-card, shadow-1, shadow-card, shadow-2, shadow-pop |
| Typography | type-page-title, type-card-title, type-body, type-field-label, type-hint, type-table-header … |
The library leaves Tailwind's own radius and shadow scales untouched, so installing it never changes existing parts of your app.
Use the semantic icon registry rather than importing icon glyphs directly. The name describes the meaning, so a glyph can change in one place:
import { Button, Icon } from "@flycommerce/ui"
export function AddOrder() {
return (
<Button>
<Icon name="orders" /> New order
</Button>
)
}The gallery's Icons page lists all 156 names.
To draw the FlyCommerce P3 Figma icon set, wrap the app (or one area of it) in P3IconProvider. Every name P3_GLYPHS lists then draws its P3 glyph, and the other names stay as they are:
import { P3IconProvider } from "@flycommerce/ui"
export function Root() {
return (
<P3IconProvider>
<App />
</P3IconProvider>
)
}P3IconProvider takes no props, so a Next.js server layout can render it. To swap in glyphs of your own, wrap an area in IconProvider with glyphs, a map from name to a react-icons component; inside P3IconProvider it wins over the P3 set. A server component can't pass components to a client one, so in Next.js build the map in a "use client" file:
"use client"
import { IconProvider } from "@flycommerce/ui"
import { FiShoppingBag } from "react-icons/fi"
const GLYPHS = { orders: FiShoppingBag }
export function AppIcons({ children }: { children: React.ReactNode }) {
return <IconProvider glyphs={GLYPHS}>{children}</IconProvider>
}A glyph that IconProvider swaps in keeps its own stroke width, such as 0.8 for the P3 bank glyph, unless the Icon passes strokeWidth.
In right-to-left layouts, the directional names mirror themselves: back, forward, prev, next, redirects, impersonate, signOut, sidebarToggle and list (exported as MIRRORED_ICONS). An icon mirrors under <DirectionProvider dir="rtl">, or inside an element with dir="rtl", and in server-rendered markup too. Don't flip these names yourself: rtl:rotate-180 on the icon, or a wrapper that flips, turns them back, and rtl:-scale-x-100 is redundant. Pass mirror={false} to keep one as drawn, for example a chevron you rotate yourself. Every other name, such as external, refresh or play, never mirrors.
<Button variant="link">
<Icon name="back" /> All posts
</Button>- Built on Radix primitives: correct roles, focus management and keyboard support.
- Focus rings appear for keyboard users only.
- Icon-only buttons take an
aria-label. Inputs pair with aFieldLabel. - Every component that renders text accepts label props (
labels,placeholder,confirmLabel, …). Pass your translations; the English defaults are for prototyping. - Layouts use logical properties, so they mirror correctly in RTL.
This repository is also a Claude Code plugin marketplace. The flycommerce-ui plugin teaches Claude which component to use, the design tokens, icon names, screen recipes and the migration path from older UI kits:
/plugin marketplace add getdokan/flycommerce-ui
/plugin install flycommerce-ui@flycommerce-ui
See plugin/README.md to enable it for a whole repository.
The package follows semantic versioning. While the version is 0.x, a minor release may include breaking changes; each one is listed in the changelog.
Please report vulnerabilities privately; see SECURITY.md.
See CONTRIBUTING.md for local development, the component rules and the release process.
MIT. The full-colour card network marks are Apache-2.0; see THIRD_PARTY_NOTICES.md.