Virtual List
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.
More from @pulld
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.
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.
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.
The single-number tile at the top of a dashboard: a label, one big value, and an optional percentage change with an up or down arrow — green when the number moved the right way, red when it did not. Use it wherever a screen opens with a row of headline figures: an analytics or metrics dashboard, an admin overview, a KPI or scorecard row, a billing and usage summary, a SaaS home screen, a revenue or traffic report. Common asks it answers: "stat card", "metric card", "KPI card", "dashboard stat tile", "number card with percentage change", "revenue card with trend arrow", "analytics summary cards", "stats row", "show total users with growth", "Stripe/Vercel-style dashboard tiles". shadcn/ui ships card as an empty container with no notion of a metric, so the value typography, the delta colouring and the arrow are hand-rolled on every dashboard. Pass `label`, `value` and optionally `delta` (a number: positive renders the up arrow, negative the down arrow, and omitting it renders no delta at all) plus a `hint` line for the comparison period, e.g. "vs. last month". `value` is a ReactNode, not a string, so a pre-formatted currency or an Intl.NumberFormat result drops straight in and the component never guesses at your locale or currency. The direction is not left to colour alone: the arrow is aria-hidden and an sr-only "Up"/"Down" is spoken before the number, so the tile still means something to a screen reader and to a red-green colour-blind reader, which a bare green percentage does not. Composes into a responsive grid to form the stats row, and pairs with gauge and progress-ring when the figure is a ratio rather than a total. Styled with shadcn tokens (card, muted-foreground) with an explicit dark-mode pair for the delta colours; lucide-react is the only dependency. Distinct from feature-card, which sells a capability with an icon and copy: this one carries a live number.
A text input whose label rests inside the field like a placeholder, then shrinks and floats up onto the top border the moment the field is focused or has a value (the Material "outlined" floating-label pattern). Use it anywhere you want a compact, self-labelling field: a login or sign-up form (email, password), a settings or profile form, a contact or checkout form, a search or filter panel, or any dense form where separate labels above every input would waste vertical space. Common asks it answers: "floating label input", "animated / material label", "label that moves up on focus", "placeholder that turns into a label", "outlined text field", "MUI TextField for shadcn". shadcn/ui ships no floating-label field — you'd otherwise wire the placeholder-shown CSS onto its Input by hand; this packages it and keeps it a real, accessible input. The float is driven purely by CSS (`:placeholder-shown` / `:focus` on the peer input) with no JS state, so it works for controlled or uncontrolled inputs, survives browser autofill, and is correct before hydration with no first-focus jump. The label is a genuine `<label htmlFor>` (not a fake overlay): the `id` defaults to a stable React.useId() value so the association always holds and clicking the label focuses the input. It forwards a ref to the underlying <input>, so every native prop works unchanged — `type` (email/password/tel/url…), `name`, `value`/`onChange`, `required`, `disabled`, `autoComplete`, and form libraries like react-hook-form. Pass `error` to show an invalid state (destructive border, ring, and label, plus `aria-invalid`). Styled with shadcn tokens (border-input, ring, background, muted-foreground, destructive) for automatic light/dark theming, and ships with zero dependencies beyond your cn util.
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.
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.
Auto-updating relative timestamp — "3 minutes ago", "just now", "in 2 days" — that re-renders on a timer so the label stays fresh without a reload. Use it wherever a raw date would be noise and recency is what matters: comment/post/message timestamps, a notification or activity feed, "last seen"/"last updated"/"last synced" labels, commit or deploy history, table rows (created/modified), or a chat's message time. shadcn/ui ships no time-ago/relative-time component. Pass date as a Date, an ISO string, or epoch milliseconds. Wording comes from the platform's own Intl.RelativeTimeFormat, so it localizes for free via the locale prop and reads correctly for both past and future times; set numeric="auto" to get "yesterday"/"tomorrow" instead of "1 day ago", and format to "short" or "narrow" for compact "3 min. ago"/"3m ago". Anything newer than justNowThreshold seconds (default 45) shows justNowLabel ("just now"). The tick rate adapts — every 15s while under a minute old, per-minute under an hour, then hourly — or pin it with updateInterval. Renders a semantic <time> element with a machine-readable dateTime and a title tooltip carrying the full localized date, and is SSR/hydration-safe. Theme-aware via shadcn's text-muted-foreground token; depends only on your cn util — no date library, no extra packages.