Registry Index

Search the catalog

Type to search items across every indexed registry, or pick a page.

ComponentReact1 dependency

Sortable List

sortable-list

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

Live

Starting the live preview…

Runs in a sandboxed frame on a separate origin. Mounted with no props — some items need input to show anything.

@pulld

More from @pulld

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.

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.

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.

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.

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.

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

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.

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.