Typed, accessible, theme-aware React/Tailwind components installable by the shadcn CLI or your AI coding agent. Free atoms plus composed Pro blocks.
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.
uiThe asymmetric panel grid behind most modern feature sections: a set of cards on one grid where a few cells are deliberately two columns wide or two rows tall, so the section reads as a composition instead of a row of identical boxes. Use it for a landing page features section, a product tour, a "why us" grid, a homepage hero collage, a portfolio or an app-store style showcase. Common asks it answers: "bento grid", "bento box layout", "bento cards", "bento section", "bento blocks", "features bento", "grid layout with different sized cards", "Apple/Linear-style feature grid", "masonry-ish marketing grid", "asymmetric card grid". shadcn/ui ships no layout components at all, so this grid is hand-rolled every time — and hand-rolling it fails in one specific way that is hard to spot: Tailwind only emits classes it can find written out in your source, so the natural `className={`col-span-${n}`}` compiles to nothing and every cell silently renders one column wide in the production build while looking correct in dev. This component keeps every span it can emit as a literal class in a lookup table, so `colSpan={2}` and `rowSpan={2}` survive the compiler. Two parts: `BentoGrid` takes `columns` (2, 3 or 4) and steps up from a single column on phones rather than fixing a track count, and `BentoGridItem` is the panel surface plus its span controls. Rows are sized minmax(11rem, auto) so equal cells line up and a tall cell is visibly twice the height; and because every layout is two columns at md and only widens at lg, each span is capped per breakpoint to the tracks that tier actually has — CSS Grid answers an over-wide span by adding an auto column, not by clamping it, and the first cell to land in that phantom column is sized by its own content. It is layout only and renders no card content of its own, so a cell can hold copy, an image, a chart or a feature-card. The grid deliberately does not use grid-auto-flow: dense — dense packing lets a later cell backfill an earlier gap, which leaves the Tab order and a screen reader reading the section in a different order than the eye sees it — and the cell is a plain div rather than a list item, so your own headings keep the document outline. No state, no effects and no "use client", so it renders as a server component, and it has zero npm dependencies; the surface uses shadcn tokens (card, card-foreground, border) so it follows light and dark themes. Distinct from feature-card, which is the icon/title/description content of one cell: this is the grid the cells sit on.
uiA year of daily counts as a grid of shaded squares — the GitHub-style contribution graph, drawn for whatever your app counts per day: commits, deploys, orders, sign-ins, posts, workouts, lessons, support tickets, API calls, or a habit tracker's streak. Pass data as [{ date: '2026-08-10', count: 12 }] and it renders 53 columns of 7 squares with month and weekday headers; sparse data is fine, since a day with no row is drawn as a day with nothing, and two rows for the same day are summed rather than one silently winning. Shading is by quartile of the days that had any activity, so a single 500-commit day does not flatten the rest of the year into one pale block the way scaling against the maximum does — pass thresholds to cut the levels yourself. Dates are held as integers, days since the epoch in UTC, and never as Date objects: new Date('2026-08-10').getDay() is parsed as UTC midnight and answers with the previous day anywhere west of Greenwich, which silently rotates the whole grid by one row, and it is the single most common defect in a hand-rolled contribution graph. Nothing reads the clock either — the window is anchored to the last date in your data, not to Date.now(), so the server and the browser always render the same markup. It is a real table with month columns, weekday row headers and a screen-reader name on every square ('12 commits on Monday, August 10, 2026'), so the year is readable to somebody who cannot tell the four shades apart; conveying a value by colour alone fails WCAG 1.4.1, and the colour key is hidden from assistive technology instead of being announced as five unlabelled swatches. The grid scrolls horizontally on a narrow screen and takes keyboard focus, because a scrollable region that cannot be reached by keyboard puts a year of data out of reach. Shades are one opacity ramp of your theme's primary token, so it follows light and dark without a palette of its own. No dependencies and no hooks, so it renders inside a React server component with no 'use client' of its own and ships no client JavaScript. Official shadcn/ui has nothing that draws this: calendar is a react-day-picker date picker for choosing a day (it pulls in date-fns and the button component), and chart is a Recharts wrapper — neither plots a value per calendar day.
uiInline two-step confirmation on the button itself: the first click arms it — the label swaps to "Confirm?" and the button turns destructive — and only the second click calls onConfirm. It disarms itself after a timeout (3s by default) and on blur, so a stray double-click, a scroll away, or a Tab out can never fire the action. Reach for it wherever a modal would outweigh the action: a delete or remove button in a table row, a card, a list item or a toolbar; removing a member or collaborator; revoking an API key, token or session; disconnecting an integration; clearing a cache or a log; unsubscribing; resetting a filter or a setting; discarding a draft; leaving a channel. Common asks it answers: "confirm before delete without a dialog", "two-step delete button react", "click twice to confirm", "are you sure button shadcn", "inline confirmation button", "destructive button with confirmation", "delete button in a table row", "undo-less delete confirmation". Props: `onConfirm` (fired only on the confirming click), `confirmText` for the armed label, `timeout` in milliseconds, plus everything else a `<button>` takes. It is one real `<button>` element — Enter and Space confirm it, `disabled` is honoured, your `className` is merged through `cn`, and it defaults to `type="button"` so an armed click inside a form cannot submit it by accident. The armed state is exposed as `data-armed` for styling and announced through a polite live region, so the change is never carried by colour and label alone. Theme-aware through the destructive tokens, no dependencies, and `"use client"` because it holds a state and a timer. What official shadcn/ui offers instead is alert-dialog: a modal that pulls in @radix-ui/react-alert-dialog, renders through a portal, traps focus and needs open state wired up — right for a page-level, consequential confirmation, heavy for a row-level one. Official button has a destructive variant but no confirmation behaviour at all. For an action severe enough that a second click is not enough — deleting a production project or an account — use type-to-confirm, which makes the user type the name first.
uiTurns 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.
uiA 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.
uiA line-by-line diff of two strings — the before/after view a screen needs when it has to show what changed: a config or settings change, a record edited in an admin panel, a document revision, a webhook payload against the last one, an audit-log entry, a restored backup next to what is live, or the edit an AI agent is proposing before the user accepts it. Pass before and after and it renders a git-style diff, unified by default or side by side with view="split". Unchanged lines collapse into a counted gap, so a 400-line file with a three-line change shows three lines and a summary instead of 400; context sets how many surrounding lines survive and context={Infinity} shows the whole text. Both line-number gutters are select-none, so selecting the diff copies the code and not a column of numbers. CRLF and LF are folded together, because a file that changed only its line endings would otherwise report every single line as rewritten. It is meaning-first rather than colour-first: every changed row carries a + or - sign and a screen-reader-only "Added line:" / "Removed line:" prefix, so the diff still reads for someone who cannot tell the red and green backgrounds apart — conveying the change by colour alone, which fails WCAG 1.4.1, is the single most common defect in a hand-rolled diff. The table also gets an sr-only caption stating how many lines were added and removed. The diff is a longest-common-subsequence over lines with the shared prefix and suffix trimmed off first, which keeps a large document with a small edit fast (a 4,000-line file with one changed line diffs in well under a millisecond) and makes an appended line read as appended instead of shifting everything by one. Pathologically large inputs degrade to "this block was replaced" rather than allocating a table of hundreds of megabytes during a render. No dependencies, no diff library and no hooks, so it renders inside a React server component without a "use client" of its own and ships no client JavaScript — which is the common case, because the text being compared has usually just been fetched on the server. Official shadcn/ui has no diff component of any kind: table is an unstyled table and chart is a Recharts wrapper, and neither computes or displays a change.
uiA text field that takes a length of time written the way people actually write one — 90m, 1h30m, 1h 30m, 2d 4h 15m, 1:30, 1.5h, 500ms, "90 minutes" — reads it into milliseconds, and echoes the reading back in words underneath it ("1 hour 30 minutes") so the interpretation is never left to be guessed at. Reach for it wherever a form asks how long rather than when: a request timeout or deadline, a cache TTL or expiry, session and token lifetimes, a retry or backoff interval, a polling or refresh interval, an SLA target, a job or cron timeout, an auto-logout window, a rate-limit window, a task estimate, a video or audio length, a snooze or reminder delay. Common asks it answers: "duration input", "duration picker", "time duration field", "timeout input", "TTL input", "interval input", "parse 1h30m", "hh:mm:ss input", "humanize duration" — the field otherwise assembled from a number box beside a unit <select>, or from parse-duration / pretty-ms / ms / humanize-duration. Official shadcn/ui has no duration component of any kind: input is a bare text box you would still have to parse, input-otp is for codes, and calendar answers which day, not how long. It settles the two things hand-rolled duration parsers get wrong. First, m versus ms: the whole run of letters is read before anything is looked up, so 500ms can never come out as 500 minutes. Second, what 1:30 means: two colon fields are read as mm:ss and three as hh:mm:ss, the way stopwatches and media players write them, and blur rewrites the entry into its canonical short form so 1:30 visibly becomes 1m 30s. Months and years are refused by name instead of being given an invented length, which also settles the usual M/m argument — parsing is case-insensitive and M is minutes. Beyond parsing: minMs/maxMs mark the field aria-invalid with a polite live message naming the bound in words, a value that is unusable or out of range is withheld from onValueChange so nothing handed to the caller needs validating twice, text that does not parse stays on screen instead of being deleted out from under the reader, and giving the field a name posts the milliseconds through a hidden input so the server is never handed prose. parseDuration and formatDuration are exported as plain functions for the rest of the app to share. One file, themed with shadcn tokens, no dependencies beyond React.
uiThe 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.
uiThe 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.
uiThe footer of a list that keeps going: an invisible sentinel that loads the next page as it scrolls into view, plus a Load more button that always does the same job by hand. Reach for it on a feed or timeline, search results, a notification or activity list, a product or photo grid, a comment thread, chat history, an audit log, or any 'show more' at the end of a long table. Common asks it answers: "infinite scroll", "infinite scrolling react", "load more on scroll", "load more button", "endless scroll", "auto load next page", "IntersectionObserver load more", "react-infinite-scroll-component alternative", "scroll pagination", "fetch next page when the sentinel is visible", "lazy load a long list". shadcn/ui's pagination is numbered page links and nothing else — it renders no rows and loads nothing — so progressive loading gets hand-rolled every time, and the same three things break. Here the page footer stays reachable, because automatic loading yields to the button after autoLoadLimit pages (default 3, and a press grants another run) instead of running the page away from whatever is below the list. Each page is announced through a polite live region ("20 more items loaded. 60 in total.") rather than rows appearing in silence, and the button uses aria-disabled instead of disabled so pressing it never drops focus out of the list. And a failed page stops the sentinel and offers Retry instead of hammering a broken endpoint in a loop. Return a promise from onLoadMore and the duplicate-fire guard is exact; a loader that only bumps a page number is held until the list actually changes, so it asks once instead of firing a burst. A first page shorter than the viewport keeps loading until the viewport is full — the usual bug there is a sentinel that never leaves the screen, so no second intersection event ever comes and the list stops loading forever. Controlled: pass hasMore, itemCount and onLoadMore, and render it directly after your rows — it draws no list of its own, so it works the same under a ul, a table or a grid. Optional loading for react-query or SWR, error for your own failure state, root for a list that scrolls inside a box rather than the page, rootMargin (default 200px) to prefetch early, auto={false} for button-only, and labels to reword or translate every string. Styled with shadcn tokens so it follows light and dark themes; lucide-react is the only dependency, with no Radix and no scroll library.
uiThe 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.
uiOne line of text with the middle removed so that both ends stay readable, fitted to whatever width the container actually gives it. Use it for file names — where ordinary CSS truncation eats the extension and every row ends up reading "quarterly-report-2026-fin…" — and for file paths, URLs, S3 and storage keys, git SHAs and commit hashes, wallet and contract addresses, API keys and tokens, request, trace and session IDs, branch names, and any other identifier whose tail is the part that tells two of them apart. Common asks it answers: "truncate the middle of a string", "middle ellipsis", "truncate a filename but keep the extension", "ellipsis in the middle of text", "shorten a wallet address to 0x1234…abcd", "truncate a long path from the middle", "text-overflow ellipsis but centered", "abbreviate a long ID", "react-middle-truncate alternative". CSS cannot do this — text-overflow: ellipsis only ever cuts the end — and shadcn/ui ships nothing for it, so it is normally hand-rolled as a fixed character count that is wrong at every container width except the one it was tuned for. This measures the rendered text against the box it has to fit and binary-searches the cut point, so it fills the space exactly; it re-measures when the column resizes and again once web fonts have loaded, because a font swap changes every glyph width without changing the box. It cuts on grapheme boundaries using Intl.Segmenter, so an emoji, flag or accented letter landing on the cut does not become a replacement glyph the way a raw slice() would — which matters more here than elsewhere, since the cut point moves every time the container resizes. The full string stays in the DOM and only the visible copy is shortened: screen readers get the whole value instead of "0x4f2a ellipsis 91bc", find-in-page still matches it, and selecting the line copies the full text exactly once rather than the shortened form. Hovering shows the full value as a tooltip. It takes its width from its container — a flex row, a grid track, or a fixed width — and needs no min-w-0 to shrink; inside a shrink-to-fit parent there is nothing to fit to, so it simply renders in full.
uiA password strength meter for a sign-up, registration, change-password or reset-password form — the bar under the password field that says Weak or Strong, plus one line saying why. It scores how many guesses a password would survive rather than ticking off one uppercase, one number, one symbol: composition rules push people toward Password1!, which is guessed instantly, and reject correct horse battery staple, which is not. NIST SP 800-63B says the same — screen against known-bad passwords and let length do the work. It detects the things that make a password look random without being random: entries from a built-in list of the passwords that top every breach dump (folded through leet substitutions, so P@ssw0rd is found where password is, and unaffected by capitalisation), repeated characters, runs through the alphabet or the digits, runs along a keyboard row, and years and dates. The check hand-rolled meters always miss is userInputs: pass the email, username, display name or your product name and Acme2026! stops scoring as strong on acme.com — including the joined-up forms that separators hide, so Acme Co catches acmeco. Pass blocklist to add your own breach list on top; the built-in one is deliberately small, because a real one is megabytes and belongs behind an API. estimatePasswordStrength is exported on its own, pure and synchronous, so the same score that draws the meter can disable your submit button or drive a zod refine — no async, no 800 kB zxcvbn bundle, no dependencies at all. Accessibility is the other half: the bars are a role="meter" with aria-valuetext, and only the band name sits in the aria-live region, so a screen reader hears "Weak" once when the password crosses a band instead of being read to on every keystroke — which is what an aria-live wrapped around the whole widget does. The advice line is tied to the meter with aria-describedby instead. Warnings and suggestions come back as stable codes with an overridable message table, so the meter translates. It uses no hooks, so it renders in a server component and needs no "use client" of its own. Official shadcn/ui has nothing for passwords — no meter, no blocklist, no scorer; its input is a bare element and field and input-group are assembly kits with no logic in them.
uiOne 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.
uiLong 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.
uiThe small inline "Saving… / Saved 2 minutes ago / Couldn't save · Retry" indicator that sits beside an autosaving surface. Use it wherever edits persist in the background instead of behind a Save button: a document, note, or rich-text editor, a settings or profile page that saves on blur, a draft post or email composer, a form with debounced autosave, a spreadsheet-style inline-edit table, or a builder/canvas. Common asks it answers: "autosave indicator", "saving spinner next to the title", "all changes saved", "draft saved status", "last saved timestamp", "Google-Docs-style save state", "how to show saving/saved/error". shadcn/ui ships nothing for this — you'd hand-roll the state wording, the spinner, and the announcement each time. You pass one `status` prop (idle | saving | saved | error): idle renders nothing visible, so you can render it unconditionally and just mirror your mutation state (react-query isPending/isError, a useActionState, or your own flag). Pass `savedAt` and the "Saved" text is followed by a live relative timestamp that keeps itself fresh — it composes the time-ago component rather than freezing a string that goes stale while the tab sits open. Pass `onRetry` and the error state grows a Retry button. Accessibility is the fiddly part it gets right: the wording lives in an always-mounted role=status region so the very first transition is actually announced, while the ticking timestamp and the Retry label sit outside it — inside, the timer would make the page announce "Saved 3 minutes ago" every minute unprompted. It stays polite rather than assertive, because aria-live is honoured at registration time and a failed autosave should not cut across someone mid-sentence. All labels are overridable for i18n. Styled with shadcn tokens (muted-foreground, destructive, ring) so it follows light/dark themes. Distinct from toast, which pops a transient message after an action, and from spinner or loading-button, which cover a single in-flight request: this one is the persistent status of a background save.
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.
uiSearch field with a leading magnifier icon and a trailing clear (✕) button that appears as soon as there is text, empties the field, and puts focus back so typing can continue. Use it above filterable lists and data tables, in sidebars and settings pages, over dropdown and combobox options, for docs and help search, for admin record lookup, and anywhere a "/" shortcut focuses a search box. Common asks it answers: "search input", "search bar", "search box", "filter input", "clearable input", "input with a clear button", "search field with icon", "type to filter a list", "table search box", "searchbar component". shadcn/ui has no search field: its input is a bare styled <input>, and its input-group is a layout kit of six parts (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) that pulls in button, input and textarea and hands you slots to hang your own icon and clear control in — you still write the clear button, the show-it-only-when-there-is-text rule, the refocus, and the event plumbing. This is that already assembled, in one import. The part that is easy to get wrong is clearing. Assigning to the input's value does not make React's onChange fire, so a hand-rolled clear button empties the box while the list behind it stays filtered on the old query. This writes through the native value setter and dispatches a bubbling input event, so onChange fires for controlled and uncontrolled usage alike and whatever filtering it drives actually updates. It also hides the WebKit search-cancel button so there is not a second ✕ beside the first, keeps the icon out of the accessibility tree and out of pointer events, gives the clear control a screen-reader label, and passes only one of value/defaultValue through so React never warns about a field switching between controlled and uncontrolled. The clear button is deliberately left out of the tab order, so Tab moves on to the next field instead of into a control that duplicates select-all-and-delete. Standard input props and a forwarded ref pass straight through, so a "/" hotkey can focus it. Theme-aware via shadcn tokens; depends only on lucide-react.
uiA 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.
uiA URL slug field that fills itself in from a title and then gets out of the way. Use it wherever a record needs a URL: the permalink or slug field in a blog post editor or CMS admin form, a page or docs route segment, a product handle, a workspace or team URL, a category or tag slug, a public profile handle. Type a title, watch the slug appear as kebab-case, and edit it whenever you want — this is the part hand-rolled fields get wrong. It keeps deriving only while the field still holds exactly what it generated, so editing the slug by hand, or loading an existing slug from your database, stops the derivation for good: renaming a published post cannot silently change its URL. Leave the field empty and blur, and it goes back to following the title. Every keystroke is sanitised in place — lowercased, spaces and punctuation collapsed to a single hyphen (or underscore), accents folded away — while the caret stays exactly where you were typing, which is what breaks when you naively assign a transformed value back to a controlled input. Unicode is handled rather than mangled: NFKD folding turns "Café au lait" into cafe-au-lait, "Łódź" into lodz, and the letters decomposition leaves whole are spelled out ("Straße" becomes strasse, not strae). Pass allowUnicode to keep the title's own script instead — without it a Japanese, Chinese, Korean, Greek, Cyrillic, Hebrew or Arabic title slugifies to an empty string, and with it combining marks stay attached to their letter, so がっこう does not quietly become かっこう. maxLength cuts a generated slug back to a whole word rather than mid-syllable, apostrophes disappear instead of splitting words ("don't panic" becomes dont-panic), and pasting a full URL takes just its last path segment. Zero dependencies, one import, shadcn tokens, optional prefix like example.com/blog/ wired to the input with aria-describedby. Official shadcn/ui has no slug or permalink field — its input is a bare element and input-group is an assembly kit with no logic in it — and a slugify npm package solves the string, not the field: the caret, the do-not-stomp rule and the typing-in-progress state are what this component is.
uiAn 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.
ui