Personal component library built with Next.js + React + shadcn/ui. Components are designed to be copied into other projects (shadcn-style copy-paste model — no package to install, no version lock-in).
Stack: Next.js 16 · React 19 · TypeScript · Tailwind CSS v4 · shadcn/ui (New York, neutral) · Radix UI · Lucide React
This section is written for AI coding agents. Follow this protocol to copy or update a component with no wasted steps.
Each component section contains everything needed. Before touching any files, read the entry for the component you want:
- Files to copy — exact paths relative to this repo root
- shadcn dependencies — run these in the target project before pasting files
- npm dependencies — install these in the target project
- Internal dependencies — files outside the component folder (e.g.
lib/) that must also be copied - Type augmentations —
.d.tsfiles that extend third-party modules; copy alongside the component files
Run shadcn and npm installs in the target project before copying files. If the target already has shadcn set up, npx shadcn@latest add is safe to re-run — it skips already-present primitives.
1. npx shadcn@latest add <primitives> # generates shadcn primitives
2. npm install <packages> # installs npm deps
3. Copy files from this repo # paste component files
If Atlaskit DnD packages are listed, always use
--legacy-peer-deps.
Preserve the directory structure. A component at components/ui/data-table/DataTable.tsx here should land at the same path in the target. The barrel index.ts must be included — imports reference it.
Any file listed under Internal dependencies that is not lib/utils.ts must also be copied. lib/utils.ts is assumed present in every shadcn project and can be skipped.
Files listed under Type augmentations (e.g. TanstackTable.d.ts) extend third-party module types. Copy them into the same path in the target project. They must be included in tsconfig.json's include glob (the default "**/*.d.ts" covers it).
After copying, confirm:
- No missing import errors (check
@/components/ui/<name>resolves) - No missing peer dependency warnings at runtime
- The component renders correctly against the Usage example in the README entry
When pulling in a newer version of a component from this repo:
- Diff the component files — look for new or removed props, changed types, new internal deps
- Re-check shadcn dependencies and npm dependencies for any additions
- Update call sites in the target project if props changed (the Props table in the README entry is the source of truth)
- Update any type augmentation files if the
.d.tschanged
Each component below lists everything you need to bring it into another project:
- Copy the listed files into the same paths in your project
- Run the
shadcncommand to install any required primitives - Run
npm installfor any additional packages - Copy any listed internal dependencies (shared utilities)
- Add any listed type augmentation files
Your target project should already have a shadcn/ui setup with lib/utils.ts (the cn() utility).
When adding or updating a component, copy this scaffold and fill it in:
### Component Name
Brief description of what the component does and its main features (2–3 sentences).
**Demo:** `localhost:3000/<route>`
#### Files to copy
\```
components/ui/<name>/ComponentName.tsx
components/ui/<name>/index.ts
\```
#### shadcn dependencies
\```bash
npx shadcn@latest add button input # ...
\```
#### npm dependencies
\```bash
npm install package-a package-b
\```
#### Internal dependencies
| File | Purpose |
| ------------------ | --------------------------- |
| `lib/some-util.ts` | Description of what it does |
#### Type augmentations
| File | What it augments |
| -------------- | ------------------------------------------------ |
| `SomeLib.d.ts` | Extends `some-package` module with custom fields |
#### Usage
\```tsx
import { ComponentName } from "@/components/ui/<name>"
export default function Page() {
return <ComponentName prop="value" />
}
\```
#### Notes
- Any special flags, caveats, or known issues.A Tremor-style horizontal progress bar with semantic color variants (default, neutral, success, warning, error), optional animation, and an optional label. Adapted from Tremor — no external dependencies beyond cn().
Demo: localhost:3000/charts/progress-bar
components/charts/progress-bar/ProgressBar.tsx
components/charts/progress-bar/index.ts
None.
None.
| File | Purpose |
|---|---|
lib/utils.ts |
cn() class utility |
import { ProgressBar } from "@/components/ui/progress-bar"
<ProgressBar value={60} variant="default" label="60%" />
<ProgressBar value={3} max={5} showAnimation label="3/5" />| Prop | Type | Default | Description |
|---|---|---|---|
value |
number |
0 |
Current progress value. |
max |
number |
100 |
Upper boundary value. |
variant |
"default" | "neutral" | "success" | "warning" | "error" |
"default" |
Color scheme of the bar. |
showAnimation |
boolean |
false |
Enables a CSS transition when the value changes. |
label |
string |
— | Optional text displayed to the right of the bar. |
className |
string |
— | Extra classes applied to the outer wrapper div. |
- Inherits all
HTMLDivElementprops (forwarded to the outer wrapper). - Value is clamped between
0andmaxinternally — no need to guard at call sites.
A fully-featured data table with sorting, filtering, pagination, row selection, column management, and CSV export. Built on TanStack Table with drag-and-drop column reordering powered by Atlaskit Pragmatic DnD.
Demo: localhost:3000/data-table
components/ui/data-table/DataTable.tsx
components/ui/data-table/DataTableBulkEditor.tsx
components/ui/data-table/DataTableColumnHeader.tsx
components/ui/data-table/DataTableFilter.tsx
components/ui/data-table/DataTableFilterbar.tsx
components/ui/data-table/DataTableLocaleContext.tsx
components/ui/data-table/DataTablePagination.tsx
components/ui/data-table/DataTableRowActions.tsx
components/ui/data-table/DataTableViewOptions.tsx
components/ui/data-table/TanstackTable.d.ts
components/ui/data-table/columnBuilder.tsx
components/ui/data-table/i18n.ts
components/ui/data-table/types.ts
components/ui/data-table/index.ts
lib/exportTableToCSV.ts
lib/exportTableToXLSX.ts
npx shadcn@latest add button checkbox dropdown-menu input label popover select tablenpm install @tanstack/react-table papaparse xlsx use-debounce tiny-invariantnpm install --legacy-peer-deps @atlaskit/pragmatic-drag-and-drop @atlaskit/pragmatic-drag-and-drop-flourish @atlaskit/pragmatic-drag-and-drop-hitbox @atlaskit/pragmatic-drag-and-drop-live-region @atlaskit/pragmatic-drag-and-drop-react-drop-indicator| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already present in any shadcn project |
lib/exportTableToCSV.ts |
CSV export helper using PapaParse |
lib/exportTableToXLSX.ts |
XLSX export helper using SheetJS |
| File | What it augments |
|---|---|
TanstackTable.d.ts |
Extends @tanstack/react-table ColumnMeta with className?: string and displayName: string |
// columns.tsx — generic factory for static utility columns (select checkbox + row actions)
import { DataTableRowActions } from "@/components/ui/data-table";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
export function createColumns<TData>(): ColumnDef<TData>[] {
const helper = createColumnHelper<TData>();
return [
helper.display({ id: "select" /* checkbox header/cell */ }),
helper.display({
id: "edit",
cell: ({ row }) => <DataTableRowActions row={row} />,
}),
];
}
// page.tsx — simple pattern (no custom cell/header)
// Use InferRowType to derive the row type automatically from the metadata
import { DataTable, ColumnMetadata, InferRowType } from "@/components/ui/data-table";
import { createColumns } from "./columns";
import { data } from "./data";
const columnsMetadata = [
{
columnId: "name",
title: "Name",
type: "text",
sortable: true,
filters: { text: true },
},
{
columnId: "amount",
title: "Amount",
type: "number",
sortable: true,
aligned: "right",
filters: { number: true },
formatter: (value) => `$${value}`,
filterValueFormatter: (value) => `$${value}`,
},
] as const satisfies ColumnMetadata[];
type Row = InferRowType<typeof columnsMetadata>;
export default function Page() {
return (
<DataTable<Row>
columns={createColumns<Row>()}
columnsMetadata={columnsMetadata}
data={data}
tableName="fund_assets"
language="en"
/>
);
}Custom cell and header renderers
When a column needs JSX beyond what formatter covers (icons, badges, data from multiple fields), use the cell field. Similarly, header fully replaces the default DataTableColumnHeader when you need a non-standard header.
Note on typing:
InferRowTypecreates a circular reference when the metadata array referencesRowinside acell/headercallback. In this case, defineRowmanually and annotate withColumnMetadata<Row>[]instead.
import {
DataTable,
DataTableColumnHeader,
ColumnMetadata,
} from "@/components/ui/data-table";
import { CheckCircle2, AlertTriangle, User } from "lucide-react";
// Define Row manually when using cell/header overrides
type Row = {
owner: string
status: "live" | "inactive" | "archived"
pl: number
plMin: number
enquadrado: string
}
const columnsMetadata = [
{
columnId: "owner",
title: "Owner",
type: "text",
sortable: true,
filters: { text: true },
// custom header — wraps DataTableColumnHeader to keep sort button
header: ({ column }) => (
<div className="flex items-center gap-1.5">
<User className="h-3.5 w-3.5 text-muted-foreground" />
<DataTableColumnHeader column={column} title="Owner" />
</div>
),
},
{
columnId: "status",
title: "Status",
type: "text",
sortable: true,
inferOptions: true,
filters: { checkbox: true },
// custom cell — badge with conditional colour
cell: ({ row }) => {
const s = row.original.status;
const styles = { live: "bg-emerald-100 text-emerald-700", inactive: "bg-muted text-muted-foreground", archived: "bg-amber-100 text-amber-700" };
return <span className={`rounded-full px-2 py-0.5 text-xs font-medium ${styles[s]}`}>{s}</span>;
},
},
{
columnId: "pl",
title: "PL",
subtitle: "(Min)", // rendered below the title by DataTableColumnHeader
type: "number",
sortable: true,
aligned: "right",
filters: { number: true },
// custom cell — icon + value from multiple fields via row.original
cell: ({ row }) => (
<div className="flex items-center gap-1.5">
{row.original.enquadrado === "True"
? <CheckCircle2 className="h-3 w-3 text-emerald-600" />
: <AlertTriangle className="h-3 w-3 text-orange-400" />}
<span className="text-xs">{row.original.pl}</span>
<span className="text-[9px]">({row.original.plMin})</span>
</div>
),
},
] as const satisfies ColumnMetadata<Row>[];
export default function Page() {
return (
<DataTable<Row>
columns={createColumns<Row>()}
columnsMetadata={columnsMetadata}
data={data}
tableName="fund_assets"
language="pt"
/>
);
}DataTable props:
| Prop | Type | Required | Description |
|---|---|---|---|
columns |
ColumnDef<TData>[] |
Yes | Static display columns — first element is the select column, last is the row-actions column |
data |
TData[] |
Yes | Row data |
columnsMetadata |
ColumnMetadata<TData>[] |
No | Declarative descriptors that auto-build the data columns and filter controls |
persistColumnOrder |
boolean |
No | Saves column order to a cookie (data-table-column-order) and restores it on mount. Defaults to false. |
tableName |
string |
No | Base filename for CSV/XLSX exports. Produces {tableName}-{YYYY-MM-DD}.csv / .xlsx. Also used as localStorage key when persistColumnOrder is true. Defaults to "export". |
enableRowSelection |
boolean |
No | Enables row checkboxes and click-to-select. Shows an accent-colored left-border on selected rows and reveals the BulkEditor bar. Defaults to false. |
enableRowActions |
boolean |
No | Appends the per-row actions column (ellipsis menu via DataTableRowActions). Defaults to false. |
enablePagination |
boolean |
No | Shows pagination controls and slices rows into pages. Defaults to true. Set to false to render all rows at once. |
pageSize |
number |
No | Number of rows per page when pagination is enabled. Defaults to 20. |
paginationDisplayTop |
boolean |
No | When true, renders the pagination row between the filterbar and the table instead of below it. Defaults to false. |
language |
DataTableLanguage |
No | UI language for built-in labels. "en" (default) or "pt". Import DataTableLanguage from @/components/ui/data-table. |
enableTextSelection |
boolean |
No | Allows users to select and copy text from table cells. Defaults to true. Set to false to disable selection (e.g. when click interactions conflict). |
enableFullscreen |
boolean |
No | Adds a fullscreen toggle button to the toolbar (right of the View button). Opens the table in a fixed overlay covering the entire viewport via a React portal. Press Escape or click the button again to exit. Defaults to false. |
enableDownload |
boolean |
No | Shows or hides the Export dropdown button (CSV / XLSX) in the toolbar. Defaults to true. |
enableColumnOptions |
boolean |
No | Shows or hides the View (column visibility/reorder) button in the toolbar. Defaults to true. |
toolbarIconsOnly |
boolean |
No | When true, toolbar buttons (Export, View, Fullscreen) render only their icon without a text label. Defaults to false. |
accentColor |
string |
No | Accent color for active filter button backgrounds, filter value labels, the row-selection indicator bar, and the clear-filters button. Accepts a Tailwind color token ("blue-600") or any CSS color value ("#3b82f6"). Defaults to zinc-800. |
onRowAction |
{ onAdd?, onEdit?, onDelete? } |
No | Callbacks for the per-row action dropdown (requires enableRowActions). Each receives row.original as TData. Only items with a provided callback are rendered in the menu. |
onBulkAction |
{ onEdit?, onDelete? } |
No | Callbacks for the bulk editor toolbar (requires enableRowSelection). Each receives TData[] for all selected rows. Commands are disabled when no callback is provided. |
ColumnMetadata fields:
| Field | Type | Description |
|---|---|---|
columnId |
keyof TData & string |
Accessor key matching the data property |
title |
string |
Column header label |
subtitle |
string |
Optional secondary label shown below the title in the header |
description |
string |
Tooltip text shown on hover of an info icon in the header |
type |
"text" | "number" |
Data type — controls available filter types |
sortable |
boolean |
Enable column sorting. Default false |
hideable |
boolean |
Show in view options toggle. Default true |
options |
OptionItem[] |
Options array for select / checkbox / checkboxSearch filters |
filters.text |
boolean |
Debounced text search input |
filters.textColumns |
string[] |
Additional column IDs searched alongside the primary text column. Placeholder auto-updates to list all column titles. |
filters.select |
boolean |
Single-value dropdown filter |
filters.checkbox |
boolean |
Multi-value checkbox filter (arrIncludesSome) |
filters.checkboxSearch |
boolean | { multiple?: boolean } |
Checkbox list with search box. multiple: false for single-select (radio-style). Default: true |
filters.number |
boolean |
Condition + value filter (only for type: "number") |
aligned |
"left" | "center" | "right" |
Cell text alignment |
formatter |
(value: unknown) => ReactNode |
Simple cell renderer — receives the column's value only |
filterValueFormatter |
(value: number | string) => string |
Formats values displayed inside the number filter popover |
cell |
(props: CellContext<TData, unknown>) => ReactNode |
Full cell renderer override — receives the complete TanStack CellContext (use row.original to access other fields). Takes precedence over formatter. |
header |
(props: HeaderContext<TData, unknown>) => ReactNode |
Full header renderer override — replaces the default DataTableColumnHeader. Wrap DataTableColumnHeader inside it to keep the sort button. |
Features:
- Metadata-driven column and filter construction — add a column by adding one object to
columnsMetadata cellandheaderoverrides on anyColumnMetadataentry — full TanStackCellContext/HeaderContextaccess, includingrow.originalfor multi-field cellsInferRowType<typeof columnsMetadata>utility — derive a fully-typed row type automatically from aconstmetadata array; whencell/headeroverrides are used, defineRowmanually and annotate withColumnMetadata<Row>[]instead- Filter types: single select, multi-checkbox, numeric condition (built-in conditions: is equal to, is between, is greater than, is less than), debounced text search
- Row selection with emerald left-border indicator
- Bulk action command bar (keyboard shortcuts:
eedit ·ddelete ·Escapeclear) - Column visibility toggle + drag-and-drop reordering with accessibility live region announcements
- Persistent column order via cookie (
persistColumnOrderprop) - Pagination (20 rows/page) with responsive first/last buttons
- CSV and XLSX export via the Export dropdown (visible, filtered rows only) — filename controlled via
tableNameprop - Fullscreen overlay mode via React portal (ESC to dismiss)
- Atlaskit DnD packages require
--legacy-peer-depsdue to a React 19 peer dependency conflict
A grid-based status heatmap that renders rows × dates cells, each colored by a status key. Useful for operational dashboards (machine status, service health, etc.). Fully driven by props — pass data, a label config, and display options.
Demo: localhost:3000/status-map
components/ui/status-map/StatusHeatmap.tsx
components/ui/status-map/types.ts
components/ui/status-map/index.ts
None required.
npm install date-fns| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already in any shadcn project |
None.
import { StatusMap } from "@/components/ui/status-map"
import type { StatusMapEntry } from "@/components/ui/status-map"
const data: StatusMapEntry[] = [
{ row: "Machine A", date: "2025-03-01", status: "green" },
{ row: "Machine A", date: "2025-03-02", status: "red" },
{ row: "Machine B", date: "2025-03-01", status: "orange" },
]
export default function Page() {
return (
<StatusMap
data={data}
labelConfig={{
green: { color: "bg-emerald-500", label: "Operational" },
orange: { color: "bg-orange-400", label: "Warning" },
red: { color: "bg-red-500", label: "Fault" },
grey: { color: "bg-muted", label: "No data" },
}}
label
labelAlign="right"
labelTop={false}
style="rounded"
onCellClick={(row, date, status) => console.log(row, date, status)}
/>
)
}Props:
| Prop | Type | Default | Description |
|---|---|---|---|
data |
StatusMapEntry[] |
— | Array of { row, date, status }. Missing row/date combinations fall back to the last key in labelConfig. |
labelConfig |
Record<string, StatusItemConfig> |
— | Maps status keys to { color: string, label: string }. color is a Tailwind bg class (e.g. "bg-emerald-500"). |
style |
"rounded" | "squared" | "tight" |
"rounded" |
Cell shape. tight collapses padding for dense grids. |
bordered |
boolean |
true |
Wraps the grid in a rounded border. |
label |
boolean |
false |
Shows a color legend. |
labelAlign |
"left" | "center" | "right" |
"left" |
Horizontal alignment of the legend. |
labelTop |
boolean |
false |
Renders the legend above the grid instead of below. |
className |
string |
— | Additional class names on the root element. |
onCellClick |
(row, date, status) => void |
— | Callback fired when a cell is clicked. |
StatusMapEntry fields:
| Field | Type | Description |
|---|---|---|
row |
string |
Row label (e.g. machine name) |
date |
string |
ISO "YYYY-MM-DD" date |
status |
string |
Key into labelConfig |
Features:
- Derives unique rows and dates automatically from
data— no manual axis config - Missing cells fall back to the last key in
labelConfig(intended as a "no data" state) - Legend shows count of cells per status in parentheses
- Horizontally scrollable for wide date ranges
onCellClickfor interactive dashboards
- Rows appear in insertion order from
data; dates are sorted ascending - The
datefield must be"YYYY-MM-DD"; the component appendsT00:00:00to avoid timezone off-by-one errors
A small label component for displaying status, categories, or counts. Inspired by Tremor's Badge. Supports five semantic color variants (default, neutral, success, warning, error) with automatic light and dark mode styling.
Demo: localhost:3000/ui/badge
components/ui/badge.tsx
None required.
npm install class-variance-authority| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already in any shadcn project |
None.
import { Badge } from "@/components/ui/badge"
<Badge variant="default">Default</Badge>
<Badge variant="neutral">Neutral</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="error">Error</Badge>Props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "neutral" | "success" | "error" | "warning" |
"default" |
Color scheme of the badge |
className |
string |
— | Additional CSS classes |
children |
React.ReactNode |
— | Badge content — text and/or Lucide icons |
Features:
- Five semantic variants:
default(blue),neutral(gray),success(emerald),warning(yellow),error(red) - Icon support — place any
lucide-reacticon as a direct child, auto-sized to 12 px - Full light and dark mode support via Tailwind color classes
- Spreads all native
spanprops for aria attributes, event handlers, etc. - Exports
badgeVariantsCVA function for reuse on other elements
- Replaces the default shadcn
badge.tsx— the variant names differ from shadcn's (default/secondary/destructive/outline) cvais already installed via shadcn'sclass-variance-authoritydependency
Accessible tabbed navigation built on Radix UI Tabs primitives. Supports line (underline indicator) and solid (pill with background) variants, icon support inside triggers, disabled states, and full keyboard navigation.
Demo: localhost:3000/tabs
components/ui/tabs.tsx
None required.
None — uses radix-ui (unified package), which is a peer dependency of any shadcn/ui setup.
| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already in any shadcn project |
None.
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
<Tabs defaultValue="overview">
<TabsList variant="line">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing" disabled>Billing</TabsTrigger>
</TabsList>
<TabsContent value="overview">Overview content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
<TabsContent value="billing">Billing content</TabsContent>
</Tabs>Props:
Tabs (root):
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue |
string |
— | Initially active tab value (uncontrolled) |
value |
string |
— | Controlled active tab value |
onValueChange |
(value: string) => void |
— | Callback fired when the active tab changes |
className |
string |
— | Additional CSS classes on the root element |
TabsList:
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"line" | "solid" |
"line" |
"line" uses a border-bottom underline; "solid" uses a pill container with background |
className |
string |
— | Additional CSS classes |
TabsTrigger:
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string |
— | Identifier for this tab (required) |
disabled |
boolean |
false |
Disables interaction with the trigger |
className |
string |
— | Additional CSS classes |
TabsContent:
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string |
— | Must match the corresponding TabsTrigger value (required) |
className |
string |
— | Additional CSS classes |
Features:
- Two visual variants:
line(underline indicator) andsolid(pill/card active state) - Icon support — place any
lucide-reacticon as a child ofTabsTrigger - Disabled triggers via the
disabledprop - Full keyboard navigation (arrow keys, Home/End) inherited from Radix UI
- Variant context passed automatically from
TabsListtoTabsTrigger— no extra prop threading needed - All sub-components accept
classNamefor full styling override
- Import as named exports:
Tabs,TabsList,TabsTrigger,TabsContent radix-uimust be installed (npm install radix-ui)
An in-page documentation block rendered at the bottom of demo pages. Shows a usage code snippet with a copy button and a props table. Follows the component's own props-driven API standard.
Demo: Visible at the bottom of every component demo page (e.g. localhost:3000/data-table, localhost:3000/status-map).
components/ui/component-doc/ComponentDoc.tsx
components/ui/component-doc/index.ts
None required.
None.
| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already in any shadcn project |
None.
import { ComponentDoc } from "@/components/ui/component-doc"
// Single flat props list:
<ComponentDoc
title="MyComponent"
description="Short description of what it does."
usage={`import { MyComponent } from "@/components/ui/my-component"
<MyComponent requiredProp="value" optionalProp />`}
props={[
{ name: "requiredProp", type: "string", required: true, description: "..." },
{ name: "optionalProp", type: "boolean", default: "false", description: "..." },
]}
/>
// Multiple named prop sections (e.g. component props + sub-type fields):
<ComponentDoc
title="MyComponent"
usage={`...`}
propSections={[
{
title: "MyComponent props",
props: [{ name: "data", type: "Row[]", required: true, description: "..." }],
},
{
title: "Row fields",
props: [{ name: "id", type: "string", required: true, description: "..." }],
},
]}
/>Props:
| Prop | Type | Description |
|---|---|---|
title |
string |
Component name shown as the section heading. |
description |
string |
Optional subtitle below the heading. |
usage |
string |
Raw code string rendered in a syntax-highlighted block with a copy button. |
props |
PropDef[] |
Flat list of prop definitions. Renders under a single "Props" heading. |
propSections |
PropSection[] |
Multiple named prop groups, each with its own sub-heading. Use instead of props when there are multiple distinct types to document. |
className |
string |
Additional class names on the root <section>. |
PropDef fields:
| Field | Type | Description |
|---|---|---|
name |
string |
Prop name (rendered in monospace) |
type |
string |
Type string (rendered in a muted code badge) |
default |
string |
Default value, if any |
required |
boolean |
Adds a red * next to the prop name |
description |
string |
Human-readable description |
- Place
<ComponentDoc>at the bottom of every demo page, after the live component demo - Update it whenever props are added, removed, or changed
A Tremor-inspired responsive area chart built on Recharts. Supports single and multi-series data, gradient/solid/no-fill modes, stacked and percentage layouts, an interactive legend with optional horizontal slider, custom tooltips, and click events on dots and legend categories.
Demo: localhost:3000/charts/area-chart
components/charts/area-chart/AreaChart.tsx
components/charts/area-chart/index.ts
components/charts/utils/chartColors.ts
components/charts/utils/chartHelpers.ts
components/charts/utils/useOnWindowResize.ts
None.
npm install recharts| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already present in any shadcn project |
components/charts/utils/chartColors.ts |
Color palette, getColorClass(), constructCategoryColors() |
components/charts/utils/chartHelpers.ts |
getYAxisDomain(), hasOnlyOneValueForKey() |
components/charts/utils/useOnWindowResize.ts |
Window resize hook used to recalculate legend height |
None.
import { AreaChart } from "@/components/charts/area-chart"
// Single series with value formatter
<AreaChart
data={revenueData}
index="month"
categories={["Revenue"]}
valueFormatter={(v) => `$${v.toLocaleString()}`}
/>
// Multi-series stacked with solid fill
<AreaChart
data={trafficData}
index="month"
categories={["Organic", "Direct", "Referral"]}
type="stacked"
fill="solid"
/>
// Interactive with event callback
<AreaChart
data={trafficData}
index="month"
categories={["Organic", "Direct", "Referral"]}
onValueChange={(event) => console.log(event)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
data |
Record<string, any>[] |
— | Required. Array of data objects with the index key and one key per category. |
index |
string |
— | Required. Key used as the X-axis label (e.g. "month"). |
categories |
string[] |
— | Required. Keys from data objects to render as area series. |
colors |
ChartColor[] |
CHART_COLORS |
Color names for each category, in order. |
valueFormatter |
(value: number) => string |
v => v.toString() |
Formats Y-axis ticks and tooltip values. |
type |
"default" | "stacked" | "percent" |
"default" |
Layout mode — default overlapping, stacked cumulative, or normalized to 100%. |
fill |
"gradient" | "solid" | "none" |
"gradient" |
Fill style for the area under each line. |
axisTextSize |
"xs" | "sm" | "md" | "lg" | number |
"xs" |
Font size for axis tick labels. Named sizes: xs=12, sm=14, md=16, lg=18px. |
legendTextSize |
"xs" | "sm" | "md" | "lg" | number |
"xs" |
Font size for legend item labels. |
dataPointTextSize |
"xs" | "sm" | "md" | "lg" | number |
"xs" |
Font size for data point labels (requires showDataPointLabels). |
showDataPointLabels |
boolean |
false |
When true, renders the formatted value as a label above each data point. |
showDataPointLabelBackground |
boolean |
false |
When true, each label gets a rounded background tinted with the series color. Requires showDataPointLabels. |
dataPointLabelFormatter |
(value: number) => string |
— | Custom formatter for data point labels. Falls back to valueFormatter when omitted. Useful for compact notation (e.g. 1500000 → "1.5M"). |
showXAxis |
boolean |
true |
Show the X-axis with tick labels. |
showYAxis |
boolean |
true |
Show the Y-axis with tick labels. |
showGridLines |
boolean |
true |
Show horizontal grid lines. |
showLegend |
boolean |
true |
Show the legend above the chart. |
showTooltip |
boolean |
true |
Show the tooltip on hover. |
legendPosition |
"left" | "center" | "right" |
"right" |
Horizontal alignment of the legend. |
enableLegendSlider |
boolean |
false |
Make the legend horizontally scrollable with arrow buttons. |
yAxisWidth |
number |
auto | Width in pixels reserved for the Y-axis. Defaults to auto-inferred from the largest formatted value in the dataset. |
autoMinValue |
boolean |
false |
Set Y-axis minimum to "auto" instead of 0. |
minValue |
number |
— | Explicit Y-axis domain minimum. |
maxValue |
number |
— | Explicit Y-axis domain maximum. |
allowDecimals |
boolean |
true |
Allow decimal Y-axis tick values. |
startEndOnly |
boolean |
false |
Show only the first and last X-axis tick labels. |
intervalType |
"preserveStartEnd" | "equidistantPreserveStart" |
"equidistantPreserveStart" |
Recharts X-axis tick interval strategy. |
tickGap |
number |
5 |
Minimum gap in pixels between X-axis tick labels. |
connectNulls |
boolean |
false |
Bridge null data points instead of creating gaps. |
xAxisLabel |
string |
— | Optional label rendered below the X-axis. |
yAxisLabel |
string |
— | Optional label rendered to the left of the Y-axis (rotated). |
onValueChange |
(value: AreaChartEventProps) => void |
— | Fired when a dot or legend item is clicked; null on deselect. |
tooltipCallback |
(content: TooltipProps) => void |
— | Side-effect callback when tooltip active state or label changes. |
customTooltip |
React.ComponentType<TooltipProps> |
— | Custom component rendered in place of the default tooltip. |
className |
string |
— | Additional classes on the outer wrapper (default height h-80). |
- The
ChartColortype andCHART_COLORSconstant are exported from the barrel for external use. - The three utility files in
components/charts/utils/are shared across chart components — copy them once and reuse. - Recharts requires a fixed height on the container; override with
className="h-96"or similar.
A Tremor-inspired responsive bar chart built on Recharts. Supports vertical and horizontal bar orientations, multi-category side-by-side and stacked layouts, percent-normalized stacking, an interactive legend with optional slider, custom tooltips, and click events on bars and legend categories.
Demo: localhost:3000/charts/bar-chart
components/charts/bar-chart/BarChart.tsx
components/charts/bar-chart/index.ts
components/charts/utils/chartColors.ts
components/charts/utils/chartHelpers.ts
components/charts/utils/useOnWindowResize.ts
None.
npm install recharts| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already present in any shadcn project |
components/charts/utils/chartColors.ts |
Color palette, getColorClass(), constructCategoryColors() |
components/charts/utils/chartHelpers.ts |
getYAxisDomain(), inferYAxisWidth(), measureTextWidth() |
components/charts/utils/useOnWindowResize.ts |
Window resize hook used to recalculate legend height |
None.
import { BarChart } from "@/components/charts/bar-chart"
// Basic single series
<BarChart
data={salesData}
index="month"
categories={["Sales"]}
valueFormatter={(v) => `$${v.toLocaleString()}`}
/>
// Stacked multi-category
<BarChart
data={departmentData}
index="month"
categories={["Engineering", "Marketing", "Support"]}
type="stacked"
/>
// Vertical (horizontal bars) with interactive events
<BarChart
data={regionData}
index="region"
categories={["Revenue"]}
layout="vertical"
valueFormatter={(v) => `$${v.toLocaleString()}`}
onValueChange={(event) => console.log(event)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
data |
Record<string, any>[] |
— | Required. Array of data objects with the index key and one key per category. |
index |
string |
— | Required. Key used as the category-axis label (e.g. "month", "region"). |
categories |
string[] |
— | Required. Keys from data objects to render as bar series. |
colors |
ChartColor[] |
CHART_COLORS |
Color names for each category, in order. |
valueFormatter |
(value: number) => string |
v => v.toString() |
Formats numeric axis ticks and tooltip values. |
layout |
"vertical" | "horizontal" |
"horizontal" |
Orientation — "horizontal" renders vertical bars, "vertical" renders horizontal bars. |
type |
"default" | "stacked" | "percent" |
"default" |
Bar display mode — side-by-side, stacked cumulative, or normalized to 100%. |
barCategoryGap |
string | number |
— | Gap between bar groups. Accepts a percentage string (e.g. "20%") or pixel value. |
axisTextSize |
"xs" | "sm" | "md" | "lg" | number |
"xs" |
Font size for axis tick labels. Named sizes: xs=12, sm=14, md=16, lg=18px. |
autoScaleLabels |
boolean |
false |
Automatically shrinks axis tick font size as entry count grows, preventing label overlap on large datasets without scroll. |
legendTextSize |
"xs" | "sm" | "md" | "lg" | number |
"xs" |
Font size for legend item labels. |
showXAxis |
boolean |
true |
Show the X-axis with tick labels. |
showYAxis |
boolean |
true |
Show the Y-axis with tick labels. |
showGridLines |
boolean |
true |
Show grid lines (horizontal for default layout, vertical for layout="vertical"). |
showLegend |
boolean |
true |
Show the legend above the chart. |
showTooltip |
boolean |
true |
Show the tooltip on hover. |
legendPosition |
"left" | "center" | "right" |
"right" |
Horizontal alignment of the legend. |
enableLegendSlider |
boolean |
false |
Make the legend horizontally scrollable with arrow buttons. |
yAxisWidth |
number |
auto | Width in pixels reserved for the Y-axis. Defaults to auto-inferred from labels. |
autoMinValue |
boolean |
false |
Set numeric axis minimum to "auto" instead of 0. |
minValue |
number |
— | Explicit numeric axis domain minimum. |
maxValue |
number |
— | Explicit numeric axis domain maximum. |
allowDecimals |
boolean |
true |
Allow decimal tick values on the numeric axis. |
startEndOnly |
boolean |
false |
Show only the first and last tick labels on the category axis. |
intervalType |
"preserveStartEnd" | "equidistantPreserveStart" |
"equidistantPreserveStart" |
Recharts tick interval strategy for the category axis. |
tickGap |
number |
5 |
Minimum gap in pixels between category-axis tick labels. |
xAxisLabel |
string |
— | Optional label rendered below the X-axis. |
yAxisLabel |
string |
— | Optional label rendered to the left of the Y-axis (rotated). |
onValueChange |
(value: BarChartEventProps) => void |
— | Fired when a bar or legend item is clicked; null on deselect. eventType is "bar" or "category". |
tooltipCallback |
(content: TooltipProps) => void |
— | Side-effect callback when tooltip active state or label changes. |
customTooltip |
React.ComponentType<TooltipProps> |
— | Custom component rendered in place of the default tooltip. |
className |
string |
— | Additional classes on the outer wrapper (default height h-80). |
- The
ChartColortype andCHART_COLORSconstant are exported from the barrel for external use. - The three utility files in
components/charts/utils/are shared withAreaChart— copy them once and reuse. - For
type="percent", pass a percent formatter:valueFormatter={(v) => \${(v * 100).toFixed(0)}%`}`. - Recharts requires a fixed height on the container; override with
className="h-96"or similar.
A Tremor-inspired donut and pie chart for visualizing part-to-whole relationships. Supports an optional center label (auto-total or custom text), click interactions with active segment highlighting, custom tooltip, and tooltip callbacks. Powered by Recharts.
Demo: localhost:3000/charts/donut-chart
components/charts/donut-chart/DonutChart.tsx
components/charts/donut-chart/index.ts
components/charts/utils/chartColors.ts
None.
npm install recharts| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already present in any shadcn project |
components/charts/utils/chartColors.ts |
Color palette, getColorClass(), constructCategoryColors() |
None.
import { DonutChart } from "@/components/charts/donut-chart"
// Basic donut
<DonutChart
data={data}
category="browser"
value="share"
valueFormatter={(v) => `${v}%`}
/>
// With center label (shows sum of all values)
<DonutChart
data={data}
category="browser"
value="share"
showLabel
valueFormatter={(v) => `${v}%`}
/>
// Pie variant
<DonutChart
data={data}
category="region"
value="revenue"
variant="pie"
valueFormatter={(v) => `$${v.toLocaleString()}`}
/>
// Interactive
<DonutChart
data={data}
category="browser"
value="share"
onValueChange={(event) => console.log(event)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
data |
Record<string, any>[] |
— | Required. Array of data objects with keys for category labels and numeric values. |
category |
string |
— | Required. Key in each data object used as the segment label (e.g. "browser", "region"). |
value |
string |
— | Required. Key in each data object used as the segment's numeric value. |
colors |
ChartColor[] |
CHART_COLORS |
Color palette for each segment. Cycles if fewer colors than segments. |
variant |
"donut" | "pie" |
"donut" |
Shape variant — "donut" renders a ring, "pie" fills the center. |
valueFormatter |
(value: number) => string |
v => v.toString() |
Formats tooltip values and the center label total. |
label |
string |
— | Custom text shown in the center (donut only). When omitted, the sum of all values is shown. |
showLabel |
boolean |
false |
Show a label in the center of the donut. Has no effect on the pie variant. |
showTooltip |
boolean |
true |
Show or hide the tooltip on hover. |
onValueChange |
(value: DonutChartEventProps) => void |
— | Fired when a segment is clicked. Returns { eventType: "sector", categoryClicked, ...dataRow } or null on deselect. |
tooltipCallback |
(content: TooltipProps) => void |
— | Side-effect callback fired when tooltip active state or hovered category changes. |
customTooltip |
React.ComponentType<TooltipProps> |
— | Custom component rendered in place of the default tooltip. Receives active and payload. |
className |
string |
— | Additional classes on the outer wrapper. The chart has min-h-40 and aspect-square by default — it fills the parent height while staying circular. |
- Default size is
h-40 w-40— override withclassName="h-48 w-48"or any size. - Center label is only rendered when
variant="donut"andshowLabel={true}. - Clicking an active segment deselects it and fires
onValueChange(null). - Only
chartColors.tsis required from the utils directory (no axis helpers needed).
A fundamental layout primitive for grouping content — KPI cards, forms, or sections. Adapts Tremor's Card design using project CSS variable tokens. Supports asChild for rendering as any semantic element.
Demo: localhost:3000/ui/card
components/ui/card/Card.tsx
components/ui/card/index.ts
None.
None (radix-ui unified package already required by other components).
| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility |
None.
import { Card } from "@/components/ui/card"
// Basic
<Card className="max-w-sm">
<p className="text-sm text-muted-foreground">Card content</p>
</Card>
// Semantic HTML via asChild
<ul>
<Card asChild>
<li>List item styled as a card</li>
</Card>
</ul>| Prop | Type | Default | Description |
|---|---|---|---|
enableFullscreen |
boolean |
false |
When true, shows a fullscreen toggle button in the top-right corner. Expands via portal; Escape to exit. |
hoverShadow |
boolean |
false |
When true, applies a subtle shadow on hover with a smooth fade-in transition. |
asChild |
boolean |
false |
When true, merges Card props onto its first child element instead of rendering a <div>. |
className |
string |
— | Additional Tailwind classes to customize the card. |
...props |
React.HTMLAttributes<HTMLDivElement> |
— | All standard div props are forwarded. |
- Default styles:
rounded-lg border border-border bg-card p-6 shadow-xs. - Override padding with
className="p-4"or any Tailwind spacing class. asChilduses Radix UI'sSlot.Rootfrom theradix-uiunified package.
A date picker input that opens a popover calendar. Supports single date, date range, and date + time selection modes with i18n (en/pt).
Demo: localhost:3000/date-picker
components/ui/date-picker/date-picker.tsx
components/ui/date-picker/index.ts
npx shadcn@latest add button popover calendarnpm install date-fns react-day-picker| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility |
import { DatePicker } from "@/components/ui/date-picker"
// Single date
<DatePicker value={date} onChange={setDate} />
// Date range
<DatePicker mode="range" value={range} onChange={setRange} />
// Date + time
<DatePicker mode="datetime" value={dateTime} onChange={setDateTime} />
// With constraints and i18n
<DatePicker
value={date}
onChange={setDate}
minDate={new Date()}
maxDate={addDays(new Date(), 30)}
language="pt"
/>| Prop | Type | Default | Description |
|---|---|---|---|
mode |
"single" | "range" | "datetime" |
"single" |
Selection mode |
value |
Date | DateRange | undefined |
— | Selected value (type depends on mode) |
onChange |
(value) => void |
— | Called when selection changes |
displayFormat |
"long" | "short" |
"long" |
"long" = full date (March 21, 2026), "short" = dd/MM/yyyy |
enableDayNavigation |
boolean |
false |
Single mode only. Adds chevron buttons to step one day forward/backward |
placeholder |
string |
Localized default | Custom placeholder text |
disabled |
boolean |
false |
Disables the trigger |
disableWeekends |
boolean |
false |
Prevents selection of Saturdays and Sundays |
disabledDates |
string[] |
— | Array of dates in "yyyy-MM-dd" format to disable |
enableYearMonthSelect |
boolean |
false |
Shows dropdown selects for year and month in the calendar header |
language |
"en" | "pt" |
"en" |
Locale for labels and formatting |
format |
string |
Per-mode default | Custom date-fns format string |
minDate |
Date |
— | Earliest selectable date |
maxDate |
Date |
— | Latest selectable date |
triggerClassName |
string |
— | Additional classes for the trigger button |
className |
string |
— | Additional classes for the popover content |
- Three modes — single date, date range (from/to), date + time (24h)
- Auto-close — popover closes on single select, stays open for datetime
- i18n — English and Portuguese labels, month names, and date formatting
- Min/max constraints — disable dates outside a range
- Controlled — fully controlled via
value/onChange
- Uses
react-day-pickerv9 anddate-fnsv4 (both already in this repo). - Time input uses 24-hour format.
- The
DateRangetype is{ from?: Date; to?: Date }fromreact-day-picker. - The discriminated union on
modemakesvalueandonChangetype-safe per mode.
A year/month picker with popover. The user selects a year from a dropdown and a month from a 3-column button grid.
Demo: localhost:3000/month-picker
components/ui/month-picker/month-picker.tsxcomponents/ui/month-picker/index.ts
shadcn: button, popover
npm: date-fns, lucide-react
import { MonthPicker } from "@/components/ui/month-picker"
const [value, setValue] = useState<{ year: number; month: number }>()
<MonthPicker value={value} onChange={setValue} />
// With month navigation arrows
<MonthPicker value={value} onChange={setValue} enableMonthNavigation />
// Constrained range
<MonthPicker
value={value}
onChange={setValue}
minDate={{ year: 2020, month: 0 }}
maxDate={{ year: 2030, month: 11 }}
/>| Prop | Type | Default | Description |
|---|---|---|---|
value |
{ year: number; month: number } | undefined |
— | Selected value. month is 0-indexed (0 = Jan, 11 = Dec) |
onChange |
(value) => void |
— | Called when a month is selected |
enableMonthNavigation |
boolean |
false |
Adds < > buttons to step one month forward/backward |
placeholder |
string |
Localized default | Custom placeholder text |
disabled |
boolean |
false |
Disables the trigger |
language |
"en" | "pt" |
"en" |
Locale for labels and month names |
size |
"default" | "sm" |
"default" |
Trigger size variant |
shadow |
"none" | "xs" | "sm" |
"xs" |
Trigger shadow |
minDate |
{ year: number; month: number } |
— | Earliest selectable year/month (defaults to 100 years back) |
maxDate |
{ year: number; month: number } |
— | Latest selectable year/month (defaults to 10 years forward) |
triggerClassName |
string |
— | Additional classes for the trigger button |
className |
string |
— | Additional classes for the popover content |
- Year dropdown + month grid — select year from dropdown, then pick month from a 3-column button grid
- Month navigation — optional < > arrows to step month-by-month
- Constrained range —
minDate/maxDatelimit selectable months and filter the grid - i18n — English and Portuguese month names and labels
- Consistent styling — matches DatePicker trigger appearance (size, shadow, icons)
A props-driven select dropdown built on Radix UI Select. Supports groups, labels, separators, searchable filtering, custom item rendering, a "last selected" cookie-based memory feature, loading skeleton state, and i18n (en/pt).
Demo: localhost:3000/ui/select
components/ui/select/select.tsx
components/ui/select/index.ts
None.
None (radix-ui unified package and lucide-react already required by other components).
| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility |
None.
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
<Select value={value} onValueChange={setValue}>
<SelectTrigger className="w-50">
<SelectValue placeholder="Pick one" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectContent>
</Select>
{/* Loading state */}
<Select>
<SelectTrigger className="w-50" loading>
<SelectValue placeholder="Loading..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="x">Option</SelectItem>
</SelectContent>
</Select>Select (root)
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string |
— | Controlled selected value. |
defaultValue |
string |
— | Uncontrolled default value. |
onValueChange |
(value: string) => void |
— | Callback when the selected value changes. |
disabled |
boolean |
false |
Disables the entire select. |
open |
boolean |
— | Controlled open state of the dropdown. |
onOpenChange |
(open: boolean) => void |
— | Callback when the open state changes. |
renderItem |
(entry: { value: string; label: string }) => ReactNode |
— | Custom render for each SelectItem's content. |
language |
"en" | "pt" |
"en" |
UI language for built-in strings. |
selectId |
string |
— | Unique ID used as the cookie key for last-selected persistence. |
enableLastSelected |
boolean |
false |
Persists last selection in a cookie and shows a footer suggestion (requires selectId). |
renderLastSelected |
(entry: { value: string; label: string }) => ReactNode |
— | Custom render for the last-selected footer content. |
SelectTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
size |
"sm" | "default" |
"default" |
Trigger height. sm = 32 px, default = 36 px. |
loading |
boolean |
false |
Shows a pulsing skeleton bar inside the trigger and disables it. |
className |
string |
— | Additional CSS classes. |
SelectContent
| Prop | Type | Default | Description |
|---|---|---|---|
searchable |
boolean |
false |
Renders a search input to filter items. Forces popper positioning. |
searchPlaceholder |
string |
"Search..." |
Placeholder for the search input. |
position |
"item-aligned" | "popper" |
"item-aligned" |
Positioning strategy. Forced to popper when searchable. |
align |
"start" | "center" | "end" |
"center" |
Alignment relative to the trigger. |
SelectItem
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string |
— | Required. The value for this option. |
searchValue |
string |
— | Custom string to match against when filtering. |
disabled |
boolean |
false |
Disables this individual item. |
- The
loadingprop onSelectTriggerreplaces children with a pulsing skeleton bar and disables the trigger. Requires theskeleton-pulsekeyframe in your global CSS. enableLastSelectedstores a 90-day cookie keyed byselectId.- When
searchableis true, the position is forced to"popper"because item-aligned mode doesn't work well with the search input.
A multi-item select with checkboxes, search input, and chip display for selected values. Consumers render MultiSelectItem children; the component manages selection state internally and reports changes via onValueChange.
Demo: localhost:3000/multi-select
components/ui/multi-select/MultiSelect.tsx
components/ui/multi-select/index.ts
npx shadcn@latest add button command popoverNone.
| File | Purpose |
|---|---|
lib/utils.ts |
cn() class utility |
import { MultiSelect, MultiSelectItem } from "@/components/ui/multi-select"
const options = [
{ value: "a", label: "Option A" },
{ value: "b", label: "Option B" },
]
<MultiSelect
placeholder="Select..."
placeholderSearch="Search..."
onValueChange={(values) => console.log(values)}
>
{options.map((o) => (
<MultiSelectItem key={o.value} value={o.value}>
{o.label}
</MultiSelectItem>
))}
</MultiSelect>| Prop | Type | Default | Description |
|---|---|---|---|
onValueChange |
(value: string[]) => void |
— | Called with the updated selected values array on each change. |
placeholder |
string |
"Select..." |
Trigger button text when nothing is selected. |
placeholderSearch |
string |
"Search..." |
Search input placeholder inside the popover. |
disabled |
boolean |
false |
Disables the trigger and prevents the popover from opening. |
className |
string |
— | Extra classes applied to the trigger button. |
children |
React.ReactNode |
— | MultiSelectItem elements to render as options. |
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string |
— | Unique identifier returned in the onValueChange array. |
children |
React.ReactNode |
— | Label shown in the list and in the selected chip. |
className |
string |
— | Extra classes applied to the command item. |
- Selected values are displayed as removable chips inside the trigger button.
- The popover width matches the trigger width via
w-(--radix-popover-trigger-width). - Relies on
cmdk(bundled with the shadcncommandprimitive) for filtering — no additional search logic needed.
A drag-and-drop formula builder that lets users compose expressions from variable blocks and math operators. Supports controlled value, click-to-add, canvas reordering, token removal, and optional formula validation.
Demo: localhost:3000/formula-builder
components/ui/formula-builder/formula-builder.tsx
components/ui/formula-builder/types.ts
components/ui/formula-builder/index.ts
None.
npm install --legacy-peer-deps @atlaskit/pragmatic-drag-and-drop @atlaskit/pragmatic-drag-and-drop-flourish @atlaskit/pragmatic-drag-and-drop-hitbox @atlaskit/pragmatic-drag-and-drop-react-drop-indicator tiny-invariant lucide-react| File | Purpose |
|---|---|
lib/utils.ts |
cn() class utility |
import { FormulaBuilder } from "@/components/ui/formula-builder"
const variables = {
height: "Person height",
weight: "Person weight",
age: "Person age",
}
<FormulaBuilder
variables={variables}
value={formula}
onChange={setFormula}
/>| Prop | Type | Default | Description |
|---|---|---|---|
variables |
Record<string, string> |
— | Required. Dictionary of variable keys to display labels. |
value |
string |
— | Controlled formula string, space-separated. |
onChange |
(formula: string) => void |
— | Callback fired when the formula changes. |
operators |
string[] |
["+", "-", "*", "/", "(", ")"] |
Operators available in the palette. |
placeholder |
string |
"Drag variables and operators here…" |
Placeholder text for the empty canvas. |
disabled |
boolean |
false |
Disables all interaction. |
showValidation |
boolean |
false |
Shows validation errors for malformed expressions. |
className |
string |
— | Additional className on the root container. |
- Drag variables and operators from palette to canvas
- Click palette items to append (alternative to drag)
- Reorder tokens by dragging within the canvas
- Remove tokens with X button
- Visual drop indicators for insertion position
- Optional validation (consecutive operators, unbalanced parentheses)
- Controlled
value/onChangeAPI - Custom drag previews
- Instance isolation (multiple builders on one page)
- Atlaskit DnD packages require
--legacy-peer-depsdue to React 19 peer dep conflict. - The formula is stored as a space-separated string of variable keys and operators.
- Unknown tokens in the
valuestring are treated as variables with the raw key as the label.