pulld

Components your AI coding agent installs by itself.

An open, shadcn-compatible component registry. Point Claude Code, Cursor, or v0 at a component and it pulls it straight into your project — typed, accessible, theme-aware.

npx shadcn@latest add https://pulld.pages.dev/r/copy-button.json

Works with the shadcn CLI & MCP. 60 free components, growing.

Or add the @pulld namespace once in components.json, then install by name (@pulld/copy-button):

{ "registries": { "@pulld": "https://pulld.pages.dev/r/{name}.json" } }

This search is pulld Search running on this page — try “let users switch to dark mode” or “copy text to clipboard”.

Components

Copy Button

Accessible icon button that copies a string to the clipboard and shows a transient copied state. Has aria-label, an aria-live announcement, keyboard focus ring, and a configurable reset timeout. Use it next to code blocks, API keys, share links, or any inline value a user might want to copy.

npx shadcn@latest add https://pulld.pages.dev/r/copy-button.json
K

Kbd

Inline keyboard key rendered as a real <kbd> element and styled as a bordered monospace keycap. Use it wherever an interface names a key the reader is meant to press: command palettes and ⌘K hints, tooltips, menu item accelerators, empty states that suggest a shortcut, onboarding tours, documentation and changelogs, and keyboard-shortcut help sheets. Common asks it answers: "kbd component", "keyboard shortcut badge", "render Cmd+K", "hotkey chip", "keycap style", "show a keybinding in a tooltip", "⇧⌘P badge", "shortcut hint next to a menu item", "Ctrl+S indicator". shadcn/ui now ships a kbd of its own, so choose deliberately rather than by search rank. Theirs is a flat sans-serif chip with no border at text-xs, and it comes with a KbdGroup wrapper for multi-key sequences, a rule that shrinks icons placed inside it, and one that inverts its colours inside a tooltip — if you want any of those, take theirs. This one is a single element with a border, monospace text at 10px and slightly wider padding, so it reads as a physical key rather than as inline text and stays legible against surrounding prose at small sizes. Both are dependency-free, use the semantic <kbd> tag so assistive technology announces the content as keyboard input, and theme through shadcn tokens. For several keys in a row, wrap them in a flex container with a gap yourself, or install keyboard-shortcuts, which composes this into a grouped help sheet opened with ?.

npx shadcn@latest add https://pulld.pages.dev/r/kbd.json
No results

Empty State

The centred placeholder a screen shows when it has nothing to draw — a dashed panel with an optional icon, a heading, one line of explanation, and room for a call to action. Use it for an empty table, list, inbox, or feed, a search or filter that matched nothing, a workspace, project or team before its first item exists, a first-run or onboarding screen, a dashboard card with no data yet, an empty cart, folder, or notification tray. Common asks it answers: "empty state", "no results found", "zero state", "blank slate", "no data placeholder", "nothing here yet", "empty list or table component", "empty search results", "first run experience", "no items yet with a create button". shadcn/ui now ships an `empty` of its own, so choose deliberately rather than by accident: theirs is a six-part compound API (Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent) that composes into any arrangement and pulls in class-variance-authority; this is the one-import version — title plus optional icon, description and action, four props in total and no dependencies at all — for the much more common case where every empty state in the app looks alike and assembling six elements at each call site is just ceremony. Two accessibility details differ as well, and they are the two most often got wrong: here the title renders as a real h3, so it joins the heading outline and screen-reader users can reach it with heading navigation, whereas the official EmptyTitle is a styled div that heading navigation cannot see; and the icon wrapper is marked aria-hidden, because it is decoration, and announcing "inbox" or "circle-slash" before the sentence that actually explains the situation is noise. The description is capped at max-w-sm so the line keeps a readable measure inside a wide table. It has no hooks and no event handlers, so it carries no "use client" and renders inside a React Server Component without pulling a client boundary in behind it; you pass your own icon element, so it adds no icon library. Styled with shadcn tokens (border, muted-foreground) for light and dark themes. Distinct from skeleton and spinner, which say the rows are still loading: this one says the rows are not coming until the user does something.

npx shadcn@latest add https://pulld.pages.dev/r/empty-state.json
Revenue
$12.4k↑12%

Stat Card

The single-number tile at the top of a dashboard: a label, one big value, and an optional percentage change with an up or down arrow — green when the number moved the right way, red when it did not. Use it wherever a screen opens with a row of headline figures: an analytics or metrics dashboard, an admin overview, a KPI or scorecard row, a billing and usage summary, a SaaS home screen, a revenue or traffic report. Common asks it answers: "stat card", "metric card", "KPI card", "dashboard stat tile", "number card with percentage change", "revenue card with trend arrow", "analytics summary cards", "stats row", "show total users with growth", "Stripe/Vercel-style dashboard tiles". shadcn/ui ships card as an empty container with no notion of a metric, so the value typography, the delta colouring and the arrow are hand-rolled on every dashboard. Pass `label`, `value` and optionally `delta` (a number: positive renders the up arrow, negative the down arrow, and omitting it renders no delta at all) plus a `hint` line for the comparison period, e.g. "vs. last month". `value` is a ReactNode, not a string, so a pre-formatted currency or an Intl.NumberFormat result drops straight in and the component never guesses at your locale or currency. The direction is not left to colour alone: the arrow is aria-hidden and an sr-only "Up"/"Down" is spoken before the number, so the tile still means something to a screen reader and to a red-green colour-blind reader, which a bare green percentage does not. Composes into a responsive grid to form the stats row, and pairs with gauge and progress-ring when the figure is a ratio rather than a total. Styled with shadcn tokens (card, muted-foreground) with an explicit dark-mode pair for the delta colours; lucide-react is the only dependency. Distinct from feature-card, which sells a capability with an icon and copy: this one carries a live number.

npx shadcn@latest add https://pulld.pages.dev/r/stat-card.json

Theme Toggle

The one-button light/dark switch you drop in a navbar, header or settings row — click it and the whole app flips theme, and the choice survives a reload. Common asks it answers: "dark mode toggle", "theme toggle button", "light dark switcher", "toggle dark mode in Tailwind", "sun moon toggle", "dark mode without next-themes", "theme switcher for shadcn", "remember the user's theme", "respect the system theme". shadcn/ui has no installable toggle: its dark-mode guide hands you a next-themes provider to wire up and a dropdown to assemble yourself, so a plain button is written by hand every time. This is that button — one file, no provider, no context, no next-themes and no extra package. It toggles the `dark` class on the html element, which is exactly what Tailwind's class dark mode and the shadcn tokens already read, so it works with the theme you have rather than introducing another one. On first load it reads the saved choice from localStorage and falls back to the OS `prefers-color-scheme`, so a first-time visitor gets their system theme and a returning one gets their own; every later click writes the choice back. That first read happens in an effect rather than during render, because `window` does not exist on the server and an inline branch would either crash SSR or hydrate to different markup than it sent — which also means the theme is applied just after first paint, so add the usual one-line script in your document head if you need to kill the flash on a static page. The Sun/Moon swap is done with the `dark:` variant rather than JS state, so the icon matches the document even if something else on the page changes the theme. It is a real button that forwards every button prop (className, id, onClick, disabled), carries an aria-label and an aria-pressed that reflects the current mode, hides both icons from screen readers, and has a focus-visible ring; styling uses shadcn tokens (accent, muted-foreground, ring, border) so it matches your other icon buttons. lucide-react is the only dependency.

npx shadcn@latest add https://pulld.pages.dev/r/theme-toggle.json
AMK+3

Avatar Stack

Row of overlapping circular avatars — a facepile — that collapses everything past max into a "+N" badge. Reach for it wherever a set of people is shown on one line: team members on a project card, assignees or reviewers on an issue or pull request, meeting attendees, who is online or currently viewing a document, a 'shared with' list, participants in a thread, or contributors on a repo. Common asks it answers: "avatar group", "avatar stack", "facepile", "overlapping avatars", "stacked profile pictures", "user avatars in a row", "+N more avatars", "avatar overflow count", "assignee avatars", "who is online avatars", "team member avatars", "participant avatars". shadcn/ui ships a single avatar and no way to group them, so the two fiddly parts get rebuilt every time: the negative margin that overlaps the circles, and the ring in the *background* colour on each one that keeps the overlap legible on a card, a table row or a dark surface. Pass avatars as an array of { src?, alt } plus max (default 4); an entry with no src falls back to the first letter of its alt, and every avatar keeps its alt as its accessible name, so a screen reader reads the people rather than a row of unlabelled images. Plain img and Tailwind tokens: unlike the official avatar it pulls in no Radix package, and it has no dependencies at all.

npx shadcn@latest add https://pulld.pages.dev/r/avatar-stack.json
••••••

Password Input

Password text input with a built-in show/hide toggle button. Toggles between password and text, has an accessible aria-label and aria-pressed, and the toggle stays out of the tab order. Use it in sign-up, login, and reset-password forms.

npx shadcn@latest add https://pulld.pages.dev/r/password-input.json

Spinner

An inline loading indicator that announces itself: a spinning lucide Loader2 inside a role="status" live region with a screen-reader-only label, so a pending operation is heard as well as seen. Reach for it while fetching data, submitting a form, loading a page or a section, as a Suspense or lazy-route fallback, beside a disabled control, inside a table cell or panel that is still filling in, or anywhere you would otherwise drop a bare "Loading…" string. Common asks it answers: "loading spinner", "react spinner component", "loader component", "busy indicator", "activity indicator", "throbber", "accessible loading state", "aria-live loading announcement", "screen reader loading", "Suspense fallback spinner", "spinning Loader2", "animate-spin loader". The usual inline version — a bare Loader2 with animate-spin dropped straight into the markup — is invisible to assistive technology: the icon is decorative, so nothing is announced and a screen-reader user waits in silence with no idea anything is happening. Here the icon is aria-hidden and the announcement comes from an sr-only label instead, "Loading" by default; set label to say what is loading ("Loading invoices") so the same component can announce something useful on every screen. It is 1rem square and inherits the current text colour, so it sits correctly inside a button, a link, or a line of muted text with no extra styling, and every span prop (id, style, className, data-*) passes straight through to the wrapper. Pick the sibling that matches the shape: loading-button for a button whose own label swaps to a busy state, progress-ring or gauge when the percentage is known — this is the indeterminate "something is happening" case. Styled with shadcn tokens for light and dark themes; lucide-react is the only dependency.

npx shadcn@latest add https://pulld.pages.dev/r/spinner.json
add …

Code Block

composes copy-button

