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/DataTableStickyHeader.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. |
stickyHeader |
boolean |
No | Floats a copy of the header at the top of the viewport once the real one scrolls out of view, so long tables keep their column labels. Rendered as a fixed clone portaled to document.body (see DataTableStickyHeader), so no overflow-hidden ancestor can clip it and horizontal scroll stays mirrored. Defaults to false. |
stickyHeaderOffset |
number |
No | Gap in px between the top of the scroll viewport and the floating header. Defaults to 16. |
stickyHeaderSortable |
boolean |
No | Whether the floating header can sort. Set to false for a labels-only bar — the sort arrows still reflect the current sort, they just stop responding to clicks. Defaults to true. |
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. |
toolbarExtras |
React.ReactNode |
No | Extra content rendered in the toolbar, between Export and View. For controls that act on the table as a whole (a level selector, a mode switch). For controls that act on the rows (expand/collapse all), pass toolbarExtrasLeading to Filterbar instead so the two families stay visually separate. |
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)
- Optional sticky header (
stickyHeader) — a fixed clone of the header floats at the top of the scroll viewport while the real one is out of view, mirroring the table's horizontal scroll; works in fullscreen too
- Atlaskit DnD packages require
--legacy-peer-depsdue to a React 19 peer dependency conflict stickyHeaderrenders a clone of the header portaled todocument.body, notposition: stickyon the real<thead>. The table sits insideoverflow-x-autowrappers that never scroll vertically, so a sticky<thead>would stick to a box that is itself scrolling off screen — i.e. do nothing. The clone measures the nearest vertically-scrolling ancestor (window if there is none) and the opaque background above the table, so it lines up inside cards, dialogs and fullscreen alike.- Frozen columns inside the sticky clone: the clone does not scroll, it is translated, so
position: stickyseesscrollLeft: 0and asticky left-0/right-0header cell would ride along with the translation instead of staying pinned.DataTableStickyHeadercounter-translates exactly those cells via two CSS variables, which puts them back over their body counterparts on both edges. If you add frozen columns, keep thestickyclass on the<th>(that is the hook) and make the cell background opaque — a translucent theme token lets the scrolling columns show through.
A virtualized, spreadsheet-style data grid for very large datasets. Excel-style per-column filters (checkbox value lists + typed conditions), cell/row/column selection with clipboard copy, drag-to-reorder and drag-to-resize columns, columns frozen to the right edge, saved views, CSV/XLSX export, a fullscreen toggle, and English/Portuguese chrome. Server-oriented by contract (a sparse getRow accessor + windowed block fetching), so it scales to millions of rows; useClientGridSource drives it from a plain array for local data.
Demo: localhost:3000/data-grid
components/ui/data-grid/ (whole folder — 21 files incl. index.ts, i18n.ts, client-source.ts)
components/ui/separator.tsx (if not already present)
npx shadcn@latest add button input popover select dropdown-menu checkbox separatornpm install @tanstack/react-virtual sonner \
@atlaskit/pragmatic-drag-and-drop @atlaskit/pragmatic-drag-and-drop-hitbox \
@atlaskit/pragmatic-drag-and-drop-react-drop-indicator papaparse xlsx --legacy-peer-deps| File | Purpose |
|---|---|
lib/utils.ts |
cn() class-name helper |
app/globals.css |
.grid-scrollbar styling + @keyframes grid-copy-flash |
app/layout.tsx |
Mount <Toaster /> (sonner) so copy/export toasts appear |
import {
DataGrid,
defaultColumnState,
useClientGridSource,
type GridColumn,
} from "@/components/ui/data-grid"
const columns: GridColumn<Row>[] = [
{ id: "name", title: "Name", type: "text", width: 180, sortable: true,
filter: { conditions: "text", paramMap: { contains: "q" } } },
{ id: "amount", title: "Amount", type: "number", width: 120, align: "right", sortable: true,
filter: { conditions: "number", paramMap: { range: { min: "min", max: "max" } } } },
]
const src = useClientGridSource({ rows, columns, sorting, filters, summaryColumn: "amount" })
// spread src.getRow / rowCount / totalCount / filterOptions / summary / resetToken / fetchRows onto <DataGrid>- Set a height via
className(e.g.h-[560px]) so the grid can virtualize. - Pinned (
pinned: "right") columns are display-only — ideal for action/link icon cells. language="pt"switches the chrome to Portuguese; defaults to"en".- Atlaskit DnD packages require
--legacy-peer-deps(React 19 peer conflict).
An Excel-like spreadsheet component for editing structured datasets. Supports typed cells (text, number, date, select, checkbox, textarea), readonly columns for fixed identifiers, keyboard navigation (Tab, Enter, Escape), per-cell validation, row selection, and add/delete row operations. Built on TanStack Table with existing Select, DatePicker, and Checkbox primitives as cell editors.
Demo: localhost:3000/editable-grid
components/ui/editable-grid/EditableGrid.tsx
components/ui/editable-grid/EditableCell.tsx
components/ui/editable-grid/useGridNavigation.ts
components/ui/editable-grid/validation.ts
components/ui/editable-grid/types.ts
components/ui/editable-grid/i18n.ts
components/ui/editable-grid/index.ts
npx shadcn@latest add table button checkbox tooltipnpm install @tanstack/react-table lucide-react| File | Purpose |
|---|---|
lib/utils.ts |
cn() class utility |
components/ui/select/ |
Select dropdown for cell editor |
components/ui/date-picker/ |
DatePicker for date cells |
components/ui/checkbox.tsx |
Checkbox for boolean cells |
components/ui/tooltip.tsx |
Tooltip for validation errors |
import { EditableGrid } from "@/components/ui/editable-grid"
import type { EditableColumnDef } from "@/components/ui/editable-grid"
type Row = { id: string; name: string; dept: string; salary: number; active: boolean }
const columns: EditableColumnDef<Row>[] = [
{ columnId: "id", title: "ID", type: "text", readonly: true },
{ columnId: "name", title: "Name", type: "text", validation: { required: true } },
{ columnId: "dept", title: "Dept", type: "select", options: [{ value: "eng", label: "Engineering" }] },
{ columnId: "salary", title: "Salary", type: "number", aligned: "right" },
{ columnId: "active", title: "Active", type: "checkbox", aligned: "center" },
]
<EditableGrid<Row>
columns={columns}
data={data}
rowIdKey="id"
onCellChange={(rowId, columnId, value) => updateData(rowId, columnId, value)}
onAddRow={() => addEmptyRow()}
onDeleteRows={(ids) => removeRows(ids)}
enableAddRow
enableDeleteRow
enableRowSelection
bordered
/>| Prop | Type | Default | Description |
|---|---|---|---|
columns |
EditableColumnDef<TData>[] |
— | Column definitions array. |
data |
TData[] |
— | Row data. Component is controlled — parent owns state. |
rowIdKey |
keyof TData & string |
— | Property used as unique row identifier. |
onCellChange |
(rowId, columnId, newValue) => void |
— | Fired when a cell value is committed. |
onRowChange |
(rowId, updatedRow) => void |
— | Fired with the full updated row after commit. |
onAddRow |
() => void |
— | Fired when add-row button is clicked. |
onDeleteRows |
(rowIds) => void |
— | Fired when rows are deleted. |
enableAddRow |
boolean |
false |
Show add-row button. |
enableDeleteRow |
boolean |
false |
Show delete buttons. |
enableRowSelection |
boolean |
false |
Show row selection checkboxes. |
language |
"en" | "pt" |
"en" |
UI language. |
bordered |
boolean |
false |
Add borders to all cells. |
compact |
boolean |
false |
Reduce padding and font size. |
accentColor |
string |
— | Accent color for checkboxes. |
| Prop | Type | Default | Description |
|---|---|---|---|
columnId |
keyof TData & string |
— | Data property key. |
title |
string |
— | Column header label. |
type |
CellType |
— | "text" | "number" | "date" | "select" | "checkbox" | "textarea" |
width |
number | string |
— | Fixed column width. |
readonly |
boolean |
false |
Visible but not editable. |
options |
OptionItem[] |
— | Dropdown options (required for "select"). |
validation |
ValidationRule |
— | Per-cell validation rules. |
placeholder |
string |
— | Placeholder text for empty cells. |
formatter |
(value, row) => ReactNode |
— | Display formatter (non-edit mode only). |
aligned |
"left" | "center" | "right" |
"left" |
Text alignment. |
- Typed cell editors: text, number, date (DatePicker), select (dropdown), checkbox, textarea
- Readonly columns: fixed identifier columns (e.g., SSN, employee ID)
- Keyboard navigation: Tab/Shift+Tab between cells, Enter to move down, Escape to cancel
- Per-cell validation: required, min/max, pattern, custom functions with error tooltips
- Row operations: add new rows, delete single or bulk-selected rows
- i18n: English and Portuguese
- Controlled component: parent owns data, grid fires callbacks on changes
- The component is controlled — pass
dataand handleonCellChangeto update state. - Checkbox cells are always interactive (no click-to-edit toggle needed).
- Select and Date cells auto-commit when a value is selected.
- Validation runs on commit. Errors show as red ring + tooltip. Values are still sent to parent even when invalid.
A tree-and-timeline chart: a sticky column pane on the left, a scrollable time grid on the right. Collapsible group rows roll their descendants into a summary track with duration-weighted progress, overlapping bars pack into lanes so the row grows instead of the bars colliding, and the scale switches between hourly, daily, weekly and monthly columns. Fully props-driven — one <Gantt> call, no compound sub-components.
Demo: localhost:3000/gantt
components/ui/gantt/Gantt.tsx
components/ui/gantt/GanttBar.tsx
components/ui/gantt/colors.ts
components/ui/gantt/timeline.ts
components/ui/gantt/types.ts
components/ui/gantt/index.ts
npx shadcn@latest add button select tooltipnpm install date-fns lucide-react| File | Purpose |
|---|---|
lib/utils.ts |
cn() class-merging utility |
Also relies on the .grid-scrollbar rules in app/globals.css for the always-visible scrollbar. Without them the chart still scrolls; it just uses the platform default scrollbar.
None.
import { Gantt } from "@/components/ui/gantt"
import type { GanttColumn, GanttRow } from "@/components/ui/gantt"
type Task = { status: string; owner: string }
const columns: GanttColumn<Task>[] = [
{ id: "name", header: "Name", width: 230, cell: (row) => row.label },
{ id: "status", header: "Status", width: 110, cell: (row) => row.data?.status },
]
const rows: GanttRow<Task>[] = [
{
id: "epic-1",
label: "Bug Tracking",
children: [
{
id: "task-1",
label: "Login redirect",
data: { status: "Done", owner: "Ana" },
bars: [{
id: "task-1-bar",
start: new Date(2026, 6, 13),
end: new Date(2026, 6, 15),
label: "Login redirect",
color: "blue",
progress: 1,
}],
},
],
},
]
<Gantt<Task>
rows={rows}
columns={columns}
defaultScale="month"
defaultDate={new Date(2026, 6, 22)}
onBarClick={(bar, row) => console.log(bar.id, row.id)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
rows |
GanttRow<T>[] |
— | Required. Row tree; a row with children becomes a collapsible group. |
columns |
GanttColumn<T>[] |
— | Required. Left pane columns. The first gets the indent + chevron. |
scale |
"day" | "week" | "month" | "quarter" | "year" |
— | Controlled preset — sets both the visible range and the column unit. |
defaultScale |
GanttScale |
"month" |
Uncontrolled initial scale. |
onScaleChange |
(scale: GanttScale) => void |
— | Fires when the toolbar picker changes. |
scales |
GanttScale[] |
all five | Presets in the toolbar picker; a single-entry array hides it. |
date |
Date |
— | Controlled anchor date the range is derived from. |
defaultDate |
Date |
new Date() |
Uncontrolled initial anchor date. |
onDateChange |
(date: Date) => void |
— | Fires on Today and the prev/next buttons. |
now |
Date | null |
current time | Position of the red now line; null hides it. Only rendered after mount. |
weekStartsOn |
0 | 1 | … | 6 |
0 |
First day of the week — affects week columns, grouping and the week range. |
title |
ReactNode |
derived | Overrides the toolbar range label. |
toolbar |
boolean |
true |
Renders the Today / scale / paging / title bar. |
actions |
ReactNode |
— | Rendered at the right end of the toolbar. |
zoomControls |
boolean |
true |
Floating +/− control; scales column width 0.5×–4×. |
rowHeight |
number |
36 |
Height of a single-lane row, in px. |
laneHeight |
number |
24 |
Extra height each additional overlap lane adds. |
nonWorking |
(start: Date, end: Date) => boolean |
weekends | Shades a column as non-working. Defaults to weekends on day columns only. |
summaryRows |
boolean |
true |
Group rows render a rollup track across their descendants. |
bordered |
boolean |
true |
Wraps the chart in a rounded border. |
height |
number | string |
520 |
Scroll viewport height; a number is px. |
onBarClick |
(bar: GanttBar, row: GanttRow<T>) => void |
— | Makes bars interactive and fires on click. |
onRowClick |
(row: GanttRow<T>) => void |
— | Fires when either pane of a row is clicked. |
onExpandedChange |
(expanded: Record<string, boolean>) => void |
— | Fires when a group expands or collapses. |
emptyState |
ReactNode |
"No rows…" |
Shown when rows is empty. |
className |
string |
— | Merged onto the outer wrapper. |
| Field | Type | Description |
|---|---|---|
id |
string |
Required. Unique row key. |
label |
string |
Required. Row name, available to every column renderer. |
bars |
GanttBar[] |
Bars on the row. Overlapping bars pack into lanes and the row grows taller. |
children |
GanttRow<T>[] |
Nested rows; presence makes the row a group. |
defaultExpanded |
boolean |
Defaults to true; pass false to start collapsed. |
data |
T |
Arbitrary payload handed to every column's cell renderer. |
| Field | Type | Description |
|---|---|---|
id |
string |
Required. Unique bar key. |
start / end |
Date |
Required. end is ignored when milestone is true. |
label |
string |
Inside the bar when it fits, otherwise just after it. |
labelPosition |
"auto" | "inside" | "outside" | "none" |
Overrides automatic placement. Default "auto". |
progress |
number |
0–1. Fills that fraction with the saturated hue, projected through the axis. |
color |
GanttColor |
One of the 8 validated hues, or neutral. Default neutral. |
variant |
"soft" | "solid" | "outline" |
soft = tint + saturated rail; outline = dashed, for planned/queued work. |
startLabel / endLabel |
string |
Small muted captions outside the bar's edges. |
icon |
ReactNode |
Rendered before the label, inside the bar. |
milestone |
boolean |
Ring marker at start instead of a bar. |
tooltip |
ReactNode |
Replaces the default label / range / progress hover card. |
disabled |
boolean |
Dims the bar and suppresses onBarClick. |
| Field | Type | Description |
|---|---|---|
id |
string |
Required. Unique column key. |
header |
ReactNode |
Required. Header cell content. |
width |
number |
Px. Defaults to 200 for the first column, 120 for the rest. |
align |
"left" | "center" | "right" |
Ignored on the first (tree) column. |
cell |
(row, ctx) => ReactNode |
Required. ctx carries depth, isGroup, expanded and the rollup span. |
- Two-pane layout: one scroll container; the column pane is
sticky left-0, the headerssticky top-0. Row alignment is exact by construction — no scroll syncing. - Tree rows: unlimited nesting, expand/collapse on the first column, controlled or uncontrolled.
- Group rollups: min start / max end across all descendants with duration-weighted progress, drawn as a hairline track with end caps and a trailing percentage.
- Overlap lanes: greedy interval packing puts colliding bars on separate lanes and grows the row instead of stacking bars on top of each other.
- Five scales:
day(hour columns) →year(month columns), each with a two-tier header. Paging steps by one range unit. - Zoom: 0.5×–4× column width, independent of scale.
- Milestones: zero-duration ring markers.
- Now line: red hairline + dot, mount-gated so it can't cause a hydration mismatch.
- Non-working shading: weekends by default on day columns; any predicate otherwise (shift envelopes, off-hours).
- Auto-focus: scrolls to the now line on mount, or to the earliest bar when now falls outside the range.
- Hover cards: every bar gets a tooltip with its label, range and progress; overridable per bar.
- Validated palette: 8 categorical hues in a fixed, colorblind-checked order (see Notes).
- Colour is a fixed 8-slot order —
blue, emerald, violet, amber, rose, teal, orange, indigo— chosen by searching orderings and keeping one that clears every adjacent-pair gate in both light and dark mode. Assign from slot 1 upward and never cycle; a 9th category folds intoneutralor gets its own view.ganttColorAt(i)does the lookup. Re-run the data-viz palette validator before changing a hue, a step or the order. - Every bar carries a saturated 3px leading rail, so the hue clears 3:1 against the surface even when the body is a pale tint.
- Bars that meet edge-to-edge keep a 2px surface gap; bars clipped by the visible range get squared-off ends.
- A trailing label is capped to the gap before the next bar in its lane, and dropped when there's no room — labels can't collide.
progressis projected through the time axis, not taken as a percentage of drawn width, so a bar clipped by the range still shows the right fill.- The component is uncontrolled by default. Pass
scale/date(with theiron*Changehandlers) to drive it from parent state. - Row heights are computed from lane counts and applied to both panes, which is what keeps them aligned — override
rowHeight/laneHeighttogether if you restyle bars.
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/MeasuredResponsiveContainer.tsx
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/MeasuredResponsiveContainer.tsx |
Wraps recharts' ResponsiveContainer; measures the slot before first paint so the chart never renders (and warns) at −1×−1 |
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)}
/>
// Reference lines + status gradient (single series)
<AreaChart
className="h-72"
data={usageData}
index="month"
categories={["Usage"]}
maxValue={1}
referenceLines={[{ y: 0.8, label: "Limit", color: "#ef4444" }]}
xColorStops={[
{ offset: 0, color: "#10b981" },
{ offset: 0.7, color: "#10b981" },
{ offset: 1, color: "#ef4444" },
]}
/>| 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 |
categories.length > 1 |
Show the legend above the chart. When omitted, it shows only for multi-series charts. |
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. |
yAxisTicks |
number[] |
— | Explicit Y-axis tick values (also drives the auto-inferred axis width). Ignored when softYAxis computes its own ticks. |
strictYAxisDomain |
boolean |
false |
Clip data outside the Y domain (allowDataOverflow) instead of letting it stretch the axis. |
referenceLines |
{ y, label?, color?, strokeDasharray? }[] |
— | Dashed horizontal reference lines (limits, targets, alerts). Lines outside the axis domain are discarded, never stretch it — include them via minValue/maxValue when they must be visible. |
xColorStops |
{ offset: number; color: string }[] |
— | Paints the FIRST series with a horizontal gradient along X (one stop per point, offset 0–1 in data order) — for series whose meaning changes over time (ok → warning → breach). The hover dot takes the color of its own point. |
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. The chart fills its parent (h-full) — give the parent a height or pass one here (e.g. h-72). |
- The
ChartColortype andCHART_COLORSconstant are exported from the barrel for external use. - The utility files in
components/charts/utils/are shared across chart components — copy them once and reuse. - Sizing: the chart fills its parent (
h-full min-h-0). Place it inside a container with a height (a sized card,h-72, a grid row) or pass a height viaclassName— an auto-height parent collapses the chart to 0. MeasuredResponsiveContainermeasures the slot before first paint, so recharts never renders (or console-warns) at −1×−1; a genuinely 0-sized slot still warns, on purpose.
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/MeasuredResponsiveContainer.tsx
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/MeasuredResponsiveContainer.tsx |
Wraps recharts' ResponsiveContainer; measures the slot before first paint so the chart never renders (and warns) at −1×−1 |
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 |
categories.length > 1 |
Show the legend above the chart. When omitted, it shows only for multi-series charts. |
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. |
negativeColor |
ChartColor |
"red" |
Color applied to bars with a negative value, overriding the category color — negative bars render below/left of the baseline and read as a different thing. |
showEndValueLabels |
boolean |
false |
Horizontal bars only (layout="vertical", non-stacked): renders the formatted value at the end of each bar, and widens the right margin to fit it. |
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
components/charts/utils/MeasuredResponsiveContainer.tsx
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/MeasuredResponsiveContainer.tsx |
Wraps recharts' ResponsiveContainer; measures the slot before first paint so the chart never renders (and warns) at −1×−1 |
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 compact decorative sparkline for stat cards and dense lists — no axes, no legend, no tooltip. Renders a soft gradient under the line by default, fills its parent container, and degrades to a muted placeholder block when there are fewer than two data points.
Demo: localhost:3000/charts/spark-area-chart
components/charts/spark-area-chart/SparkAreaChart.tsx
components/charts/spark-area-chart/index.ts
components/charts/utils/chartColors.ts
components/charts/utils/MeasuredResponsiveContainer.tsx
None.
npm install recharts| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already present in any shadcn project |
components/charts/utils/chartColors.ts |
chartColorToCss(), constructCategoryColors() |
components/charts/utils/MeasuredResponsiveContainer.tsx |
Wraps recharts' ResponsiveContainer; measures the slot before first paint so the chart never renders (and warns) at −1×−1 |
None.
import { SparkAreaChart } from "@/components/charts/spark-area-chart"
// Size via the container — the chart fills it
<div className="h-10 w-28">
<SparkAreaChart
data={history}
index="month"
categories={["value"]}
colors={["emerald"]}
/>
</div>
// Line only, custom hex color
<SparkAreaChart
data={history}
index="month"
categories={["value"]}
colors={["#8b5cf6"]}
showGradient={false}
strokeWidth={1.5}
/>| Prop | Type | Default | Description |
|---|---|---|---|
data |
Record<string, unknown>[] |
— | Required. Data rows. Non-numeric values are ignored when computing the Y domain. |
index |
string |
— | Required. X-axis field key. Kept for API symmetry with the other charts; the axis is never rendered. |
categories |
string[] |
— | Required. Keys to render as series. |
colors |
(ChartColor | string)[] |
["emerald"] |
One palette name or hex color per category, in order. |
showGradient |
boolean |
true |
Soft gradient fill under each line. |
strokeWidth |
number |
2 |
Line thickness in pixels. |
className |
string |
— | Additional classes on the wrapper. The chart fills its parent — size the parent, or pass h-*/w-* here. |
- With fewer than two points it renders a
bg-muted/40block instead of an empty chart — a one-point sparkline is a dot, which reads as a rendering bug. - No tooltip by design: it is decorative. When users need to read values, use
AreaChart.
A waterfall chart explaining how a starting total becomes an ending total through a sequence of additions and subtractions. Bars typed "total" anchor at zero and reset the running total; bars typed "delta" float from the previous running total and take their color from the sign. Dashed connectors bridge consecutive bars, and the Y axis derives nice bounds and ticks from the stacked extent.
Demo: localhost:3000/charts/waterfall-chart
components/charts/waterfall-chart/WaterfallChart.tsx
components/charts/waterfall-chart/index.ts
components/charts/utils/chartColors.ts
components/charts/utils/chartHelpers.ts
components/charts/utils/MeasuredResponsiveContainer.tsx
None.
npm install recharts| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already present in any shadcn project |
components/charts/utils/chartColors.ts |
chartColorToCss(), palette names |
components/charts/utils/chartHelpers.ts |
computeNiceTicks(), inferYAxisWidth() |
components/charts/utils/MeasuredResponsiveContainer.tsx |
Wraps recharts' ResponsiveContainer; measures the slot before first paint so the chart never renders (and warns) at −1×−1 |
None.
import {
WaterfallChart,
type WaterfallChartDatum,
} from "@/components/charts/waterfall-chart"
const data: WaterfallChartDatum[] = [
{ label: "Opening", value: 1200, type: "total" },
{ label: "Inflows", value: 480, type: "delta" },
{ label: "Outflows", value: -350, type: "delta" },
{ label: "Closing", value: 1330, type: "total" },
]
<div className="h-80">
<WaterfallChart data={data} valueFormatter={(v) => `$${v}`} />
</div>| Prop | Type | Default | Description |
|---|---|---|---|
data |
WaterfallChartDatum[] |
— | Required. Ordered steps: { label, value, type: "total" | "delta" }. Totals anchor at zero (value is the absolute total); deltas are signed contributions. |
valueFormatter |
(value: number) => string |
v => v.toString() |
Formats Y-axis ticks, tooltip values and legend. |
showLegend |
boolean |
true |
Legend mapping the three roles (totals, additions, subtractions). |
showTooltip |
boolean |
true |
Tooltip with the step value and the running total. |
showGridLines |
boolean |
true |
Horizontal grid lines. |
showConnectors |
boolean |
true |
Dashed lines bridging the end of one bar to the start of the next. |
totalsColor |
ChartColor | string |
"blue" |
Color of total bars (palette name or hex). |
additionsColor |
ChartColor | string |
"emerald" |
Color of positive delta bars. |
subtractionsColor |
ChartColor | string |
"red" |
Color of negative delta bars. |
yAxisWidth |
number |
auto | Pixels reserved for the Y axis; inferred from the formatted ticks when omitted. |
axisTextSize |
"xs" | "sm" | "md" | "lg" | number |
"xs" |
Axis label font size. |
labelTruncateAt |
number |
— | Truncates long X labels with an ellipsis (full label stays in the tooltip). |
minWidthPerBar |
number |
52 |
Minimum width per bar; when bars would get narrower, the chart scrolls horizontally instead of squeezing. |
className |
string |
— | Additional classes on the wrapper. The chart fills its parent — give the parent a height (e.g. h-80). |
- A
"total"step sets the running total rather than adding to it — use it for opening/closing anchors and any intermediate subtotal. - Deltas are colored by sign, not by category, so
value: -350is always the "subtractions" color regardless of position.
A single horizontal stacked bar showing how a total breaks down across categories, paired with an aligned legend listing each category's value and share. Hovering a segment or a legend row highlights both and dims the rest. Pure CSS + Tailwind — no charting library.
Demo: localhost:3000/charts/composition-bar
components/charts/composition-bar/CompositionBar.tsx
components/charts/composition-bar/index.ts
components/charts/utils/chartColors.ts
None.
None — no charting library required.
| File | Purpose |
|---|---|
lib/utils.ts |
cn() utility — already present in any shadcn project |
components/charts/utils/chartColors.ts |
Color palette, getColorClass(), constructCategoryColors(), isHexColor() |
None.
import { CompositionBar } from "@/components/charts/composition-bar"
// Basic composition
<CompositionBar
data={data}
category="assetClass"
value="amount"
valueFormatter={(v) => `$${v.toLocaleString()}`}
/>
// Sorted, custom palette
<CompositionBar
data={data}
category="source"
value="sessions"
sortOrder="descending"
colors={["violet", "amber", "rose", "teal", "cyan"]}
/>
// Bar only, no legend
<CompositionBar
data={data}
category="assetClass"
value="amount"
showLegend={false}
barHeight={8}
/>
// Fill a fixed-height card
<div className="h-64">
<CompositionBar
data={data}
category="assetClass"
value="amount"
fillParent
/>
</div>| Prop | Type | Default | Description |
|---|---|---|---|
data |
Record<string, any>[] |
— | Required. Array of data objects with keys for the category label and the numeric value. |
category |
string |
— | Required. Key in each data object used as the segment label (e.g. "assetClass", "source"). |
value |
string |
— | Required. Key in each data object used as the segment's numeric value. Percentages are computed against the sum of all values. |
colors |
(ChartColor | string)[] |
CHART_COLORS |
Palette for the segments. Accepts palette names or raw hex strings; cycles if fewer colors than categories. |
valueFormatter |
(value: number) => string |
v => v.toString() |
Formats the raw value in the legend and tooltip. |
percentFormatter |
(percent: number) => string |
p => `${p.toFixed(1)}%` |
Formats the share percentage in the legend and tooltip. Always receives the true percentage, never the inflated render width. |
sortOrder |
"ascending" | "descending" | "none" |
"none" |
Sort order applied to segments by value. "none" preserves the input array order. |
barHeight |
number |
12 |
Height of the bar in pixels. |
minSegmentPercent |
number |
0.5 |
Floor (in percent) applied to rendered segment widths so tiny slices stay visible. The inflation is reclaimed from the largest segment so widths still sum to 100. |
showLegend |
boolean |
true |
Show the legend rows below the bar (swatch, label, value, percentage). |
showTooltip |
boolean |
true |
Show the hover tooltip. Also gates the hover dimming of non-hovered segments. |
fillParent |
boolean |
false |
Stretch to the parent's height and distribute legend rows evenly across the available vertical space. |
emptyLabel |
string |
— | Text rendered under the placeholder bar when data is empty or all values sum to zero. |
className |
string |
— | Additional classes on the outer wrapper. |
- Rows with non-finite values are dropped. If the total is
<= 0, the empty state renders instead of the bar. minSegmentPercentonly changes rendered widths — the legend and tooltip always report the true share.fillParentrequires the parent to be a flex column with a fixed height for the legend distribution to be visible.- The built-in tooltip labels are hardcoded to Portuguese (
Total/Participação). EditChartTooltipinCompositionBar.tsxif you need different copy — there is nocustomTooltipprop. - 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.