Read-only code snippet in a bordered, scrollable panel with a copy button that fades in on hover or keyboard focus and an optional uppercase language label in the corner. Reach for it whenever a page has to show code the reader will copy rather than edit: install and CLI commands in a README or docs site, curl and SDK examples in API reference pages, config file snippets, error output or stack traces, migration and changelog before/after blocks, an onboarding "paste this into your terminal" step, or the code sample on a developer landing page. Common asks it answers: "code block", "code snippet component", "copy code button", "pre code with copy", "terminal command block", "docs code sample", "shadcn code block", "snippet with language label", "copyable command". shadcn/ui has no code block at all, so this normally gets rebuilt from a bare <pre> plus a hand-wired clipboard button that forgets the copied state and never reveals itself to a keyboard user — here the button is inside focus-within, so tabbing to it makes it appear, and the copied confirmation is announced (it composes pulld's copy-button). Deliberately a container, not a highlighter: pass a `code` string and it renders semantic <pre><code> with `data-language` set, so it costs no bundle and stays out of the way if you later pipe in shiki, Prism, or highlight.js — unlike react-syntax-highlighter, which drags a whole grammar bundle in for a snippet you only wanted to display. Long lines scroll horizontally instead of wrapping. Extra props land on the wrapper div. No dependencies beyond your cn util.

npx shadcn@latest add https://pulld.pages.dev/r/code-block.json

Loading Button

composes spinner

Submit button that shows a spinner, announces itself and refuses to fire twice while an async action is in flight. Set loading={true} for the duration of the request — form submit, save, sign-in, checkout, delete confirmation, "generate" in an AI app, or any handler that awaits fetch — and the button disables itself so a second click cannot send the request again; loadingText swaps the label for "Saving…" or "Charging card…" while it waits. Common asks it answers: "react button with loading spinner", "disable button while submitting", "prevent double submit", "async submit button", "pending state button shadcn", "button spinner while awaiting fetch", "form submit loading state". Official shadcn/ui has no loading state anywhere in this path: its button ships no loading, pending or busy prop, and its spinner is a bare spinning icon with an aria-label — pairing them, disabling the button, and keeping the two in step is left to you, and doing it by hand is where the double-submit bug comes from. This one carries aria-busy while pending, and the spinner it composes is pulld's, which puts the label in a polite live region rather than only on the icon, so the wait is announced instead of being a silent frozen button. Two things worth knowing before you drop it in a form: it defaults to type="button", so pass type="submit" explicitly when it submits a form, and disabling a focused button takes it out of the tab order — the live region is what carries the state to a screen reader once focus has moved. The spinner atom installs with it through registryDependencies; there is nothing else to add.

npx shadcn@latest add https://pulld.pages.dev/r/loading-button.json

Confirm Button

Inline two-step confirmation on the button itself: the first click arms it — the label swaps to "Confirm?" and the button turns destructive — and only the second click calls onConfirm. It disarms itself after a timeout (3s by default) and on blur, so a stray double-click, a scroll away, or a Tab out can never fire the action. Reach for it wherever a modal would outweigh the action: a delete or remove button in a table row, a card, a list item or a toolbar; removing a member or collaborator; revoking an API key, token or session; disconnecting an integration; clearing a cache or a log; unsubscribing; resetting a filter or a setting; discarding a draft; leaving a channel. Common asks it answers: "confirm before delete without a dialog", "two-step delete button react", "click twice to confirm", "are you sure button shadcn", "inline confirmation button", "destructive button with confirmation", "delete button in a table row", "undo-less delete confirmation". Props: `onConfirm` (fired only on the confirming click), `confirmText` for the armed label, `timeout` in milliseconds, plus everything else a `<button>` takes. It is one real `<button>` element — Enter and Space confirm it, `disabled` is honoured, your `className` is merged through `cn`, and it defaults to `type="button"` so an armed click inside a form cannot submit it by accident. The armed state is exposed as `data-armed` for styling and announced through a polite live region, so the change is never carried by colour and label alone. Theme-aware through the destructive tokens, no dependencies, and `"use client"` because it holds a state and a timer. What official shadcn/ui offers instead is alert-dialog: a modal that pulls in @radix-ui/react-alert-dialog, renders through a portal, traps focus and needs open state wired up — right for a page-level, consequential confirmation, heavy for a row-level one. Official button has a destructive variant but no confirmation behaviour at all. For an action severe enough that a second click is not enough — deleting a production project or an account — use type-to-confirm, which makes the user type the name first.

npx shadcn@latest add https://pulld.pages.dev/r/confirm-button.json
Search…K

Command Palette

A ready-made ⌘K command palette: one component you drop in, open with a keyboard shortcut, and fill with actions. Use it for global search, jump-to-page navigation, quick actions and power-user shortcuts in dashboards, admin panels, editors, docs sites and any app that has outgrown its nav bar. Common asks it answers: "command palette", "cmd+k menu", "ctrl+k search", "command menu", "quick switcher", "spotlight-style search", "raycast-style launcher", "jump to anything", "action launcher", "global search dialog", "cmdk alternative", "command palette without cmdk". shadcn/ui does ship command, and the difference is what you get handed: that one is nine primitives — Command, CommandDialog, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, CommandSeparator, CommandShortcut — wrapping the cmdk npm package and pulling in the dialog item, which you then assemble into a palette yourself. This is a single component that depends on nothing but lucide-react: no cmdk, no dialog, no assembly. It arrives with the parts that are otherwise left to you — recently used entries surfaced when the input is empty, fuzzy filtering that highlights the matched characters in each result, grouped sections, wrap-around arrow-key navigation, and an async source hook so results can come from your own endpoint instead of a hard-coded array. The fiddly part of a palette is not the list, it is the focus: opening it traps focus so Tab cannot wander into the page behind, closing it puts focus back on whatever the reader was on, and the highlighted row is exposed with combobox and listbox roles plus aria-activedescendant, so the active option is announced while the text cursor stays in the input where typing belongs. If you would rather not run search infrastructure, the exported pulldSearchSource helper points the same async source at pulld Search for hosted semantic results.

npx shadcn@latest add https://pulld.pages.dev/r/command-palette.json
Changes saved

Toast

A complete toast / notification system in one file: call `toast()` — or `toast.success` / `.error` / `.info` / `.warning` / `.loading` / `.promise` — from anywhere in your app, and render a single `<Toaster />` at the root. No provider, no context, nothing to wire up. Use it for save and delete confirmations, form submission results, copy-to-clipboard feedback, async job status, optimistic updates that may fail, undo prompts, connection lost / restored notices, rate-limit and validation errors — any "it worked" or "it failed" message that should not interrupt what the user is doing. Common asks it answers: "toast notification react", "toast component shadcn", "snackbar component", "notification popup", "flash message", "alert toast", "undo toast with action button", "promise toast for async requests", "loading toast that turns into success", "toast without a provider", "toast from outside a component", "sonner alternative", "react-hot-toast alternative", "react-toastify alternative", "notification system react", "show a message after form submit". `toast.promise(request, { loading, success, error })` moves one toast through all three states in place, so an async call needs a single line instead of a chain of manual dismissals — `success` and `error` also accept a function, so the final message can quote the resolved value or the thrown error. Every toast takes a `description`, an `action` button (the undo affordance), a `duration` where `Infinity` pins it until dismissed, and an `id` you can reuse to update a toast already on screen. `<Toaster />` takes any of six positions. The queue lives outside React through useSyncExternalStore, so a toast can be fired from an event handler, a fetch or axios interceptor, a route guard, or a plain module — the places a hook-based API cannot reach, and the usual reason a toast library ends up wrapped in a context that has to be threaded everywhere. The details a rushed implementation drops: auto-dismiss pauses while the pointer is over the stack or focus is inside it, so a toast cannot vanish mid-sentence or while a keyboard user is reaching for its action button, and it stays paused across a promise's loading → success swap. Errors announce assertive and everything else polite, so a failure is not queued behind three success messages. Swipe-to-dismiss on touch, and enter / exit animations that respect prefers-reduced-motion. Official shadcn/ui no longer ships a toast of its own — it points at sonner, an npm dependency you do not control. This is one file you own and can edit, styled with your own theme tokens, whose only package import is lucide-react (already present in a shadcn project).

npx shadcn@latest add https://pulld.pages.dev/r/toast.json
Search…

Search Input

Search field with a leading magnifier icon and a trailing clear (✕) button that appears as soon as there is text, empties the field, and puts focus back so typing can continue. Use it above filterable lists and data tables, in sidebars and settings pages, over dropdown and combobox options, for docs and help search, for admin record lookup, and anywhere a "/" shortcut focuses a search box. Common asks it answers: "search input", "search bar", "search box", "filter input", "clearable input", "input with a clear button", "search field with icon", "type to filter a list", "table search box", "searchbar component". shadcn/ui has no search field: its input is a bare styled <input>, and its input-group is a layout kit of six parts (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) that pulls in button, input and textarea and hands you slots to hang your own icon and clear control in — you still write the clear button, the show-it-only-when-there-is-text rule, the refocus, and the event plumbing. This is that already assembled, in one import. The part that is easy to get wrong is clearing. Assigning to the input's value does not make React's onChange fire, so a hand-rolled clear button empties the box while the list behind it stays filtered on the old query. This writes through the native value setter and dispatches a bubbling input event, so onChange fires for controlled and uncontrolled usage alike and whatever filtering it drives actually updates. It also hides the WebKit search-cancel button so there is not a second ✕ beside the first, keeps the icon out of the accessibility tree and out of pointer events, gives the clear control a screen-reader label, and passes only one of value/defaultValue through so React never warns about a field switching between controlled and uncontrolled. The clear button is deliberately left out of the tab order, so Tab moves on to the next field instead of into a control that duplicates select-all-and-delete. Standard input props and a forwarded ref pass straight through, so a "/" hotkey can focus it. Theme-aware via shadcn tokens; depends only on lucide-react.

npx shadcn@latest add https://pulld.pages.dev/r/search-input.json
3+

Number Input

A number field with − and + stepper buttons either side of it. Reach for it wherever someone adjusts a number by one rather than typing it: a quantity picker in a cart, checkout or order form, seats, guests, rooms, tickets or attendees, a per-page or page-size control, retries, timeout or concurrency in a settings panel, font size, padding or spacing in an editor — anywhere you would otherwise reach for a bare <input type="number"> or a spinbutton. It keeps type="number" underneath, so native validation and valueAsNumber still work, and fixes the parts of it that are unpleasant in practice: the browser's own spin buttons are hidden (they are inconsistent across browsers and absent on mobile) and replaced with real, theme-aware, keyboard-reachable ones, inputMode="decimal" brings up the numeric keypad on phones, and the value is tabular-nums so the digits do not jump as they change. Decimal steps do not drift: stepping by 0.1 counts the step's decimal places and rounds to them, so you get 0.3 rather than 0.30000000000000004. Each button clamps to min or max and disables itself once the value is at that bound, and the buttons are taken out of the tab order so Tab still lands on the field itself. The step is written through the input's native value setter and dispatches a real input event, which is the part that is easy to get wrong: onChange fires whether the field is controlled or uncontrolled, so react-hook-form, Formik and plain useState all see the change instead of silently missing button presses. Only one of value/defaultValue ever reaches the input, so React never warns about switching between controlled and uncontrolled. Official shadcn/ui has no number field: its input is a bare 768-byte element, and input-group and field are assembly kits that pull in button, input, textarea, label and separator without a line of numeric logic between them — no stepping, no clamping, no min/max state. This depends only on lucide-react for the two icons.

npx shadcn@latest add https://pulld.pages.dev/r/number-input.json
42

OTP Input

One-time passcode / verification code input split into individual single-character slots (default 6). Use it for two-factor authentication (2FA), email confirmation codes, phone/SMS verification, and authenticator app codes on verify, login-challenge, and confirm-email screens. Paste a full code into any slot and it distributes across them, supports OS one-time-code autofill, auto-advances as you type, Backspace clears and steps back, and arrow/Home/End keys move between slots. Works controlled or uncontrolled, forwards a ref to focus from a shortcut, fires onComplete when the last slot fills, and can mirror its value into a hidden input for native form submit. Unlike a single text field it ships the familiar boxed code UI with accessible per-digit labels and a group label. Theme-aware via shadcn tokens; no dependencies.

npx shadcn@latest add https://pulld.pages.dev/r/otp-input.json
reactui|

Tag Input

Multi-value text input that turns typed entries into removable chips — press Enter or comma to add a tag, click the × or press Backspace on an empty field to remove the last one, and paste a comma- or newline-separated list to add many at once. Use it for tags, labels, keywords, categories, email recipients, skills, or allowed domains — any free-form list of short values on a form or filter bar. Works controlled or uncontrolled via a string[] value, forwards a ref to focus the field, and supports a max count, case-insensitive de-duplication, and a validate hook to reject bad entries. shadcn/ui ships no tag or chips input; chips use secondary tokens so they follow your theme, each remove button is aria-labelled, and add/remove is announced via an aria-live region. Depends only on lucide-react.

npx shadcn@latest add https://pulld.pages.dev/r/tag-input.json
tok_1a2b

Copy Field

composes copy-button

Read-only field that shows a value with a copy button docked at its right edge: one click copies, and focusing or clicking anywhere in the field selects the whole value. Use it wherever a generated string is read once and copied — an API key or secret, an access token, a client ID and client secret, a database connection string, an invite or share link, a webhook or callback URL, a license key, recovery or backup codes, a referral code, a wallet address, an account, order or transaction ID, an ngrok or preview URL, the CLI command your onboarding tells the user to run. Select-on-focus is the part that matters and the part hand-rolled versions leave out: navigator.clipboard.writeText rejects on an insecure origin, inside a sandboxed iframe, or when the document is not focused, and a copy button that swallows that error leaves the user with a value they cannot get out of the box. Here the whole value is already selected, so Ctrl/Cmd+C works, and it is reachable from the keyboard because focus alone selects it. The button announces itself properly too — it composes pulld's copy-button, so its accessible name flips between "Copy to clipboard" and "Copied", a polite live region says "Copied" for a screen reader, the icon is aria-hidden, and the copied state reverts after a timeout you can set. The field renders in monospace so an O and a 0 are distinguishable, forwards a ref so you can focus it from a shortcut, and takes an aria-label (default "Copyable value"). Official shadcn/ui has no copy field: its input-group is an assembly kit of six parts (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) that pulls in button, input and textarea and contains no clipboard call, no read-only handling and no selection behaviour — you would be writing all of the above yourself. Two files, no npm dependencies, shadcn tokens, dark mode included.

npx shadcn@latest add https://pulld.pages.dev/r/copy-field.json
DayWeek

Segmented Control

A row of 2–4 mutually exclusive choices drawn as one moving pill on a shared track — the iOS-style segmented control, and what most dashboards use to switch a view or a range without navigating anywhere. Reach for it wherever a single setting has a handful of choices that all fit on screen at once: List/Grid/Board, Day/Week/Month or 24h/7d/30d above a chart, Light/Dark/System, °C/°F, Monthly/Yearly on a pricing page, Newest/Oldest, All/Active/Archived, Preview/Code on a docs example, Table/JSON on a response viewer. Common asks it answers: "segmented control", "segmented button", "iOS segmented control", "pill toggle", "toggle switcher", "view switcher", "time range switcher", "chart period selector", "sort or filter toggle", "unit toggle", "tabs without panels", "Ant Design Segmented", "MUI ToggleButtonGroup" — the control usually faked with a row of buttons and a useState. How it differs from the neighbours official shadcn/ui ships: tabs and toggle-group each pull in a Radix package (@radix-ui/react-tabs, @radix-ui/react-toggle-group), and button-group is a layout wrapper with no selection of its own. This is one file with no dependencies, and it is a real radio group — role=radiogroup on the track, role=radio and aria-checked on every segment — so assistive technology announces one setting with a selected option among several rather than a row of unrelated buttons. Tabs additionally owns panels and the tab/tabpanel relationship, which is the wrong contract when the choice only filters or reframes data already on the page, and a switch only covers two states. The keyboard follows the radio pattern rather than the button one: arrow keys (left/right and up/down) move and select in a single press and wrap around the ends, Home/End jump to the first and last usable segment, disabled segments are stepped over instead of trapping focus, and a roving tabindex keeps the whole group one tab stop with the selected segment as the entry point. Also: per-segment disabling as well as a whole-group disabled state, controlled or uncontrolled through a string value with onValueChange, a focus-visible ring, and shadcn tokens throughout so it follows the theme in light and dark.

npx shadcn@latest add https://pulld.pages.dev/r/segmented-control.json
23

Step Indicator

Horizontal stepper that shows where someone is in a fixed sequence — numbered circle markers joined by a connecting line, each drawn as complete (filled, with a check), current (ringed and highlighted) or upcoming (muted). Pass the steps and a current index and it derives every state; there is nothing to keep in sync by hand. Reach for it at the top of anything multi-step: a checkout or cart flow, a signup and onboarding wizard, account or workspace setup, a KYC or identity-verification flow, a document or tax filing, a multi-page form split across screens, an upload-then-review-then-publish pipeline, a survey or quiz, or an installer. Common asks it answers: "stepper component", "step indicator react", "multi-step form progress", "wizard steps ui", "checkout progress bar with steps", "onboarding progress indicator", "shadcn stepper", "progress steps 1 2 3", "form wizard header". shadcn/ui has no stepper of any kind — its registry has no step, stepper or wizard item, and its progress component is a single indeterminate bar with no notion of discrete stages, labels or a current position, so this normally gets rebuilt by hand out of divs and borders. Built as an ordered list, because the steps are an ordered list: the active one carries aria-current="step", every marker states its own status in screen-reader-only text (Completed / Current step / Not completed) rather than leaving the meaning to a colour and a tick, and the check icon is aria-hidden so it is not announced twice. Conveying stage by colour alone fails WCAG 1.4.1, which is why the status is always spelled out. Pass onStepClick and the steps already reached become real buttons with a focus-visible ring, while upcoming steps stay inert — a stepper that lets someone jump forward past validation is worse than one that is not clickable at all. Theme-aware through shadcn tokens with dark mode, and the only dependency is lucide-react for the check icon.

npx shadcn@latest add https://pulld.pages.dev/r/step-indicator.json
★★★★

Rating

Star rating for both input and display, with half-star support. Use it to collect a score — rate this product, leave a review, a satisfaction or feedback rating, thumbs/stars on an order or support ticket — or, in read-only mode, to show an average score next to reviews, listings, or search results (an average like 3.7 fills 70% of a star). shadcn/ui ships no rating component. Works controlled or uncontrolled via a number value with onValueChange, forwards a ref, and posts through a hidden input in native forms via `name`. Set allowHalf to pick half stars — click the left half of a star, or step by 0.5 with the keyboard. Accessible as a slider: it's focusable with a focus-visible ring, arrow keys raise and lower the score, Home clears to 0 and End maxes out, and it exposes aria-valuenow plus a spoken aria-valuetext ("3.5 out of 5 stars"); read-only mode renders as a labelled image instead. The empty and filled stars are decorative and aria-hidden. Configure the number of stars with max and pixel size with size. Theme-aware via shadcn tokens (filled stars use the primary color, so it follows your theme in light and dark mode); only depends on lucide-react for the star icon.

npx shadcn@latest add https://pulld.pages.dev/r/rating.json

Timeline

Vertical timeline that renders a list of events as dots on a connecting line, each with an optional time, title, and description. Use it for an activity feed, audit or history log, a changelog or release notes, order/shipment tracking, a deploy or CI/CD run log, notifications, a comment or event stream, a roadmap, or an 'about' resume/experience list — anywhere you show what happened in chronological order. shadcn/ui ships no timeline. Pass an items array (title, optional time, description, icon, and a color accent); the timestamp renders as a semantic <time> element with a machine-readable dateTime, the connecting line and decorative dots are aria-hidden, and each marker takes a per-item color (muted, primary, success, warning, destructive) so you can flag status like succeeded/failed/pending. Pass an icon to render an icon badge instead of a plain dot. It's a pure display component with no state, so it works in server components with no 'use client'. Theme-aware via shadcn tokens with dark mode; no dependencies (bring your own icons).

npx shadcn@latest add https://pulld.pages.dev/r/timeline.json
New — try it →×

Announcement Bar

A dismissible bar pinned above your header for a message the whole site needs to see: a launch or new release, a promo, sale or free-shipping offer, a scheduled-maintenance or downtime window, an incident or status notice, a beta or early-access note, a cookie or GDPR notice, a plan-expiring or payment-failed warning, or a 'you are viewing the docs for an old version' banner. Give it an id and it remembers the dismissal in localStorage, so a visitor who closes it does not meet it again on the next page or the next visit; bump version (or the id) when the wording changes and it comes back for everyone. It renders nothing until it has mounted, which is the part that is easy to get wrong on your own: reading localStorage while rendering makes the server and the browser disagree and React throws a hydration mismatch, and rendering the bar first and hiding it afterwards flashes a banner the user already dismissed on every single page load. The bar is a labelled landmark region rather than a plain div, so it is reachable by landmark navigation instead of being an unnamed strip of text before the header; the close button has a real accessible name and a focus ring, the leading icon is aria-hidden because it repeats the text, and dismissible={false} drops the button entirely for a banner that must stay put. Two looks — primary is a solid accent bar, default is a muted bar with a bottom border — both from shadcn tokens, so it follows light and dark with the rest of your theme. Takes an action slot for the trailing 'Read more' or 'Upgrade' link and calls onDismiss so you can log it. Official shadcn/ui has nothing that does this: alert is a static box that cannot be closed and remembers nothing, alert-dialog is a modal that blocks the page until answered, and sonner is a toast that floats in and disappears on a timer — none of them is a persistent top-of-page bar, and none of them survives a page load. lucide-react is the only dependency, for the close icon.

npx shadcn@latest add https://pulld.pages.dev/r/announcement-bar.json
Drop files

File Dropzone

Drag-and-drop file upload area that also opens the native file picker on click, Enter, or Space. Use it wherever a user uploads a file: an avatar or image upload, a CSV or spreadsheet import, a document/PDF/resume upload, attachments, a bulk media drop, or an import step in a form or wizard. shadcn/ui ships no dropzone or file input. Wraps a hidden <input type="file"> so it stays a real form control, and filters both dropped and picked files by `accept` (mime, mime wildcard like image/*, or .ext), `maxSize` in bytes, and `maxFiles`, calling `onReject` with the reason (type, size, too-many) for anything skipped. Works controlled or uncontrolled via a File[] value, forwards a ref to the input, and toggles a highlighted drag-over state. Accessible as a role=button region with keyboard activation, a focus-visible ring, an aria-live announcement on every add/reject, and a disabled state; supports single or multiple files and custom children for the drop label. Depends only on lucide-react for the upload icon.

npx shadcn@latest add https://pulld.pages.dev/r/file-dropzone.json
72%

Progress Ring

Circular (radial) progress indicator drawn with SVG stroke-dasharray, showing completion as a ring that fills clockwise from 12 o'clock with a smooth animated transition. Use it when a linear progress bar doesn't fit the space or you want the percent in the center — file/image upload progress, a usage or quota meter (storage, API credits, plan limits), a goal/completion or profile-strength tracker, a step or onboarding progress badge, or a countdown/timer dial. Set showValue to render the rounded percent in the middle, pass children for a custom center (icon or label), or set indeterminate to spin an arc while the total is unknown. Exposes role=progressbar with aria-valuenow/min/max. Unlike shadcn's linear Progress, this is a compact radial gauge. Depends only on your cn util — no extra packages.

npx shadcn@latest add https://pulld.pages.dev/r/progress-ring.json
Pro
$29/mo
UnlimitedAnalytics

Pricing Card

A single pricing plan card — the tier box on a pricing page, a plans-and-billing settings screen, an upgrade or paywall modal, a compare-plans table, or the subscription row in an account page. It takes a plan name, a big price with its billing period ($29 /mo), an optional one-line pitch, a feature list and a call-to-action, and several of them sit side by side to make the whole pricing table. Features are the part that is usually got wrong. Pass a string for an included feature or an object to mark one excluded; an excluded row is muted and struck through, and it also carries a screen-reader-only "Not included:" prefix — because text-decoration: line-through is not announced, so a purely visual strikethrough tells a sighted reader the feature is missing and tells a screen-reader user it is included. The check and minus icons are aria-hidden, since the text already says which is which. Set featured to mark the recommended tier: it draws a primary ring and a "Most popular" badge, and badge takes any other label (Best value, Current plan). headingLevel picks h2, h3 or h4 so three cards dropped into a section do not break the page outline — a pricing table is the classic place where a hardcoded h3 lands under the wrong h2. Against official shadcn/ui there is no pricing card to compare with, only pieces to assemble: card is a 1,828-character generic container with no price, plan, tier or feature vocabulary in it at all, item is a ten-part compound kit (ItemGroup, Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader, ItemFooter, ItemSeparator) that additionally pulls in separator and is built for list rows, and badge and button each drag in @radix-ui/react-slot. None of the four contains a single pricing term. This is one component with lucide-react as its only dependency, theme-aware through shadcn tokens in light and dark.

npx shadcn@latest add https://pulld.pages.dev/r/pricing-card.json
Fast
Ships in milliseconds

Feature Card

An icon + heading + one line of copy, as the repeating tile in the features or benefits section of a landing or marketing page — a “why us” grid, “what’s included”, value props, product highlights, services, capabilities, or a perks row on a pricing page. Drop several into a responsive grid (grid-cols-2 / grid-cols-3) and that is the whole section; each card takes icon, title, description and optionally href. Set href and the entire card becomes the click target: it renders as an <a> rather than a <div>, with hover and a focus-visible ring, so keyboard users get one tab stop per card instead of hunting for a small link nested inside it. shadcn/ui ships no feature card. Its card is a generic container (~1.8KB of source, no dependencies) with no icon slot and no href — the icon square, the heading and the link are all yours to assemble. Its newer item is the closest thing in shape and does have an icon slot, but it is a ten-part compound kit (ItemGroup, Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader, ItemFooter, ItemSeparator) that also pulls in separator, and it is built for list rows rather than a marketing grid. Two concrete differences past the assembly work: official’s ItemTitle renders a <div>, so a features grid built from it contributes nothing to the document outline, whereas this renders a real heading you choose with headingLevel (h2/h3/h4, default h3) so screen-reader users can jump feature to feature; and official’s ItemMedia sets no aria-hidden, so a purely decorative icon can still be announced, whereas this marks the icon square aria-hidden and lets the title carry the meaning. Neither official component accepts an href, so “make the whole tile a link” is hand-wired in both. Zero dependencies — bring your own icon element (a lucide-react icon, an emoji, an <img>); it sits in a tinted primary/10 square, and everything else follows your shadcn tokens including dark mode.

npx shadcn@latest add https://pulld.pages.dev/r/feature-card.json
72

Gauge

A semicircular (half-circle) gauge / dial that shows a single measurement within a known range, drawn with an SVG arc that fills from the left with a smooth animated transition. Use it whenever you want an at-a-glance meter rather than task progress: CPU/memory/server load, disk or storage usage, API rate-limit or quota consumption, a health/uptime or performance score, a speedometer, temperature/humidity, battery or signal level, a credit/risk or Lighthouse-style score, or a KPI/target attainment dial. Set `segments` to change the arc color at thresholds — e.g. green under 60, amber under 85, red to 100 — passing shadcn/Tailwind color classes so zones stay theme-aware; omit it for a single primary-colored dial. Configure the scale with min/max, render the number in the center with showValue, add a caption via label, or supply children for fully custom center content, and format the number with formatValue. shadcn/ui ships no gauge, meter, or speedometer component; unlike the linear Progress or a radial progress ring (which read as completion), this exposes role=meter with aria-valuemin/max/now for a measured value. No dependencies beyond your cn util.

npx shadcn@latest add https://pulld.pages.dev/r/gauge.json
3 minutes ago

Time Ago

Auto-updating relative timestamp — "3 minutes ago", "just now", "in 2 days" — that re-renders on a timer so the label stays fresh without a reload. Use it wherever a raw date would be noise and recency is what matters: comment/post/message timestamps, a notification or activity feed, "last seen"/"last updated"/"last synced" labels, commit or deploy history, table rows (created/modified), or a chat's message time. shadcn/ui ships no time-ago/relative-time component. Pass date as a Date, an ISO string, or epoch milliseconds. Wording comes from the platform's own Intl.RelativeTimeFormat, so it localizes for free via the locale prop and reads correctly for both past and future times; set numeric="auto" to get "yesterday"/"tomorrow" instead of "1 day ago", and format to "short" or "narrow" for compact "3 min. ago"/"3m ago". Anything newer than justNowThreshold seconds (default 45) shows justNowLabel ("just now"). The tick rate adapts — every 15s while under a minute old, per-minute under an hour, then hourly — or pin it with updateInterval. Renders a semantic <time> element with a machine-readable dateTime and a title tooltip carrying the full localized date, and is SSR/hydration-safe. Theme-aware via shadcn's text-muted-foreground token; depends only on your cn util — no date library, no extra packages.

npx shadcn@latest add https://pulld.pages.dev/r/time-ago.json
design ×eng ×

Multi Select

Searchable multi-select dropdown — pick several options from a list, shown as removable badges in the trigger. Use it wherever a form needs "choose one or more": assigning tags/labels/categories to a post or product, picking team members or assignees, filtering a table by multiple statuses, selecting skills, permissions/roles, regions, or channels. shadcn/ui ships no multi-select — its Select and Combobox are single-value only; this fills that gap with the select-only combobox ARIA pattern (combobox trigger + aria-multiselectable listbox, aria-activedescendant tracking, checkbox-style checked marks, selection changes announced via aria-live). Type in the built-in search box to filter options (hide it with hideSearch for short lists), arrow keys navigate, Enter toggles, Escape closes, Backspace removes the last selected value; click a badge's × to deselect without opening. Works controlled (value + onChange) or uncontrolled (defaultValue) with a string[] of values, supports per-option disabled and a max selection cap, and follows your theme via shadcn tokens (secondary badges, accent highlight, popover surface). No Radix, no cmdk — depends only on lucide-react and your cn util.

npx shadcn@latest add https://pulld.pages.dev/r/multi-select.json
02hrs:14min:33sec

Countdown

A live countdown timer to a future moment — it re-renders every second, ticks down, never goes negative, and fires an onComplete callback once when it reaches zero. Use it wherever you're waiting on a deadline: a product/waitlist launch or "coming soon" page, a sale/offer/flash-deal or cart-reservation expiry, an OTP/verification resend or rate-limit cooldown, an auction or bid close, a webinar/event/stream start time, a maintenance window, a booking or checkout hold, or a quiz/game round timer. Pass `to` as a Date, an ISO string, or epoch milliseconds. By default it renders labeled days/hours/minutes/seconds segments (the days block appears only once at least a day remains, or force it with showDays) using shadcn card/border/foreground tokens with tabular-nums so digits don't jitter. For a fully custom face — a compact "02:14:33", a circular ring, marketing hero digits — pass a render-prop child that receives { days, hours, minutes, seconds, total, isComplete } and return your own markup. shadcn/ui ships no countdown or timer component; this is the future-facing counterpart to a relative "time ago" label. It's SSR/hydration-safe (server and first client render agree, then a real clock takes over) and accessible: role=timer with an aria-atomic sr-only sentence ("2 days, 14 hours, 33 minutes remaining") while the visual segments are aria-hidden, so screen readers can read the state without being spammed each second. Tune the tick with interval (100 for smooth, 60000 for minute-only) and swap the finished view with completedLabel. Depends only on your cn util — no date library, no extra packages.

npx shadcn@latest add https://pulld.pages.dev/r/countdown.json
Project name

Inline Edit

Click-to-edit text that stays in place: it shows a value as plain text with a subtle pencil affordance, then swaps to an input right where the text was when you click it (or focus it and press Enter/Space). Enter or blur commits the change, Escape reverts to the original — no separate dialog, drawer, or edit form. Use it to rename a title, project, board, list, file, or column, edit a table/grid cell or a kanban card name, or tweak a single profile or settings field (display name, bio, label) directly on the page. Common asks it answers: "edit in place", "click to edit", "editable label/text", "rename inline", "inline text edit", "double-click to rename". shadcn/ui ships no inline-edit or editable-text component — you'd otherwise wire an Input plus toggle state and keyboard handling by hand; this packages that: controlled via value + onSave (called with the trimmed new value only when it actually changed), auto-focuses and selects the text on entry so typing replaces it, and offers placeholder for empty values and saveOnBlur to require an explicit Enter instead of committing on blur. Fully keyboard-accessible (real button trigger, aria-labels on both the trigger and the input, focus-visible rings), theme-aware through shadcn tokens (input/accent/muted-foreground) with no hardcoded colors, and dependency-free apart from lucide-react and your cn util.

npx shadcn@latest add https://pulld.pages.dev/r/inline-edit.json

Autosize Textarea

A textarea that auto-grows as you type and stops at a maximum height, then scrolls. Use it wherever a fixed-height box is wrong: a chat, message, or AI prompt composer, a comment/reply/review box, a commit or PR description, a bio, note, changelog, or feedback field — anywhere a one-line input is too small but a tall empty box wastes the page. Common asks it answers: "auto resize textarea", "auto-growing / expanding textarea", "textarea that grows with content", "chat input that expands", "textarea min rows max rows", "react-textarea-autosize alternative". shadcn/ui's own textarea is a fixed-height styled element with a drag handle — this replaces that behaviour: minRows sets the collapsed height, maxRows caps the growth before it starts scrolling, and the height is measured from the element's real computed line-height, padding, and box-sizing, so it stays correct with your own font size, custom padding, or a className override. It also re-measures whenever the field's own width changes — a window resize, but equally a collapsing sidebar, an opening panel, or a tab becoming visible, none of which resize the window — and renders at roughly the right height before hydration via the rows attribute, so there is no first-paint jump. Keeps the native <textarea> element and forwards a ref to it, so labels, placeholders, form libraries (react-hook-form), and validation all work unchanged; styled with shadcn tokens (border-input, ring, muted-foreground) for automatic light/dark theming, and ships with zero dependencies beyond your cn util.

npx shadcn@latest add https://pulld.pages.dev/r/autosize-textarea.json
$1,234.50

Currency Input

A money input that shows a grouped, currency-formatted amount ($1,234.50) when idle and the raw number while you're editing, so the cursor never fights the thousands separators or symbol. Use it in any form that takes an amount: a price or product cost field, an invoice/quote line item, a budget/limit/goal, a donation, tip, or payment amount, a salary or rate field, an expense entry, or a checkout total. Common asks it answers: "currency input", "money input", "price field", "formatted amount input", "thousands separator input", "dollar/euro input", "react-currency-input alternative". shadcn/ui ships no currency or money field — you'd otherwise bolt masking onto its Input by hand; this packages it: value/onValueChange work in plain numbers (major units, e.g. 1234.5), not strings, so there's no parsing on your side, and it commits the rounded number on blur to the currency's own precision. The symbol, grouping, symbol placement, and decimal places come from Intl.NumberFormat via the `currency` (ISO 4217, default USD) and `locale` (default en-US) props, so $/€/¥ and 2-decimal vs 0-decimal (JPY) currencies all render correctly with no hardcoding. Typing follows that same locale, so comma-decimal locales (de-DE, fr-FR) accept "1.234,50" and "1234,50" rather than silently misreading them, and grouping characters or a pasted currency symbol are ignored; set allowNegative for refunds or adjustments. It stays controlled or uncontrolled like a native input, forwards a ref to the real <input> (so <Label htmlFor>, name, placeholder, and form libraries like react-hook-form all work), uses inputMode="decimal" for a numeric mobile keypad, and is styled with shadcn tokens (border-input, ring, muted-foreground, tabular-nums) for automatic light/dark theming with no extra dependencies beyond your cn util. Distinct from number-input, which is a stepper for counts/quantities with +/− buttons; this one is for formatted money.

npx shadcn@latest add https://pulld.pages.dev/r/currency-input.json
jane@acme.co
Email

Floating Label Input

A text input whose label rests inside the field like a placeholder, then shrinks and floats up onto the top border the moment the field is focused or has a value (the Material "outlined" floating-label pattern). Use it anywhere you want a compact, self-labelling field: a login or sign-up form (email, password), a settings or profile form, a contact or checkout form, a search or filter panel, or any dense form where separate labels above every input would waste vertical space. Common asks it answers: "floating label input", "animated / material label", "label that moves up on focus", "placeholder that turns into a label", "outlined text field", "MUI TextField for shadcn". shadcn/ui ships no floating-label field — you'd otherwise wire the placeholder-shown CSS onto its Input by hand; this packages it and keeps it a real, accessible input. The float is driven purely by CSS (`:placeholder-shown` / `:focus` on the peer input) with no JS state, so it works for controlled or uncontrolled inputs, survives browser autofill, and is correct before hydration with no first-focus jump. The label is a genuine `<label htmlFor>` (not a fake overlay): the `id` defaults to a stable React.useId() value so the association always holds and clicking the label focuses the input. It forwards a ref to the underlying <input>, so every native prop works unchanged — `type` (email/password/tel/url…), `name`, `value`/`onChange`, `required`, `disabled`, `autoComplete`, and form libraries like react-hook-form. Pass `error` to show an invalid state (destructive border, ring, and label, plus `aria-invalid`). Styled with shadcn tokens (border-input, ring, background, muted-foreground, destructive) for automatic light/dark theming, and ships with zero dependencies beyond your cn util.

npx shadcn@latest add https://pulld.pages.dev/r/floating-label-input.json
3selectedDeleteClear

Bulk Action Bar

The bar that appears once rows are selected in a table or list — it shows "3 items selected", holds your bulk actions (delete, archive, export, assign, move, approve, mark as read) as children, and gives the user a way out of selection mode. Use it with any multi-select surface: a data table with row checkboxes, an admin users/orders/invoices list, a file manager or media library, an inbox, a moderation queue, or a mobile-style edit mode. Common asks it answers: "bulk action bar", "bulk actions toolbar", "selection toolbar", "batch actions", "n selected bar", "contextual action bar", "what to show when table rows are checked". shadcn/ui ships no such component — its data-table recipe leaves selected-row UI entirely to you; this packages the part everyone rewrites. It hides itself at count 0, so you render it unconditionally and just pass the selected count. The count is announced to screen readers from a live region that stays mounted even at zero (a live region inserted together with its text is not reliably announced, so a bar that unmounts completely would swallow the first update), and the visible count is aria-hidden to avoid a double read. Escape clears the selection, ignoring already-handled key presses so a dialog opened from one of your actions still closes normally. It is a labelled region, not an ARIA toolbar, because a toolbar is expected to implement roving-tabindex arrow navigation and claiming the role without it reads worse than plain tab order. Pass variant="floating" (default) for a pinned bar above the page bottom or "inline" to sit in the flow above the table; itemName/itemNamePlural handle the noun and irregular plurals. Styled with shadcn tokens (background, border, muted-foreground, accent, ring) for automatic light/dark theming, with zero dependencies beyond your cn util. Distinct from toast, which reports a result after the fact; this one hosts the actions themselves.

npx shadcn@latest add https://pulld.pages.dev/r/bulk-action-bar.json
Saving…Saved 2 min ago

Save Status

composes time-ago

The small inline "Saving… / Saved 2 minutes ago / Couldn't save · Retry" indicator that sits beside an autosaving surface. Use it wherever edits persist in the background instead of behind a Save button: a document, note, or rich-text editor, a settings or profile page that saves on blur, a draft post or email composer, a form with debounced autosave, a spreadsheet-style inline-edit table, or a builder/canvas. Common asks it answers: "autosave indicator", "saving spinner next to the title", "all changes saved", "draft saved status", "last saved timestamp", "Google-Docs-style save state", "how to show saving/saved/error". shadcn/ui ships nothing for this — you'd hand-roll the state wording, the spinner, and the announcement each time. You pass one `status` prop (idle | saving | saved | error): idle renders nothing visible, so you can render it unconditionally and just mirror your mutation state (react-query isPending/isError, a useActionState, or your own flag). Pass `savedAt` and the "Saved" text is followed by a live relative timestamp that keeps itself fresh — it composes the time-ago component rather than freezing a string that goes stale while the tab sits open. Pass `onRetry` and the error state grows a Retry button. Accessibility is the fiddly part it gets right: the wording lives in an always-mounted role=status region so the very first transition is actually announced, while the ticking timestamp and the Retry label sit outside it — inside, the timer would make the page announce "Saved 3 minutes ago" every minute unprompted. It stays polite rather than assertive, because aria-live is honoured at registration time and a failed autosave should not cut across someone mid-sentence. All labels are overridable for i18n. Styled with shadcn tokens (muted-foreground, destructive, ring) so it follows light/dark themes. Distinct from toast, which pops a transient message after an action, and from spinner or loading-button, which cover a single in-flight request: this one is the persistent status of a background save.

npx shadcn@latest add https://pulld.pages.dev/r/save-status.json
2 problemsEnter your emailChoose a password

Form Error Summary

The block that appears above a form after a failed submit — "There are 3 problems with your submission" followed by one link per error that jumps focus straight to the field it came from. Use it on any form long enough that the broken field can be off screen: signup and checkout, account or billing settings, a multi-step wizard, an onboarding or application form, an admin create/edit page, or anywhere a server action returns field errors. Common asks it answers: "error summary", "validation summary", "show all form errors at the top", "list validation errors with links to fields", "focus the first invalid field on submit", "accessible form errors", "GOV.UK-style error summary", "react-hook-form errors object to a summary". shadcn/ui's form ships per-field messages only — the summary, the focus move, and the field links are left to you, and they are the parts that decide whether a keyboard or screen-reader user can actually find what broke. Pass an `errors` array of `{ fieldId, message }` mapped straight from react-hook-form's formState.errors, a zod flatten(), or a server action's fieldErrors; an empty array renders nothing, so it can sit in the JSX unconditionally. Give it `focusKey={formState.submitCount}` and a second submit that fails identically still announces. Accessibility is the whole point: it announces by moving focus to a container labelled by its heading, rather than through a live region — a live region reads the messages but leaves focus behind, so the links the user needs are somewhere they must go hunting for, and doing both reads everything twice. Each message links to its field and focuses it on click, falling back to the first focusable control inside when the id names a wrapper (radio group, checkbox group, custom combobox); errors with no fieldId render as plain text for form-level failures like a declined card. The container uses a plain focus ring, not focus-visible, because focus arrives programmatically and browsers do not reliably paint it otherwise. headingLevel keeps the heading in your page outline. Styled with shadcn destructive/ring tokens for light and dark themes; lucide-react is the only dependency. Distinct from toast, which pops a transient message, and from an inline field message, which only helps once you have already found the field.

npx shadcn@latest add https://pulld.pages.dev/r/form-error-summary.json
Type acme-prod
acme-pro
Delete

Type-to-Confirm

The confirmation step in front of an irreversible action: the user has to type the resource's own name ("acme-prod") before the destructive button turns on. Use it wherever a misclick would be unrecoverable — deleting a project, repository, workspace, organisation, cluster, database, or environment, removing a team member, revoking an API key, wiping data, cancelling a subscription, or any "danger zone" section of a settings page. Common asks it answers: "type to confirm", "type the project name to delete", "type DELETE to confirm", "confirm delete by typing name", "GitHub-style delete confirmation", "danger zone dialog", "destructive action modal", "disable the delete button until the name matches". shadcn/ui ships alert-dialog as an empty shell — the typed match, the disabled-until-it-matches wiring, and the announcement are left to you every time; this packages them into one drop-in that sits inside your existing dialog or card, so nothing here assumes which official components you have installed. Pass `phrase` (the name) and `onConfirm`; both sides are trimmed before comparing, so a pasted name that picked up a trailing space still matches, and an empty phrase never matches, which stops an untouched field from arming a delete. Set `caseSensitive={false}` to let "delete" pass for "DELETE", and mirror your mutation with `pending` to lock the field and swap the button label. The field opts out of autocomplete, autocorrect, autocapitalisation, and spellcheck — on a phone the first letter would otherwise be capitalised and an exact match made impossible to type. The button is genuinely disabled rather than aria-disabled, which screen readers skip: nothing is lost by that, because the label states what to type, the description explains that the button is waiting for it, and an always-mounted live region announces the moment it turns on (a live region inserted together with its text is not reliably announced, so one that appeared only on match would swallow that update). It renders as a real <form>, so Enter submits and, inside a dialog, focus lands on the field on open with no extra wiring. Styled with shadcn tokens (border-input, destructive, muted-foreground, ring) for automatic light/dark theming, with zero dependencies beyond your cn util. Distinct from confirm-button, which is a two-step click for cheap, reversible actions; this is the high-friction guard for the ones you cannot take back.

npx shadcn@latest add https://pulld.pages.dev/r/type-to-confirm.json
Shortcuts×SearchKNew issueC

Keyboard Shortcuts

composes kbd

The help sheet that opens when the user presses ? — a modal listing every keyboard shortcut in the app, grouped by area, with the key caps drawn per platform. Use it as soon as an app has shortcuts worth discovering: an editor, inbox or mail client, issue tracker, admin dashboard, IDE-like tool, dev tool, chat or any keyboard-first product where power users expect ? to explain itself. Common asks it answers: "keyboard shortcuts dialog", "keyboard shortcuts modal", "shortcuts help sheet", "press ? to see shortcuts", "shortcut cheat sheet", "hotkey list", "keymap overlay", "GitHub/Gmail/Linear-style shortcuts help", "show all hotkeys", "⌘K help screen". shadcn/ui ships nothing for this and its kbd is a bare key cap, so the sheet, the grouping, the ?-to-open wiring and the cross-platform key rendering are hand-rolled every time. Pass a `shortcuts` array of `{ keys, description, group? }`; groups render in the order they first occur, so the array is the outline. Write `"Mod"` in keys and it renders ⌘ on Apple platforms and Ctrl everywhere else — one source of truth instead of a Mac branch through your docs — and the literal token `"then"` renders as text rather than a cap so chords read as G then P. It documents shortcuts rather than binding them: your app already owns the handlers, and a component that registered them too would fight whatever hotkey library you use. The only key it owns is the one that opens it, and that listener ignores presses while focus is in an input, textarea, select or contenteditable, so typing "?" in a message box does not throw a modal over the composer. Platform detection runs in an effect, not during render — `navigator` does not exist on the server, so an inline branch would crash SSR or hydrate to different markup than it sent. Accessibility is the part that is easy to get wrong: the key caps are aria-hidden and each row carries an sr-only spoken form, because a screen reader meeting ⌘ announces "place of interest sign" or nothing at all, so the row reads "Open search, Command K"; opening moves focus into the dialog, which is what announces it, instead of a live region that would read the whole sheet twice; Tab is trapped, Escape closes, focus returns to whatever was focused before, and the scrolling list is itself focusable so a long list can be scrolled from the keyboard. Composes the kbd component for the caps. Styled with shadcn tokens (popover, muted-foreground, ring, border) for light and dark themes; lucide-react is the only dependency. Distinct from command-palette, which is a ⌘K launcher for running commands: this one is the reference card that tells users the shortcuts exist.

npx shadcn@latest add https://pulld.pages.dev/r/keyboard-shortcuts.json
logo.png62%data.csv×

Upload List

The list of files under a dropzone or file picker — one row each with the file name, its size, a progress bar while it uploads, an error with a retry button when it fails, and an X to drop it from the queue. Use it on any screen that accepts files: an attachment picker, an image or avatar upload, a CSV/spreadsheet import step, a document or PDF upload, a bulk media drop, or an import wizard. Common asks it answers: "file upload list", "upload queue", "show selected files with progress", "file list with remove button", "upload progress bar per file", "attachment list", "retry failed upload", "Dropbox/Gmail-style upload rows". shadcn/ui ships no file upload of any kind, and its progress primitive is a single bar with no notion of a file, so the row layout, the byte formatting, the per-file progress and the failure affordance are hand-rolled every time. It pairs with the file-dropzone component, which hands you a File[] and deliberately stops there: this is the half that shows what happened to those files. Pass an `items` array of `{ id, name, size?, status, progress?, error? }` where status is pending | uploading | done | error; omit `progress` and the bar goes indeterminate for uploads with no known length, and an empty array renders nothing so you can mount it unconditionally next to your queue state. It is presentational on purpose and never uploads anything — you keep the requests, the concurrency, the cancellation and the retry policy, and pass `onRemove`/`onRetry` to get the buttons. Accessibility is where a queue usually goes wrong and this one is built around it: progress sits in a role=progressbar, which is not a live region, so a file crawling from 1% to 100% does not narrate every tick; instead an always-mounted role=status region announces only the rows that just finished or just failed, batched into one message per change; the first render is treated as the starting state, so a list that mounts with finished rows stays silent; and every remove/retry button carries the file name in its accessible name, because a column of buttons all called "Remove" is unusable without sight of the row. Sizes are formatted to KB/MB/GB with tabular numerals, long names truncate with a title tooltip, and it is styled with shadcn tokens (muted-foreground, destructive, primary, accent, ring) so it follows light and dark themes; lucide-react is the only dependency. Distinct from save-status, which is a one-line indicator for a single background save, and from progress-ring, which is one circular meter: this is the multi-file queue.

npx shadcn@latest add https://pulld.pages.dev/r/upload-list.json

Bento Grid

The asymmetric panel grid behind most modern feature sections: a set of cards on one grid where a few cells are deliberately two columns wide or two rows tall, so the section reads as a composition instead of a row of identical boxes. Use it for a landing page features section, a product tour, a "why us" grid, a homepage hero collage, a portfolio or an app-store style showcase. Common asks it answers: "bento grid", "bento box layout", "bento cards", "bento section", "bento blocks", "features bento", "grid layout with different sized cards", "Apple/Linear-style feature grid", "masonry-ish marketing grid", "asymmetric card grid". shadcn/ui ships no layout components at all, so this grid is hand-rolled every time — and hand-rolling it fails in one specific way that is hard to spot: Tailwind only emits classes it can find written out in your source, so the natural `className={`col-span-${n}`}` compiles to nothing and every cell silently renders one column wide in the production build while looking correct in dev. This component keeps every span it can emit as a literal class in a lookup table, so `colSpan={2}` and `rowSpan={2}` survive the compiler. Two parts: `BentoGrid` takes `columns` (2, 3 or 4) and steps up from a single column on phones rather than fixing a track count, and `BentoGridItem` is the panel surface plus its span controls. Rows are sized minmax(11rem, auto) so equal cells line up and a tall cell is visibly twice the height; and because every layout is two columns at md and only widens at lg, each span is capped per breakpoint to the tracks that tier actually has — CSS Grid answers an over-wide span by adding an auto column, not by clamping it, and the first cell to land in that phantom column is sized by its own content. It is layout only and renders no card content of its own, so a cell can hold copy, an image, a chart or a feature-card. The grid deliberately does not use grid-auto-flow: dense — dense packing lets a later cell backfill an earlier gap, which leaves the Tab order and a screen reader reading the section in a different order than the eye sees it — and the cell is a plain div rather than a list item, so your own headings keep the document outline. No state, no effects and no "use client", so it renders as a server component, and it has zero npm dependencies; the surface uses shadcn tokens (card, card-foreground, border) so it follows light and dark themes. Distinct from feature-card, which is the icon/title/description content of one cell: this is the grid the cells sit on.

npx shadcn@latest add https://pulld.pages.dev/r/bento-grid.json
srcappindex.tspublic

Tree View

The nested list you can open, close and walk with the arrow keys: a file explorer or file tree, a folder or directory tree, a category or taxonomy picker, an org chart, a JSON or API-schema browser, a docs sidebar with nested sections. Common asks it answers: "tree view", "tree component", "file tree", "folder tree", "directory tree", "file explorer sidebar", "nested list with expand/collapse", "collapsible tree", "recursive tree from JSON", "expandable folder list", "VS Code-style explorer", "category tree", "org chart tree". shadcn/ui ships no tree of any kind — its collapsible is one open/closed section and its sidebar nests menus without the tree semantics — so this gets hand-rolled every time, and the part that gets dropped is always the keyboard. Pass a `data` array of `{ id, label, children?, icon? }`: a node with a `children` array is a parent (an empty array is an empty folder, which still opens), a node without one is a leaf. Open state and selection are each controlled (`expandedIds` / `selectedId` plus `onExpandedChange` / `onSelect`, which hands you the whole node) or uncontrolled (`defaultExpandedIds` / `defaultSelectedId`), so it drops into a router-driven sidebar or runs on its own. It is the real ARIA tree pattern, not a pile of nested collapsibles: role=tree / treeitem / group with aria-expanded, aria-selected and aria-level/posinset/setsize, and a roving tabindex so the whole tree is one Tab stop instead of one stop per row. Up/Down walk only the rows actually on screen, Right opens a parent and then steps into it, Left closes it or jumps out to the parent, Home/End hit the ends, Enter/Space select, and type-ahead jumps to the next row starting with what you typed (repeat a letter to cycle). Three details that are easy to get wrong are handled: closing a subtree that contains the focused row hands focus back to the row being closed instead of dropping it on <body>; the row is named by its own label via aria-labelledby, because a treeitem owns its child group and a name computed from contents would read the entire subtree as one row's name; and the disclosure arrow is a click target rather than a nested <button>, since a treeitem must not contain its own focusable elements. Renders folder/file icons by default (`showIcons={false}` for category or org trees), `indent` sets the per-level offset, and per-node `icon` overrides a single row. Styled with shadcn tokens (accent, muted-foreground, ring) so it follows light and dark themes; lucide-react is the only dependency, with no Radix and no state library. Distinct from command-palette, which is a flat searchable launcher: this is for structure you navigate rather than a name you already know.

npx shadcn@latest add https://pulld.pages.dev/r/tree-view.json

Sortable List

Drag-to-reorder list: grab a row's grip handle and drop it in a new place. Reach for it whenever the order itself is the data — reordering tasks or a to-do list, ranking priorities or search results, arranging table columns, form fields, dashboard widgets, nav or sidebar links, playlist tracks, image or gallery order, question order in a quiz, steps in a workflow or recipe, or the cards inside one kanban column. Common asks it answers: "sortable list", "drag and drop list", "reorderable list", "drag to reorder", "drag handle list", "reorder items react", "sortable without dnd-kit", "react-beautiful-dnd replacement", "draggable list order", "move item up and down". shadcn/ui ships nothing that reorders — there is no sortable, no draggable, no dnd primitive anywhere in the catalog — so this is hand-rolled every time, and the half that gets dropped is always the keyboard. Here the whole interaction works without a mouse: Tab reaches the list once (roving tabindex), arrow keys walk it, Space or Enter picks a row up, arrows move the picked-up row, Space or Enter drops it, Escape puts it back where it started, and each step is spoken through an assertive live region ("Picked up Design review. Position 2 of 5."). Every announcement is overridable through `labels` for other languages. Dragging is plain pointer events — no dnd-kit, no react-dnd, no HTML5 drag-and-drop — so touch works, rows are measured once per drag and displaced with transforms, and rows of different heights land exactly where they look like they will. Controlled: pass `items` (`{ id, label }` plus whatever else you carry) and persist the array `onReorder` hands back; `renderItem` draws the row body beside the handle. Depends only on lucide-react and your cn util.

npx shadcn@latest add https://pulld.pages.dev/r/sortable-list.json
Load more

Infinite Scroll

The footer of a list that keeps going: an invisible sentinel that loads the next page as it scrolls into view, plus a Load more button that always does the same job by hand. Reach for it on a feed or timeline, search results, a notification or activity list, a product or photo grid, a comment thread, chat history, an audit log, or any 'show more' at the end of a long table. Common asks it answers: "infinite scroll", "infinite scrolling react", "load more on scroll", "load more button", "endless scroll", "auto load next page", "IntersectionObserver load more", "react-infinite-scroll-component alternative", "scroll pagination", "fetch next page when the sentinel is visible", "lazy load a long list". shadcn/ui's pagination is numbered page links and nothing else — it renders no rows and loads nothing — so progressive loading gets hand-rolled every time, and the same three things break. Here the page footer stays reachable, because automatic loading yields to the button after autoLoadLimit pages (default 3, and a press grants another run) instead of running the page away from whatever is below the list. Each page is announced through a polite live region ("20 more items loaded. 60 in total.") rather than rows appearing in silence, and the button uses aria-disabled instead of disabled so pressing it never drops focus out of the list. And a failed page stops the sentinel and offers Retry instead of hammering a broken endpoint in a loop. Return a promise from onLoadMore and the duplicate-fire guard is exact; a loader that only bumps a page number is held until the list actually changes, so it asks once instead of firing a burst. A first page shorter than the viewport keeps loading until the viewport is full — the usual bug there is a sentinel that never leaves the screen, so no second intersection event ever comes and the list stops loading forever. Controlled: pass hasMore, itemCount and onLoadMore, and render it directly after your rows — it draws no list of its own, so it works the same under a ul, a table or a grid. Optional loading for react-query or SWR, error for your own failure state, root for a list that scrolls inside a box rather than the page, rootMargin (default 200px) to prefetch early, auto={false} for button-only, and labels to reword or translate every string. Styled with shadcn tokens so it follows light and dark themes; lucide-react is the only dependency, with no Radix and no scroll library.

npx shadcn@latest add https://pulld.pages.dev/r/infinite-scroll.json

Virtual List

A long list that only puts the rows you can see into the DOM: five thousand rows render as about thirty nodes, so the page stops taking seconds to paint and scrolling stops stuttering. Reach for it on an admin table or data grid, a log, audit or event viewer, chat and message history, search results over a big local array, a file or asset browser, a select with thousands of options, or any list where you already hold every row in memory. Common asks it answers: "virtual list react", "virtualized list", "windowing", "react-window alternative", "react-virtualized alternative", "TanStack Virtual without the wiring", "render 10000 rows react", "long list is slow to render", "list virtualization with dynamic row heights", "variable height virtual list", "scroll performance long list", "only render visible items". shadcn/ui has no virtualization at all — its table renders every row you hand it — so this gets wired up by hand against TanStack Virtual or react-window each time, and the same four things break. Focus survives here: the row you tabbed into stays mounted after it scrolls out of the window, instead of being unmounted under you and dropping focus to the top of the page. Screen readers get the real position, because every row carries aria-posinset and aria-setsize — "item 4,213 of 5,000", not a count of the handful that happen to be mounted — and the spacer that holds the scroll height is marked presentational so the list and its items stay related. The view does not jump: rows are measured as they mount with a ResizeObserver, and when a row above the viewport turns out taller than the estimate, or older rows are prepended, the scroll offset is corrected against a row-keyed anchor in a layout effect, before the browser paints. That anchor is why prepending older chat messages keeps the message you were reading exactly where it was. And positions can be restored, via defaultScrollOffset plus a ref handle with scrollToIndex(index, "auto" | "start" | "center" | "end"), scrollToOffset and getScrollOffset. Rows may be any height and nothing has to be declared up front; estimateItemHeight (default 48) is only the guess used before a row has been measured, and overscan (default 4) sets how many rows are kept mounted beyond the edges. Controlled by count plus a render function — children is called with an index, so the data can live anywhere — with itemKey for stable identity, onScroll and empty. Defaults to role list/listitem; pass role="listbox" and itemRole="option" when the rows are selectable. Set the height with className (the default is h-72); rows are absolutely positioned, so give them padding rather than a vertical margin. Vertical only, and find-in-page reaches mounted rows only, which is inherent to windowing. Styled with shadcn tokens so it follows light and dark themes, and it ships with no dependencies at all — no Radix, no virtualization library.

npx shadcn@latest add https://pulld.pages.dev/r/virtual-list.json
Show more

Read More

Long text clamped to a few lines with a Show more / Show less toggle that appears only when the text is genuinely too long. Use it wherever text is usually short but occasionally is not: product and marketplace listing descriptions, comments, reviews and replies, user bios and profile blurbs, release notes and changelog entries, incident and error detail, log lines, AI answers and summaries, job posts, FAQ answers, and long cells in a card or table. Common asks it answers: "read more button", "show more / show less", "expandable text", "truncate text with a show more link", "line clamp with toggle", "collapsible paragraph", "see more link", "clamp description to 3 lines", "react-show-more-text alternative", "text truncation with expand". shadcn/ui ships nothing for this, and its collapsible is a different thing — a generic open/close container whose trigger is always there and which does no clamping — so the genuinely awkward part is left to you: deciding whether the toggle should exist at all. This measures the rendered text and renders the control only when the clamped box actually overflows, so a list of mostly-short entries does not sprout a pointless "Show more" under every one of them. It re-measures when the column resizes and the text rewraps, and again once web fonts have loaded, because a clamped box keeps its height while the line count underneath it changes; an element that is off screen in a closed tab or accordion measures zero, which it treats as "unknown" rather than "it fits", so the toggle is not dropped while the text is out of view. The clamp is applied as inline style rather than Tailwind's line-clamp-N utility, because `lines` is a runtime value and a dynamic `line-clamp-${n}` class is invisible to Tailwind's scanner — it would work in dev and silently vanish from the production build. Accessibility is where the hand-rolled version usually goes wrong: the full text always stays in the DOM and is only clipped visually, so screen readers read all of it and find-in-page still reaches it, instead of the usual text.slice(0, 200) that destroys the content for everybody; the control is a real button carrying aria-expanded and aria-controls pointing at the text. Clipped is not hidden, so a link inside the invisible part is still in the tab order — focus landing there expands the block rather than letting the browser scroll the clamped box and shear the text mid-line. Collapsing pulls the block back into view when it has already scrolled off the top, so the reader is not dumped further down the page. Uncontrolled by default; pass expanded and onExpandedChange to drive it from an "expand all" control. Styled with shadcn tokens (ring, muted-foreground) so it follows light and dark themes, and ships with no dependencies beyond your own cn util.

npx shadcn@latest add https://pulld.pages.dev/r/read-more.json
quarterlyv3.xlsx

Middle Truncate

One line of text with the middle removed so that both ends stay readable, fitted to whatever width the container actually gives it. Use it for file names — where ordinary CSS truncation eats the extension and every row ends up reading "quarterly-report-2026-fin…" — and for file paths, URLs, S3 and storage keys, git SHAs and commit hashes, wallet and contract addresses, API keys and tokens, request, trace and session IDs, branch names, and any other identifier whose tail is the part that tells two of them apart. Common asks it answers: "truncate the middle of a string", "middle ellipsis", "truncate a filename but keep the extension", "ellipsis in the middle of text", "shorten a wallet address to 0x1234…abcd", "truncate a long path from the middle", "text-overflow ellipsis but centered", "abbreviate a long ID", "react-middle-truncate alternative". CSS cannot do this — text-overflow: ellipsis only ever cuts the end — and shadcn/ui ships nothing for it, so it is normally hand-rolled as a fixed character count that is wrong at every container width except the one it was tuned for. This measures the rendered text against the box it has to fit and binary-searches the cut point, so it fills the space exactly; it re-measures when the column resizes and again once web fonts have loaded, because a font swap changes every glyph width without changing the box. It cuts on grapheme boundaries using Intl.Segmenter, so an emoji, flag or accented letter landing on the cut does not become a replacement glyph the way a raw slice() would — which matters more here than elsewhere, since the cut point moves every time the container resizes. The full string stays in the DOM and only the visible copy is shortened: screen readers get the whole value instead of "0x4f2a ellipsis 91bc", find-in-page still matches it, and selecting the line copies the full text exactly once rather than the shortened form. Hovering shows the full value as a tooltip. It takes its width from its container — a flex row, a grid track, or a fixed width — and needs no min-w-0 to shrink; inside a shrink-to-fit parent there is nothing to fit to, so it simply renders in full.

npx shadcn@latest add https://pulld.pages.dev/r/middle-truncate.json
On this pageInstallationUsageOptionsAPI

Table of Contents

A table of contents for the page the reader is on, with the section they are currently reading highlighted as they scroll. Use it for the "On this page" rail beside documentation and guides, API references, changelogs and release notes, long blog posts and tutorials, handbooks, legal and policy pages, and reports. Common asks it answers: "table of contents component", "toc sidebar", "on this page nav", "scrollspy", "scroll spy in React", "highlight the active heading while scrolling", "docs right rail", "anchor link navigation", "in-page navigation", "sticky table of contents", "MDX toc", "react-scrollspy alternative". shadcn/ui ships nothing for this, and its navigation-menu and sidebar are for moving between pages, not around one. You pass the headings in as items — the shape rehype-slug, MDX and Contentlayer pipelines already hand you — so the list is rendered on the server and the links work before, and without, JavaScript; only the highlight needs the client. The awkward part is deciding which heading counts as current, and this fixes the three ways a hand-rolled one gets it wrong. The last section is normally shorter than the viewport, so its heading never reaches the activation line and the final entry can never light up — reaching the bottom of the scrollable area selects the last heading, because there is nothing further to read. A section stays current while it is being read rather than only while its heading is on screen, which is where an IntersectionObserver checking is-it-visible goes blank on any section taller than the window. And clicking an entry starts a scroll lasting hundreds of milliseconds, during which every heading it travels past would light up in turn, leaving the entry you clicked as the one thing not highlighted; the list holds your choice until the scroll settles, and hands control straight back if you grab the page mid-flight. offset clears a sticky site header, both for where a click lands and for where the current section begins, since the browser's own fragment jump puts the heading underneath it. It re-measures on resize and once web fonts have loaded, follows a nested scroller when the app shell scrolls an inner element instead of the window, and honours prefers-reduced-motion. The active entry is marked with aria-current="location" rather than colour alone, so it is announced and not merely seen; because clicking has to preventDefault to apply the offset, focus is moved to the heading the way the browser would have, so a keyboard reader lands in the section instead of carrying on down the contents. Modifier and middle clicks are left alone, so opening a section in a new tab still works.

npx shadcn@latest add https://pulld.pages.dev/r/toc.json
03/14/2026

Date Input

A date field you type into, one segment at a time, that emits an ISO "YYYY-MM-DD" string. Use it for date of birth and signup forms, booking and check-in/check-out dates, card expiry, invoice and due dates, report ranges, and admin filters — anywhere the reader already knows the date and wants to type it rather than hunt for it in a month grid. Common asks it answers: "date input", "date field", "typed date entry", "dd/mm/yyyy input", "segmented date field", "date of birth input", "birthday field", "keyboard accessible date picker", "date picker without a calendar", "react-day-picker alternative", "input type=date replacement", "styled native date input". shadcn/ui ships no typed date entry: its calendar is a month grid you click, and the Date Picker page composes that calendar into a popover behind a read-only trigger button, so the only keyboard route is arrow-keying around a grid; input-otp is segmented but for fixed-length codes with no date meaning. This is the typing half, and it composes with calendar rather than replacing it. The work is in the parts that are easy to get wrong. Segment order comes from the locale through Intl, so en-US renders month/day/year, en-GB and de-DE day/month/year, and ja-JP year/month/day, instead of the hardcoded M/D/Y that silently means the wrong day for most of the world; the calendar is pinned to Gregorian, so a Buddhist or Japanese-era locale cannot hand back the year 2569 or 8 to be emitted as though it were Gregorian. The day is clamped whenever the month or year changes, so January 31 switched to February becomes the 28th — or the 29th in a leap year, by the full 4/100/400 rule — instead of the silent rollover into March that a raw Date gives you. Auto-advance is decided by range rather than by a fixed two-digit count: typing 5 into the month jumps straight to the next segment because no month starts with 5, while 1 waits for a possible 10, 11 or 12, and a pair that cannot exist starts a new number instead of dropping the keystroke. Arrow keys step a segment, wrapping month and day, clamping the year, and seeding an empty segment from today; Backspace clears and steps back; Home, End and left/right move between segments. Each segment is a spinbutton with its own label and value range, and the month is announced by name rather than as a bare number. Values outside min/max are flagged with aria-invalid without ever blocking typing, the way a native date input behaves. Works controlled or uncontrolled, forwards a ref to the first segment so a shortcut can focus it, and mirrors the ISO value into a hidden input for native form submit. Theme-aware via shadcn tokens; no dependencies — no date library, no react-day-picker.

npx shadcn@latest add https://pulld.pages.dev/r/date-input.json
Requests · 30d

Sparkline

An inline SVG trend line — a sparkline — that shows the shape of a series in about the space of a line of text. Use it when you need a chart small enough to live inside something else: a 7-day or 30-day trend next to a KPI in a stat card or dashboard tile, a per-row usage or activity graph in a table (requests, spend, errors, signups, page views), a mini price or metric history, a tiny “last N days” graph in a list item, or any micro / inline / thumbnail chart where axes, gridlines, a legend and a tooltip would just be noise. It renders as a plain <svg> with no hooks, no state and no effects, so it works unchanged inside a React Server Component, in a static export, and with JavaScript disabled — there is no “use client” in the file. Different from shadcn/ui’s official chart, which is a ~10KB wrapper around Recharts (it declares recharts@2.15.4 as a dependency and also pulls in card) meant for full charts with axes, tooltips and legends: this is one zero-dependency file that draws a single path and needs nothing but your cn util. Different from gauge and progress-ring, which draw one current value as an arc rather than a series over time. It handles the parts hand-written sparklines get wrong: null, undefined and NaN entries are treated as gaps that keep their slot on the x axis and break the line, instead of being dropped (which slides the rest of the series sideways) or drawn as zero (which invents a crash that is not in the data); a flat series is centred rather than dividing by zero and emitting a NaN path that silently renders nothing at all; the plot area is inset by half the stroke so the highest and lowest points are not sliced in half by the viewport edge; vector-effect=“non-scaling-stroke” keeps the line an even weight when the SVG is stretched across a wide table cell, and the last-value dot is drawn as a round line cap so it stays a circle instead of being squashed into an ellipse by that same stretch. Pass min and max to pin the scale so a whole column of sparklines is actually comparable — autoscale every row to its own extremes and they all end up looking like the same shape. It also ships an aria-label generated from the data (“12 points, up from 3 to 91, low 3, high 94”), where shadcn’s own chart.tsx sets no role=“img” or aria-label of its own; pass your own aria-label to override it, or aria-hidden when a surrounding stat card already announces the number. Props: data, width, height, min, max, strokeWidth, area, showLast, formatValue.

npx shadcn@latest add https://pulld.pages.dev/r/sparkline.json
My Post!
/blog/my-post

Slug Input

A URL slug field that fills itself in from a title and then gets out of the way. Use it wherever a record needs a URL: the permalink or slug field in a blog post editor or CMS admin form, a page or docs route segment, a product handle, a workspace or team URL, a category or tag slug, a public profile handle. Type a title, watch the slug appear as kebab-case, and edit it whenever you want — this is the part hand-rolled fields get wrong. It keeps deriving only while the field still holds exactly what it generated, so editing the slug by hand, or loading an existing slug from your database, stops the derivation for good: renaming a published post cannot silently change its URL. Leave the field empty and blur, and it goes back to following the title. Every keystroke is sanitised in place — lowercased, spaces and punctuation collapsed to a single hyphen (or underscore), accents folded away — while the caret stays exactly where you were typing, which is what breaks when you naively assign a transformed value back to a controlled input. Unicode is handled rather than mangled: NFKD folding turns "Café au lait" into cafe-au-lait, "Łódź" into lodz, and the letters decomposition leaves whole are spelled out ("Straße" becomes strasse, not strae). Pass allowUnicode to keep the title's own script instead — without it a Japanese, Chinese, Korean, Greek, Cyrillic, Hebrew or Arabic title slugifies to an empty string, and with it combining marks stay attached to their letter, so がっこう does not quietly become かっこう. maxLength cuts a generated slug back to a whole word rather than mid-syllable, apostrophes disappear instead of splitting words ("don't panic" becomes dont-panic), and pasting a full URL takes just its last path segment. Zero dependencies, one import, shadcn tokens, optional prefix like example.com/blog/ wired to the input with aria-describedby. Official shadcn/ui has no slug or permalink field — its input is a bare element and input-group is an assembly kit with no logic in it — and a slugify npm package solves the string, not the field: the caret, the do-not-stomp rule and the typing-in-progress state are what this component is.

npx shadcn@latest add https://pulld.pages.dev/r/slug-input.json
••••••••
Fair

Password Strength

A password strength meter for a sign-up, registration, change-password or reset-password form — the bar under the password field that says Weak or Strong, plus one line saying why. It scores how many guesses a password would survive rather than ticking off one uppercase, one number, one symbol: composition rules push people toward Password1!, which is guessed instantly, and reject correct horse battery staple, which is not. NIST SP 800-63B says the same — screen against known-bad passwords and let length do the work. It detects the things that make a password look random without being random: entries from a built-in list of the passwords that top every breach dump (folded through leet substitutions, so P@ssw0rd is found where password is, and unaffected by capitalisation), repeated characters, runs through the alphabet or the digits, runs along a keyboard row, and years and dates. The check hand-rolled meters always miss is userInputs: pass the email, username, display name or your product name and Acme2026! stops scoring as strong on acme.com — including the joined-up forms that separators hide, so Acme Co catches acmeco. Pass blocklist to add your own breach list on top; the built-in one is deliberately small, because a real one is megabytes and belongs behind an API. estimatePasswordStrength is exported on its own, pure and synchronous, so the same score that draws the meter can disable your submit button or drive a zod refine — no async, no 800 kB zxcvbn bundle, no dependencies at all. Accessibility is the other half: the bars are a role="meter" with aria-valuetext, and only the band name sits in the aria-live region, so a screen reader hears "Weak" once when the password crosses a band instead of being read to on every keystroke — which is what an aria-live wrapped around the whole widget does. The advice line is tied to the meter with aria-describedby instead. Warnings and suggestions come back as stable codes with an overridable message table, so the meter translates. It uses no hooks, so it renders in a server component and needs no "use client" of its own. Official shadcn/ui has nothing for passwords — no meter, no blocklist, no scorer; its input is a bare element and field and input-group are assembly kits with no logic in them.

npx shadcn@latest add https://pulld.pages.dev/r/password-strength.json
retries: 3-timeout: 30+timeout: 60debug: off

Diff View

A line-by-line diff of two strings — the before/after view a screen needs when it has to show what changed: a config or settings change, a record edited in an admin panel, a document revision, a webhook payload against the last one, an audit-log entry, a restored backup next to what is live, or the edit an AI agent is proposing before the user accepts it. Pass before and after and it renders a git-style diff, unified by default or side by side with view="split". Unchanged lines collapse into a counted gap, so a 400-line file with a three-line change shows three lines and a summary instead of 400; context sets how many surrounding lines survive and context={Infinity} shows the whole text. Both line-number gutters are select-none, so selecting the diff copies the code and not a column of numbers. CRLF and LF are folded together, because a file that changed only its line endings would otherwise report every single line as rewritten. It is meaning-first rather than colour-first: every changed row carries a + or - sign and a screen-reader-only "Added line:" / "Removed line:" prefix, so the diff still reads for someone who cannot tell the red and green backgrounds apart — conveying the change by colour alone, which fails WCAG 1.4.1, is the single most common defect in a hand-rolled diff. The table also gets an sr-only caption stating how many lines were added and removed. The diff is a longest-common-subsequence over lines with the shared prefix and suffix trimmed off first, which keeps a large document with a small edit fast (a 4,000-line file with one changed line diffs in well under a millisecond) and makes an appended line read as appended instead of shifting everything by one. Pathologically large inputs degrade to "this block was replaced" rather than allocating a table of hundreds of megabytes during a render. No dependencies, no diff library and no hooks, so it renders inside a React server component without a "use client" of its own and ships no client JavaScript — which is the common case, because the text being compared has usually just been fetched on the server. Official shadcn/ui has no diff component of any kind: table is an unstyled table and chart is a Recharts wrapper, and neither computes or displays a change.

npx shadcn@latest add https://pulld.pages.dev/r/diff-view.json

Calendar Heatmap

A year of daily counts as a grid of shaded squares — the GitHub-style contribution graph, drawn for whatever your app counts per day: commits, deploys, orders, sign-ins, posts, workouts, lessons, support tickets, API calls, or a habit tracker's streak. Pass data as [{ date: '2026-08-10', count: 12 }] and it renders 53 columns of 7 squares with month and weekday headers; sparse data is fine, since a day with no row is drawn as a day with nothing, and two rows for the same day are summed rather than one silently winning. Shading is by quartile of the days that had any activity, so a single 500-commit day does not flatten the rest of the year into one pale block the way scaling against the maximum does — pass thresholds to cut the levels yourself. Dates are held as integers, days since the epoch in UTC, and never as Date objects: new Date('2026-08-10').getDay() is parsed as UTC midnight and answers with the previous day anywhere west of Greenwich, which silently rotates the whole grid by one row, and it is the single most common defect in a hand-rolled contribution graph. Nothing reads the clock either — the window is anchored to the last date in your data, not to Date.now(), so the server and the browser always render the same markup. It is a real table with month columns, weekday row headers and a screen-reader name on every square ('12 commits on Monday, August 10, 2026'), so the year is readable to somebody who cannot tell the four shades apart; conveying a value by colour alone fails WCAG 1.4.1, and the colour key is hidden from assistive technology instead of being announced as five unlabelled swatches. The grid scrolls horizontally on a narrow screen and takes keyboard focus, because a scrollable region that cannot be reached by keyboard puts a year of data out of reach. Shades are one opacity ramp of your theme's primary token, so it follows light and dark without a palette of its own. No dependencies and no hooks, so it renders inside a React server component with no 'use client' of its own and ships no client JavaScript. Official shadcn/ui has nothing that draws this: calendar is a react-day-picker date picker for choosing a day (it pulls in date-fns and the button component), and chart is a Recharts wrapper — neither plots a value per calendar day.

npx shadcn@latest add https://pulld.pages.dev/r/calendar-heatmap.json
$npm run build✓ built in 1.2s✗ 2 errorsexit code 1

ANSI Log

Renders raw terminal output — escape sequences and all — as styled, theme-aware HTML. Reach for it wherever a process's own output has to be shown inside a page: CI and build logs, deploy and release output, npm/pnpm/cargo/docker build output piped into a dashboard, test-runner results, job and worker logs in an admin panel, an agent or LLM tool-call transcript, git and lint output in a code-review UI, or the output pane of a web terminal. Common asks it answers: "ansi to html react", "render ANSI colours in the browser", "ci log viewer component", "terminal output component react", "build log with colours", "convert ANSI escape codes", "shadcn log viewer", "docker logs in a web UI", "colored console output in React". It handles the parts a hand-rolled converter gets wrong. A carriage return moves the cursor instead of breaking the line, so a progress bar that redraws itself stays one line reading "100%" rather than turning into a hundred lines of noise — and the tail a shorter redraw does not cover survives, exactly as on a real terminal. Erase-in-line (all three modes), backspace, the 16 named colours, the full 256-colour palette including the 6x6x6 cube and the 24-step grey ramp, 24-bit truecolor, both the semicolon and the colon spelling of extended colour that libvte and kitty emit, bold, dim, italic, underline, strike and reverse video. Every sequence it does not implement — cursor moves, hide-cursor, alternate-screen, window-title OSC — is consumed rather than printed as visible gibberish, which is the usual failure of a parser that only knows about the colour sequence. OSC 8 hyperlinks keep their label and drop their target deliberately: a URL in a log is exactly as attacker-supplied as the log is, and turning it into a live anchor would put javascript: one click away. Colour follows your theme instead of a fixed terminal palette — the 16 named colours are light/dark pairs chosen against the panel, and backgrounds are drawn as a translucent wash rather than a solid block, so text can never land on a saturated slab below contrast in one theme or the other. The scroll region takes keyboard focus, since a log that only scrolls with a mouse puts the right-hand end of every long line out of reach. The optional line-number gutter is not selectable, so dragging across the log copies the log and not a column of numbers, and maxLines keeps the end of the log — the failure is at the bottom — while saying out loud how many earlier lines it dropped instead of quietly presenting a suffix as the whole thing. No dependencies and no hooks, so it renders inside a React server component with no "use client" of its own and ships no client JavaScript. shadcn/ui has nothing of the kind: there is no log, terminal or ANSI item in its registry, and a plain code block shows escape sequences as literal characters.

npx shadcn@latest add https://pulld.pages.dev/r/ansi-log.json
0 9 * * 1-5At 09:00, Mon–FriThu 09:00 UTCFri 09:00 UTC

Cron Expression

Turns a cron expression into a sentence anyone can read, and lists the next times it fires. Reach for it wherever a schedule is shown rather than edited: a scheduled-jobs table in an admin panel, the summary line under a cron input, backup and report-delivery settings, sync and webhook retry schedules, a CI/CD or deploy cadence, a GitHub Actions / Vercel Cron / Cloudflare Workers Triggers schedule rendered in your own dashboard, or the live preview beside a cron builder. Common asks it answers: "cron to human readable react", "explain a cron expression", "crontab parser component", "describe cron in plain English", "next run time from a cron expression", "cron preview shadcn", "validate a cron expression in a form", "what does 0 9 * * 1-5 mean". It handles the parts a hand-rolled parser gets wrong. The day-of-month and day-of-week fields are ORed when neither is a literal star and ANDed when either one is — so 0 0 13 * 5 runs on the 13th OR on every Friday, not only on Friday the 13th, and 0 0 13 * 0-6 runs every single day even though 0-6 covers the same seven days a star does. That rule is cron's oldest trap, it keys off syntax rather than coverage, and the component both applies it and says so on screen when it is in play. Sunday is both 0 and 7, and the fold happens after a range is expanded, so 5-7 means Friday, Saturday and Sunday instead of collapsing into a backwards range. Ranges, lists, steps, */n, a-b/n, the n/step shorthand, three-letter month and weekday aliases in any position, and the @daily / @hourly / @weekly / @monthly / @yearly / @midnight nicknames all parse; @reboot is reported as having no calendar schedule rather than being invented one; a six- or seven-field expression is named as Quartz/Spring syntax rather than dismissed as invalid, and L, W and # are named as Quartz extensions. Next runs are computed in UTC — the zone GitHub Actions, Vercel Cron and Cloudflare Triggers all schedule in — by stepping whichever field fails rather than a minute at a time, so an expression that only matches on February 29 costs a few thousand comparisons instead of two million, and one that can never match (February 30) ends empty instead of hanging. Run times are formatted without Intl, because a locale-dependent string renders differently on the server and in the browser and turns into a hydration mismatch; pass formatRun to localise it yourself. Nothing reads the clock, so the same props always produce the same markup. An invalid expression is reported inline, in words as well as in colour, with the field and token that failed — conveying state by colour alone fails WCAG 1.4.1 — and the parse helpers (parseCron, describeCron, nextCronRuns) are exported so the same expression can be validated in a form before it is saved. No dependencies and no hooks, so it renders inside a React server component with no 'use client' of its own and ships no client JavaScript. Official shadcn/ui has nothing for scheduling: calendar is a date picker built on react-day-picker, and progress is a bar with no notion of recurrence.

npx shadcn@latest add https://pulld.pages.dev/r/cron-expression.json
Images46%Video30%Other24%

Ratio Bar

One horizontal bar that shows how a whole is divided up, with a legend that names every part — the GitHub-style language / storage bar. Reach for it whenever the question is "what is this made of?" rather than "how far along is it?": disk or storage usage broken down by file type, a plan or quota bar (seats used, API calls, build minutes, bandwidth), a budget or spend breakdown by category, traffic by source or device, test results split into passed / failed / skipped, a portfolio or vote split, tickets by status, or a repository language bar. Common asks it answers: "stacked bar component react", "percentage breakdown bar shadcn", "storage usage bar", "disk usage breakdown", "quota / capacity bar", "segmented progress bar", "share of total bar", "distribution bar", "usage meter with legend", "percentages that add up to 100". Pass `parts` as `{ label, value }` objects in any unit you like — bytes, requests, dollars — and only the ratios are used. Add `total` to switch from "parts of a whole" to "used out of a capacity": the gap is drawn as empty track and listed as its own row (rename it with `remainderLabel`, or pass `null` to draw it without listing it). `precision` adds decimals, `formatValue` puts the raw figure next to each share, `showLegend={false}` keeps the legend for screen readers only, and each part takes a `className` for its colour (the default is a ramp of your primary colour, which is theme-aware in any shadcn project; pass `bg-chart-1`…`bg-chart-5` or your own classes for distinct hues). It handles the parts a hand-rolled version gets wrong. The percentages are apportioned by largest remainder rather than rounded one at a time, so three equal parts read 34 / 33 / 33 instead of 33 / 33 / 33 and the column always totals exactly 100. A part too small to round to a whole percent reads "<1%" rather than the lie "0%", and a part that is nearly but not quite everything reads ">99%" rather than "100%". Tiny slices keep a two-pixel minimum so they stay visible without stealing width from the rest, while a part worth exactly zero draws nothing at all and is still listed. Negative, NaN and Infinity values count as zero instead of collapsing the layout. The legend names and quantifies every part, so nothing is carried by colour alone (WCAG 1.4.1) and the bar itself is aria-hidden. No hooks and no clock: it renders inside a React server component with no "use client" of its own, ships no client JavaScript, and produces identical markup on the server and in the browser. `ratioPercents` is exported for the same figures in a table or tooltip. Official shadcn/ui has nothing for this: progress is a single value with no parts, and chart is a Recharts wrapper for plotted series rather than one inline bar with no dependencies.

npx shadcn@latest add https://pulld.pages.dev/r/ratio-bar.json

Scroll Progress

A bar that fills as the reader scrolls — the reading indicator across the top of an article, and the "how much is left?" cue on anything long. Use it on blog posts and long-form articles, documentation pages, guides and tutorials, changelogs and release notes, terms / privacy / policy pages, onboarding and multi-section landing pages, reports, and long forms or checkout flows where the reader wants to know how much further there is to go. Common asks it answers: "reading progress bar", "scroll progress bar react", "scroll indicator component", "article reading progress", "page scroll percentage", "Medium-style progress bar", "progress bar at top of page on scroll", "how far down the page has the user scrolled", "scroll-linked progress indicator", "blog reading indicator", "useScrollProgress hook", "track scroll position in React". Drop in `<ScrollProgress className="fixed inset-x-0 top-0 z-50" />` for the classic placement, or render it as an ordinary block under a sticky header. Pass `target={articleRef}` when progress should mean "through this article" rather than "down this page" — on a page that continues into related posts, a comment thread or a tall footer, a whole-page bar is still short of the end when the article has actually been read, and a tracked element fills exactly as the last line arrives. `indicatorClassName` styles the filled part; the track and fill use your `--muted` and `--primary` tokens, so both themes follow automatically with no hardcoded colours. It settles the details a hand-rolled version gets wrong. Measurement is throttled to one requestAnimationFrame per scroll burst and quantised before it reaches state, so a flick that moves the bar by less than a fifth of a pixel re-renders nothing. Content that grows after first paint — an image finishing decoding, a lazily loaded section, an accordion opening, a web font swapping in — is picked up through a ResizeObserver and `document.fonts.ready`, where a scroll-and-resize-only implementation keeps reporting the old page height. A tracked element inside an app shell that scrolls its own `<main>` instead of the window is measured against that scroller, not the viewport, which is the layout where a naive bar sits frozen. When the content already fits on screen the bar reads full rather than empty, because everything there is to read is visible — the usual choice of 0 leaves a permanently empty bar on every short page, which looks broken rather than finished. The first paint is server-safe: it renders an empty bar on the server and takes its real measurement in a layout effect before the browser paints, so there is no hydration mismatch and no visible jump on a page restored mid-scroll. Decorative by design — the scrollbar already tells assistive technology where the reader is, and a `role="progressbar"` updating every frame of a scroll is announced as a stream of numbers over whatever is being read, so the element is `aria-hidden` instead of noisy. `useScrollProgress` is exported for indicators this component does not draw (a percentage in the header, a circular ring, chapter markers) so they share one number instead of a second implementation that disagrees at the edges. No dependencies beyond React. Official shadcn/ui has nothing scroll-aware: its progress is a Radix bar you drive with a value you already have, not one derived from the reader's position.

npx shadcn@latest add https://pulld.pages.dev/r/scroll-progress.json
1h 30m1 hour 30 minutes

Duration Input

A text field that takes a length of time written the way people actually write one — 90m, 1h30m, 1h 30m, 2d 4h 15m, 1:30, 1.5h, 500ms, "90 minutes" — reads it into milliseconds, and echoes the reading back in words underneath it ("1 hour 30 minutes") so the interpretation is never left to be guessed at. Reach for it wherever a form asks how long rather than when: a request timeout or deadline, a cache TTL or expiry, session and token lifetimes, a retry or backoff interval, a polling or refresh interval, an SLA target, a job or cron timeout, an auto-logout window, a rate-limit window, a task estimate, a video or audio length, a snooze or reminder delay. Common asks it answers: "duration input", "duration picker", "time duration field", "timeout input", "TTL input", "interval input", "parse 1h30m", "hh:mm:ss input", "humanize duration" — the field otherwise assembled from a number box beside a unit <select>, or from parse-duration / pretty-ms / ms / humanize-duration. Official shadcn/ui has no duration component of any kind: input is a bare text box you would still have to parse, input-otp is for codes, and calendar answers which day, not how long. It settles the two things hand-rolled duration parsers get wrong. First, m versus ms: the whole run of letters is read before anything is looked up, so 500ms can never come out as 500 minutes. Second, what 1:30 means: two colon fields are read as mm:ss and three as hh:mm:ss, the way stopwatches and media players write them, and blur rewrites the entry into its canonical short form so 1:30 visibly becomes 1m 30s. Months and years are refused by name instead of being given an invented length, which also settles the usual M/m argument — parsing is case-insensitive and M is minutes. Beyond parsing: minMs/maxMs mark the field aria-invalid with a polite live message naming the bound in words, a value that is unusable or out of range is withheld from onValueChange so nothing handed to the caller needs validating twice, text that does not parse stays on screen instead of being deleted out from under the reader, and giving the field a name posts the milliseconds through a hidden input so the server is never handed prose. parseDuration and formatDuration are exported as plain functions for the rest of the app to share. One file, themed with shadcn tokens, no dependencies beyond React.

npx shadcn@latest add https://pulld.pages.dev/r/duration-input.json

Hosted service

Pro blocks

Composed, opinionated blocks built from the free atoms — a license unlocks install. One-time, $39.

Dashboard Overview PRO

composes stat-card, avatar-stack, empty-state, theme-toggle, loading-button

A complete, responsive dashboard overview section: a header with team avatars, theme toggle, and a primary action, a 4-up stat-card row, and a recent-activity card with an empty-state fallback. Composes pulld's stat-card, avatar-stack, empty-state, theme-toggle, and loading-button so it drops into any shadcn project. Pro block.

npx shadcn@latest add "https://pulld.pages.dev/r/pro/dashboard-overview.json?key=YOUR_KEY"
Get a license — $39 one-time