Typed, accessible, theme-aware React/Tailwind components installable by the shadcn CLI or your AI coding agent. Free atoms plus composed Pro blocks.
A searchable multi-select: choose several options from a list you define, each shown as a removable badge in the trigger. Reach for it wherever a field stores an array rather than one value: assigning tags, labels, topics or categories to a post, product, ticket or issue; picking assignees, reviewers, attendees, team members or recipients; granting permissions, roles, scopes or groups; filtering a table, dashboard or report by several statuses, owners, channels, regions or vendors; choosing skills, industries, languages, integrations or notification channels; deciding which columns a table shows; and the "applies to" row on a rule, policy, coupon or automation. Common asks it answers: "multi select", "multiselect", "shadcn multi select", "shadcn multiselect combobox", "select multiple react", "multiple select dropdown", "multi select with search", "multi select with badges", "checkbox dropdown", "react-select alternative", "cmdk multi select", "combobox multiple", "assignee picker", "tag select", "accessible multi select", "select several options". Official shadcn/ui has no multi-select of any shape: across all sixty-three of its components the word "multiple" does not appear once, and its select, native-select and combobox are single-value by construction. The two things it has that hold more than one answer are a different tool — checkbox is one control per option, and toggle-group takes Radix's type="multiple" but is a row of always-visible buttons. Both stop working somewhere around a dozen options; this is the control for the list that has to be searched, and it is the gap that sends people to react-select or a hand-rolled cmdk popover. Distinct from pulld's tag-input, and the pair is worth knowing: this picks from a fixed set of options you supply and returns their values, while tag-input lets someone type new free-text tags that did not exist before. Choose by whether an unknown answer is allowed. Built on the select-only combobox pattern rather than a div with click handlers: the trigger is role="combobox" with aria-expanded and aria-haspopup, the panel is an aria-multiselectable listbox, the highlighted row is tracked with aria-activedescendant, chosen rows carry aria-selected with a check, and every add and remove is announced through a polite sr-only live region — the part a hand-rolled version always omits, which leaves a screen-reader user with no confirmation that anything happened. The keyboard is complete: Enter, Space or Down opens; arrows move; Enter toggles; Escape closes; Backspace removes the last badge from the trigger, and again from an empty search box; each badge's × removes just that one and never opens the panel. Type in the built-in search box to filter, or pass hideSearch for a short list and the listbox itself takes focus and the arrow keys. Works controlled (`value` + `onChange`) or uncontrolled (`defaultValue`), always a string[] of option values. `max` caps the selection and disables the unchosen rows at the cap rather than silently ignoring a click; per-option `disabled` blocks one row; `placeholder`, `searchPlaceholder` and `emptyMessage` are yours. Themed with shadcn tokens (secondary badges, accent highlight, popover surface, ring), so it follows light and dark mode. No Radix and no cmdk — one file, lucide-react for the three icons and your own cn util.
An offline banner that is right about being offline: it tells someone the page has lost the network, and — the harder half — only tells them it is back once that has actually been verified. Reach for it wherever losing the connection loses work or misleads the reader: a long form, an editor or a checkout someone is mid-way through; a dashboard, wallboard or monitoring view whose numbers stop being true the moment the feed dies; a chat, inbox or collaborative document where silence reads as "nobody is talking"; a PWA, field app or point-of-sale used on a phone that drifts in and out of coverage; and any app that queues writes to flush on reconnect. Common asks it answers: "offline banner", "offline detection react", "network status component", "useOnline hook", "useNetworkStatus", "detect offline react", "navigator.onLine react", "connection lost banner", "reconnecting indicator", "internet connection detector", "react-detect-offline alternative", "shadcn offline banner", "no internet message component", "online offline event react", "captive portal detection", "heartbeat ping component". The reason to install one rather than write it is that the three-line version everyone writes is wrong, and wrong in the direction that matters. navigator.onLine does not report whether the internet works; it reports whether the machine has a network interface that is up. A laptop joined to a café or hotel access point whose portal has not been logged into reads online. So does one on Wi-Fi whose upstream has died, one behind a captive portal that answers for every server with its own login page, and one where DNS alone has stopped resolving. Every one of those is true while nothing whatsoever loads — so a banner built on that flag stays hidden through precisely the outages people complain about, and the window.addEventListener("online") that clears it fires when an interface came up, not when anything can be reached. The false direction is the trustworthy one: the browser is not wrong about having no interface at all. So this component believes false immediately and treats true as a claim to be checked, by actually asking the network for something. What that costs is kept honest, because a component that invents a request every few seconds forever is one people rip out. Mount does not probe — the page in front of the user arrived over the very network in question, and its own load is the freshest evidence there is. The online event starts a probe instead of being believed, and only the probe's answer clears the banner. The offline event lands immediately, with nothing to wait for. While unreachable, probes back off exponentially with jitter, because everyone whose access point rebooted starts their backoff on the same tick and would otherwise arrive back together at the worst possible moment — and they stop entirely when the interface itself is down, since there is nothing to ask and the event will say when there is. A hidden tab probes nothing at all and restarts on visibilitychange, which is also what catches the laptop that slept and woke up on a different network. Steady polling while everything is fine is off by default and there when a wallboard needs it. The probe itself is two details a hand-rolled fetch misses. It refuses to follow redirects, which is what turns a captive portal's 302-to-its-own-login-page back into the failure it is rather than a perfectly good 200. And it counts any HTTP response as reachable, a 404 or a 502 included: the question is whether packets get to a server and back, and a 404 answers it as well as a 200 does — which is why the default /favicon.ico is safe on a site that does not have one, and why a version checking res.ok reports such a site as permanently offline. A deadline is enforced too, because a black-holed connection does not fail, it hangs. Official shadcn/ui has nothing here: no offline, online or network item, and navigator.onLine, the online/offline events and any form of reachability check appear nowhere in its sixty-three components. Within pulld it is the detector, not another notifier — toast is the right home for "it worked / it failed" messages your code decides to send, while this one works out, on its own, whether the network is actually there. useNetworkStatus() is exported for a bar of your own design, or for pausing polling, disabling a submit button and flushing a queue on reconnect, and it hands back a check() to call the moment one of your own requests fails — a far better signal than any poll. checkReachable() and nextProbeDelay() are exported too. The wording sits in an always-mounted polite live region, because a live region inserted together with its text is not reliably announced and the banner would be silent for exactly the people who cannot see it. Every colour is a shadcn token, so it follows light and dark, and the whole thing is one file.
A number field with − and + stepper buttons either side of it. Reach for it wherever someone adjusts a number by one rather than typing it: a quantity picker in a cart, checkout or order form, seats, guests, rooms, tickets or attendees, a per-page or page-size control, retries, timeout or concurrency in a settings panel, font size, padding or spacing in an editor — anywhere you would otherwise reach for a bare <input type="number"> or a spinbutton. Common asks it answers: "number input react", "shadcn number input", "shadcn ui number field", "quantity selector react", "quantity picker component", "quantity input cart", "stepper input react", "react number stepper", "increment decrement input react", "numeric input with plus and minus buttons", "spinbutton react", "input type number without spinners", "hide number input arrows css", "remove spinner from input type number", "number input min max clamp react", "number input step decimal", "0.30000000000000004 step react", "floating point drift number input", "inputMode decimal react", "numeric keypad mobile input react", "number input react-hook-form", "number input controlled uncontrolled warning", "setting state from a button does not fire onChange", "react-number-format alternative", "数量入力 react", "プラスマイナス付き 数値入力", "数値ステッパー shadcn", "input type=number の矢印を消したい". It keeps type="number" underneath, so native validation and valueAsNumber still work, and fixes the parts of it that are unpleasant in practice: the browser's own spin buttons are hidden (they are inconsistent across browsers and absent on mobile) and replaced with real, theme-aware, keyboard-reachable ones, inputMode="decimal" brings up the numeric keypad on phones, and the value is tabular-nums so the digits do not jump as they change. Decimal steps do not drift: stepping by 0.1 counts the step's decimal places and rounds to them, so you get 0.3 rather than 0.30000000000000004. Each button clamps to min or max and disables itself once the value is at that bound, and the buttons are taken out of the tab order so Tab still lands on the field itself. The step is written through the input's native value setter and dispatches a real input event, which is the part that is easy to get wrong: onChange fires whether the field is controlled or uncontrolled, so react-hook-form, Formik and plain useState all see the change instead of silently missing button presses. Only one of value/defaultValue ever reaches the input, so React never warns about switching between controlled and uncontrolled. Official shadcn/ui has no number field: its input is a bare 768-byte element, and input-group and field are assembly kits that pull in button, input, textarea, label and separator without a line of numeric logic between them — no stepping, no clamping, no min/max state. This depends only on lucide-react for the two icons.
A one-time passcode field split into separate digit boxes — the "enter the 6-digit code we sent you" screen. Reach for it on any challenge step where a short numeric code is typed or pasted: two-factor and multi-factor sign-in (2FA/MFA), an authenticator app's TOTP code, an SMS or phone verification code, an email confirmation code, a magic-link fallback code, account recovery, a step-up check before a payment or a destructive settings change, device or TV pairing, and PIN entry. Common asks it answers: "otp input", "one time password input", "verification code input", "6 digit code input", "enter code boxes", "2fa code field", "sms code input", "confirmation code input", "pin input react", "segmented code input", "react-otp-input alternative", "input-otp alternative", "otp input without dependencies", "shadcn otp input". Official shadcn/ui covers the same screen with input-otp, and the difference is the dependency: that one is a wrapper around the third-party input-otp package (plus lucide-react for its separator), so installing it adds a runtime dependency and hands the caret and paste behaviour to a library. This is the same boxed UI written out in a single file with no dependencies at all — reach for it when the project is keeping its dependency list short, when a package has to be vendored or audited before it can be added, or when you want the behaviour in code you can read and change rather than configure. Digits only, by design: the value is stripped to digits and truncated to `length` (default 6) on every path in, so a controlled parent, a paste and a keystroke cannot disagree about what is in the field. Pasting a full code into any box distributes it across the rest and lands the caret on the last filled one, typing auto-advances, Backspace clears and steps back, and the arrow keys, Home and End move between boxes. The first box carries autocomplete="one-time-code", so iOS and Android offer the code from the incoming SMS, and every box is inputMode="numeric" with pattern="[0-9]*" for a numeric keypad on mobile. Every box is labelled "Digit N of 6" and the group carries a name of its own, so a screen reader user is told where they are instead of hearing six unlabelled text fields. Works controlled (`value` + `onChange`) or uncontrolled (`defaultValue`), fires `onComplete` once when the last box fills — on the fill only, not on every later edit — and forwards a ref to the first box so a page can focus it on mount or from a shortcut. Passing `name` mirrors the joined value into a hidden input, so it posts with a plain HTML form, a Next.js server action, or React Hook Form without a controller. Themed with shadcn tokens, so it follows dark mode.
A password generator: a read-only field holding the generated password, a Generate control, a copy button, and switches for length, character classes and look-alike exclusion. Reach for it wherever a screen offers to invent a credential rather than ask for one — the "Suggest a strong password" affordance beside a sign-up, registration or change-password field, an admin creating a user or issuing a temporary password for a new employee, a settings screen minting an API key, access token, client secret or webhook signing key, a service-account or database credential during setup, a Wi-Fi or router passphrase printed for someone to type on another device, and any rotate-credentials or reset flow. It is the third piece of a set and does the opposite job to the other two: password-input is the field a person types their own password into, password-strength scores a password a person chose, and this one produces a password nobody chose. shadcn/ui ships no generator — there is no crypto call anywhere in its registry — so an agent asked for one writes it inline, and inline is where it goes wrong in ways the output never shows. Math.random is a fast PRNG whose state is recoverable from its own output, so passwords built on it are not unguessable while looking exactly as random; folding a random number into the alphabet with % tilts the result toward the low characters; and satisfying "must contain a digit" by overwriting a fixed position tells an attacker where the digit is. This draws from crypto.getRandomValues, redraws the values that would bias the fold instead of folding them, and guarantees each enabled class by drawing one character per class and then shuffling with a Fisher-Yates pass whose indices come from the same unbiased source, so no position is special. It reports entropy in bits, which is honest arithmetic here precisely because the draw really is uniform. The first password is drawn in an effect rather than during render, so a server-rendered page does not hydrate with a mismatch — the failure that makes a generator look broken in a Next.js app while working perfectly in isolation. The exported generatePassword, buildPools and entropyBits work on their own for seeding, CLI use or tests. Look-alike characters (0O1lI) can be excluded for passwords read off one screen and typed into another. Every colour is a shadcn token so it follows light and dark, and the live region announces that a new password exists without ever speaking the password itself.
A password field with the show/hide eye already built in — the one control every sign-up form needs and shadcn/ui does not ship. It is a drop-in replacement for `<input type="password">`: every native prop passes straight through (`name`, `autoComplete`, `required`, `minLength`, `placeholder`, `disabled`, `onChange`), and the ref lands on the field itself, so `register()` from react-hook-form and a shadcn `FormField` bind to it exactly as they would to a bare input. Reach for it in sign-up and registration forms, login and sign-in screens, change-password and reset-password flows, the “confirm password” field beside it, the password half of a credentials dialog, a database or SMTP connection form, and any secret — an API key, a token, a recovery phrase — that a person has to read back to check they typed it correctly. The reveal toggle is where hand-rolled versions go wrong, so all five decisions are already made: the button’s accessible name follows the state (“Show password” when hidden, “Hide password” when shown) rather than freezing on one of them; `aria-pressed` reports whether the password is currently visible; the eye is `aria-hidden`, so a screen reader hears the name and not the glyph; the button is `type="button"`, so pressing it inside a form does not submit the form; and it is deliberately kept out of the tab order — a keyboard user tabbing off a password should land on the submit button, not on a reveal control they did not ask for. It stays reachable by mouse, touch and screen-reader navigation. Common asks it answers: “password input”, “password field with show hide”, “show password toggle”, “reveal password button”, “eye icon password input”, “toggle password visibility react”, “password visibility toggle component”, “hide password input”, “shadcn password input”, “shadcn password field”, “login form password field”, “sign up password input”, “react password input component”, “MUI password field equivalent”, “antd Input.Password equivalent”, “chakra password input equivalent”. shadcn/ui’s own `input` is the plain field — it takes a `type` and styles it, with no toggle, no eye and no mention of passwords anywhere in its source; `input-group` is a layout kit for placing an adornment beside a field, which leaves the toggle’s behaviour and its accessible naming for you to write. This is that field with both already decided. Pair it with this registry’s `password-strength`, the meter that goes underneath and scores how many guesses a password would survive: this component is the field, that one is the verdict on what gets typed into it. Needs `lucide-react` for the eye.
A password strength meter for a sign-up, registration, change-password or reset-password form — the bar under the password field that says Weak or Strong, plus one line saying why. It scores how many guesses a password would survive rather than ticking off one uppercase, one number, one symbol: composition rules push people toward Password1!, which is guessed instantly, and reject correct horse battery staple, which is not. NIST SP 800-63B says the same — screen against known-bad passwords and let length do the work. It detects the things that make a password look random without being random: entries from a built-in list of the passwords that top every breach dump (folded through leet substitutions, so P@ssw0rd is found where password is, and unaffected by capitalisation), repeated characters, runs through the alphabet or the digits, runs along a keyboard row, and years and dates. The check hand-rolled meters always miss is userInputs: pass the email, username, display name or your product name and Acme2026! stops scoring as strong on acme.com — including the joined-up forms that separators hide, so Acme Co catches acmeco. Pass blocklist to add your own breach list on top; the built-in one is deliberately small, because a real one is megabytes and belongs behind an API. estimatePasswordStrength is exported on its own, pure and synchronous, so the same score that draws the meter can disable your submit button or drive a zod refine — no async, no 800 kB zxcvbn bundle, no dependencies at all. Accessibility is the other half: the bars are a role="meter" with aria-valuetext, and only the band name sits in the aria-live region, so a screen reader hears "Weak" once when the password crosses a band instead of being read to on every keystroke — which is what an aria-live wrapped around the whole widget does. The advice line is tied to the meter with aria-describedby instead — give the component an id and it derives one for the advice and wires it up; without an id the advice is still read, just in document order rather than together with the meter. Warnings and suggestions come back as stable codes with an overridable message table, so the meter translates. It uses no hooks, so it renders in a server component and needs no "use client" of its own. Official shadcn/ui has nothing for passwords — no meter, no blocklist, no scorer; its input is a bare element and field and input-group are assembly kits with no logic in them. It scores a password but does not collect one: the field it sits under is this registry’s `password-input`, which is the same bare element with the show/hide eye and its accessible naming already wired. Pass this component that field’s value and the two compose into the whole password row.
An international phone number field: a searchable country picker, that country's calling code shown as a live prefix, and one E.164 string ("+819012345678") coming back out. Reach for it wherever a form asks for a number someone can actually be reached on: sign-up and onboarding; two-factor authentication and SMS one-time codes; WhatsApp, SMS and voice notification preferences; shipping, billing and delivery contact details; the callback number on a support or contact form; restaurant, clinic, salon and appointment booking; driver, courier and rider contact on a marketplace; the phone row on a CRM or address-book record; KYC and identity verification; account recovery; and the "mobile" field in profile, account and organisation settings. Common asks it answers: "phone input", "phone number input react", "international phone input", "phone input with country code", "country code selector phone", "tel input component", "E.164 input", "shadcn phone input", "shadcn phone number field", "react-phone-number-input alternative", "intl-tel-input react", "phone field with flags", "dial code dropdown", "mobile number input", "international telephone input shadcn", "phone number formatting as you type". Official shadcn/ui has nothing for telephones — not a phone item, not a `type="tel"` anywhere in its sixty-three components, no calling codes and no country data — so the whole of this is on you, and the data is where hand-rolled versions go wrong. It is also the one field in this family that cannot read its data out of the runtime: `Intl.supportedValuesOf` rejects every phone-shaped key, `Intl.Locale` exposes no telephony property and `Intl.DisplayNames` names regions but not calling codes, so no browser knows that Japan is +81. This component therefore ships the table — 246 countries, every ISO 3166-1 country that has an assigned calling code, cross-checked between two independent sources — and carrying it is a smaller liability than it looks, because calling codes are close to frozen: the last new one was South Sudan's +211 in 2011. The table holds calling codes and never a calling code plus an area code, which is the distinction hand-rolled fields miss: the Bahamas is +1, not +1-242, because 242 belongs to the ten-digit national number a Bahamian dials. Twenty-five countries share +1 and four share +44, so the country is kept as its own piece of state and never inferred back out of the digits — a stored "+12425550100" cannot say whether its owner is in Nassau or Nevada, and `countryName` submits the answer alongside the number so the field comes back up on the country the person actually picked. What it does not do is as deliberate as what it does: it assembles, caps the result at E.164's fifteen digits and spaces them for reading, and it never claims a number is valid. Per-country validity is a rule for 246 countries and the library that knows them is larger than this entire registry, so it is left to your submit handler rather than faked — a field that pretends otherwise fails exactly the people whose country it got wrong. Typing is handled properly rather than approximately: digits group as you type, the caret is tracked in digits rather than in character offsets so a number stays correctable in the middle instead of only from the end, Backspace and Delete remove a digit rather than a separator that was never typed (the single most common complaint about masked inputs), pasting "+81 90-1234-5678" moves the country and strips the code instead of doubling it, and `format` takes a mask like "## #### ####" for a form that only ever collects one country's numbers. Controlled or uncontrolled, and correct in both: `value` + `onValueChange` for the number, `country` + `onCountryChange` for the country, `defaultValue`/`defaultCountry` for neither. The digits stay in local state while the value round-trips, so a parent that debounces, validates or is simply slow does not erase what was just typed — the failure that makes most hand-rolled controlled phone fields impossible to type into. It is a real composite control, not a styled div: a `type="tel"` input with `inputMode="tel"` and `autoComplete="tel-national"`, the calling code wired into the input's `aria-describedby` so it is announced rather than being visual context a screen reader never reaches, the country picker a full `role="combobox"` listbox with search, arrow keys and `aria-activedescendant`, and a `focus-within` ring around the number half. `countries` narrows it to where you operate, `priority` pins the two or three countries most sign-ups come from, `flags` adds the flag emoji (off by default — Windows ships no flag glyphs), and `getDialCode()`, `splitPhoneNumber()`, `toE164()`, `formatPhoneDigits()` and `getRegionCountry()` are exported so a confirmation screen, an SMS log or an admin table spells the same number the same way. Styled entirely with shadcn tokens (input, ring, popover, muted-foreground), so it follows light and dark mode, and it ships zero npm dependencies — no libphonenumber, no country-data package, no icon package.
A single pricing plan card — one tier's box, with its name, its price, what it includes and the button that takes it. Reach for it wherever a plan has to be chosen or shown: a marketing pricing page, a plans-and-billing settings screen, an upgrade, paywall or trial-ended modal, a compare-plans table, the subscription row in an account page, a checkout or plan-picker step, a seat or usage tier selector, and the free/pro/enterprise row on a landing page. Common asks it answers: "pricing card react", "pricing table component", "shadcn pricing card", "shadcn pricing page", "shadcn pricing table", "pricing tiers component react", "subscription plan card", "plan comparison card", "free pro enterprise cards", "most popular plan highlight", "featured pricing tier ring", "recommended plan badge", "upgrade modal component", "paywall component react", "billing settings plan card", "monthly yearly price toggle card", "price with billing period", "feature list with checkmarks react", "included and excluded features list", "strikethrough feature not announced screen reader", "line-through accessibility", "check and cross feature list accessible", "tailwind pricing card", "next.js pricing page component", "stripe pricing table alternative", "build your own pricing table", "heading level prop react", "pricing table heading hierarchy", "料金プラン カード react", "価格表 コンポーネント". It takes a plan name, a big price with its billing period ($29 /mo), an optional one-line pitch, a feature list and a call-to-action, and several of them sit side by side to make the whole pricing table. Features are the part that is usually got wrong. Pass a string for an included feature or an object to mark one excluded; an excluded row is muted and struck through, and it also carries a screen-reader-only "Not included:" prefix — because text-decoration: line-through is not announced, so a purely visual strikethrough tells a sighted reader the feature is missing and tells a screen-reader user it is included. The check and minus icons are aria-hidden, since the text already says which is which. Set featured to mark the recommended tier: it draws a primary ring and a "Most popular" badge, and badge takes any other label (Best value, Current plan). headingLevel picks h2, h3 or h4 so three cards dropped into a section do not break the page outline — a pricing table is the classic place where a hardcoded h3 lands under the wrong h2. Against official shadcn/ui there is no pricing card to compare with, only pieces to assemble: card is a 1,828-character generic container with no price, plan, tier or feature vocabulary in it at all, item is a ten-part compound kit (ItemGroup, Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader, ItemFooter, ItemSeparator) that additionally pulls in separator and is built for list rows, and badge and button each drag in @radix-ui/react-slot. None of the four contains a single pricing term. Within pulld it is the marketing-page counterpart to feature-card, which sells a capability rather than a tier, and it pairs with segmented-control for the monthly/yearly switch above a row of them. This is one component with lucide-react as its only dependency, theme-aware through shadcn tokens in light and dark.
Prints one region of the page — an invoice, a receipt, an order confirmation, a ticket or boarding pass, a packing slip, a report or dashboard panel, a chart, a table, a quote or estimate, an itinerary, a certificate, a prescription, a shipping label, a résumé, or a card carrying a generated QR code — instead of printing the page it happens to be sitting on. Common asks it answers: 'react print component', 'print div react', 'print only part of the page', 'print specific div react', 'window.print prints whole page', 'react-to-print alternative', 'print button shadcn', 'print invoice react', 'print receipt component', 'save as pdf button react', 'print styles missing react', 'printed div has no css', 'canvas blank when printed', 'form values empty when printed', 'background color not printing', 'print preview cuts off scrollable div', 'how to detect print finished react', '@media print react component'. shadcn/ui has nothing here at all: window.print, beforeprint, afterprint, @media print and print-color-adjust each appear exactly zero times across the whole 255KB of component source its registry serves, so this gets written inline every time. Not to be confused with pulld recovery-codes, which builds a sheet of its own out of data you hand it and is the right choice for backup codes; this one copies a region of the live page, styles and all. The inline version has two shapes and both are wrong. onClick={() => window.print()} prints the document — nav, sidebar, cookie banner — and clips a scrolling panel to whatever was scrolled into view. The fix usually reached for next is a global @media print rule that hides everything except one class, which puts a layout concern into app-wide CSS and breaks the next time the markup moves. This clones the region into an off-screen document instead, and the details that make that hold up are the ones a hand-rolled version leaves out. Styles do not come with a clone, so the page's own are carried over: link elements are re-linked by href rather than serialised, because reading cssRules on a cross-origin sheet — a font service, a CDN build — throws SecurityError and silently drops exactly the stylesheets you cannot see, while constructable adoptedStyleSheets have no href and are serialised instead. A base element is written into the head, because a srcdoc frame has no URL of its own and every relative image on the sheet would otherwise resolve against nothing. What the user typed is copied onto the clone, because value, checked and selected are properties and not attributes, so a filled-in form serialises back to the blank form it was authored as — on a sheet that looks finished. Canvases are redrawn as images, because cloneNode copies the element and not the bitmap, and a chart, a sparkline or a captured signature otherwise prints as white space; a tainted canvas loses its own picture rather than the sheet. print-color-adjust is forced on, because browsers drop background colours and a colour-coded table prints white on white. Scroll boxes are unclipped, because a panel keeps its height on paper and everything below its fold is simply absent. The frame recipe is the rest of it: 0x0 and transparent rather than display:none (a frame that is not displayed prints a blank page), srcdoc assigned before insertion so the only load event is the sheet's rather than the initial about:blank, printing on that load event because it is what waits for the copied stylesheets, a 3s bound on that wait so one unreachable CDN cannot leave the button doing nothing forever, and teardown on afterprint rather than on the next line, because print() blocks in Chrome and Firefox but returns immediately in Safari where removing the frame would cancel a dialog still open. The API: target is a ref to the region, documentTitle becomes the sheet's title and therefore the default filename under Save as PDF, pageStyle appends CSS after the built-in rules so you can override them, onBeforePrint fires before serialisation (expand rows, reveal detail), and onPrintEnd reports 'done' | 'unavailable' | 'failed'. That outcome is deliberately not 'printed': afterprint fires for a cancelled dialog exactly as it does for a finished job, and every 'mark as printed' flag built on it eventually lies, so this one refuses to claim more than the browser said. type='button' so it will not submit a surrounding form, the state is announced through an sr-only aria-live region, the icon is aria-hidden, the button disables itself while a sheet is being prepared so a second click cannot open a second dialog, and the reset timer is cleared on unmount. Nothing rendered depends on feature detection, only what the click does, so the server and the first client render agree and there is no hydration mismatch. Styled with shadcn tokens (input, accent, ring) for light and dark, className merges rather than fights, focus ring is focus-visible. One file, one lucide icon, no print library.
A circular progress indicator: an SVG ring that fills clockwise from twelve o'clock, with the percentage — or any icon or short label you pass — sitting in the middle of it. Reach for it when a linear bar is the wrong shape: file, image and video upload progress, a storage or quota meter (disk, seats, API credits, plan limits), a goal, streak or activity ring, profile or setup completeness, onboarding and multi-step progress badges, a countdown or timer dial, per-row progress in a compact table or card, and score or health dials on a dashboard. Common asks it answers: "circular progress", "circle progress bar", "radial progress", "progress circle", "donut progress", "percentage circle", "completion ring", "activity ring", "upload progress circle", "quota ring", "react-circular-progressbar alternative", "shadcn circular progress". Official shadcn/ui has no radial progress component: its progress is a linear bar that pulls in @radix-ui/react-progress and is a client component, with no notion of a circle, a percentage readout or an unknown total; its spinner turns forever and reports no value at all. This one is a single file with no npm dependencies whatsoever — not Radix, not class-variance-authority — and it declares no "use client", so it renders inside a React Server Component instead of dragging a client boundary along with it. Set showValue to print the rounded percent in the centre, or pass children to put an icon, a fraction or a short word there instead. Set indeterminate when the total is not known yet — an upload with no content length, a job whose size the server has not reported — and it spins a fixed quarter arc and drops aria-valuenow, which is how a progressbar is supposed to say "running, position unknown" rather than lying with a number. Otherwise it exposes role=progressbar with aria-valuenow, aria-valuemin and aria-valuemax and takes an aria-label for its accessible name, and the ring itself is aria-hidden so the value is announced once rather than twice. size and strokeWidth are plain pixel props, the geometry is derived so the stroke never clips at the edge of the box, a value past max or below zero is clamped instead of drawing an impossible ring, and max=0 reads as empty rather than dividing by zero. Themed with shadcn tokens so it follows dark mode.
A scannable QR code, drawn as SVG, for putting a link, a Wi-Fi network or a 2FA secret on screen where a phone can pick it up. Reach for it wherever something has to get from this screen onto someone's device — a two-factor enrolment screen, a Wi-Fi guest network on a poster or a hotel card, an event ticket, a boarding pass or a venue check-in, a payment or invoice link, a table ordering code, a menu, a device or kiosk pairing step, an app-store or download link, an invite or referral link, a shipping or returns label, a warranty or product registration, a business card or conference badge, a receipt, a signup link on a slide, and any hand-off from a desktop app to the phone in someone's pocket. Common asks it answers: "qr code react", "react qr code component", "generate qr code react", "shadcn qr code", "qr code generator react", "qrcode.react alternative", "react-qr-code alternative", "qrcode npm alternative", "qr code without dependencies", "qr code no npm package", "qr code svg react", "qr code canvas vs svg", "qr code server component", "qr code next.js app router", "qr code ssr", "wifi qr code generator", "share wifi password qr code", "WIFI: qr format", "otpauth qr code", "2fa qr code react", "totp enrollment qr", "google authenticator qr code", "qr code not scanning", "qr code won't scan", "qr code scans on some phones", "quiet zone qr code", "qr code margin", "qr code dark mode", "inverted qr code not scanning", "white qr code on dark background", "qr code error correction level", "qr code with logo in middle", "qr code capacity limit", "qr code too long", "qr code changes size when text changes", "qr code utf-8", "qr code japanese characters", "qr code emoji", "qr code blurry", "qr code print quality", "qr code accessibility", "qr code screen reader", "qr code aria-label", "QRコード react", "QR コード 生成", "二次元コード コンポーネント", "WiFi QRコード". Official shadcn/ui has nothing here and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and searching all 245,417 bytes of it, the strings qr, QR, Reed, Galois, errorCorrection, quietZone, finderPattern, alignmentPattern, shapeRendering, crispEdges, otpauth, wifi, barcode and scanner are every one of them a zero hit. So this gets solved by adding a package, and that is the first thing worth knowing about this one: the encoder is in the file. Three packing modes chosen by content, all four correction levels, versions 1 to 40, Reed–Solomon over GF(256), block interleaving, and all eight masks scored — no dependencies at all, not even an icon, in a registry whose 83 other components have never needed more than lucide-react. Because a QR encoder's bugs are invisible, correctness here is measured rather than asserted. Every grid this ships was rendered to a PNG and read back by Apple's Vision framework, an independent decoder sharing no code with it: 426 codes on 2026-09-12, covering all 40 versions at all four levels, every mask forced, the mode and capacity boundaries, UTF-8 and emoji, a version 40 code at its 2,953-byte maximum, and 240 random payloads at the shipped defaults, each decoded string compared byte for byte with its input. That sweep found a real bug — one error-correction table was a row short from version 32 up, so nine versions produced codes that rendered perfectly and decoded to nothing — and the verified grids are frozen in the test suite so it cannot come back. Beyond the encoding, three things decide whether a code actually works. The first is the quiet zone: a scanner finds a code by finding its border, so four light modules of margin are not decoration, and a code butted against other content renders beautifully and is simply never seen. Nothing catches that except pointing a real camera at it, so the default is the required value and the light ground is drawn as part of the SVG rather than left to whatever is behind it. The second is colour, which is functional rather than decorative and therefore does not follow your theme by default: dark modules on a light ground is what the format specifies and what decoders assume, inverted codes are read by some phones, fewer cheap scanners and no printer at all. So the code stays black on white in dark mode — which is what a boarding pass, a wallet app and a bank statement all do — and moduleClassName and backgroundClassName are there when you have tested otherwise. The third is SVG rather than canvas: a canvas is a bitmap that resamples badly the moment it is scaled or printed, and to anything that is not an eye it is a blank rectangle. This carries role="img" and a label, and deliberately does not read the payload aloud unless you ask — sixty characters of URL letter by letter helps nobody, and the two things most often in a code are a Wi-Fi password and a 2FA secret. A QR is something you point a second device at, so put the payload on the page as well: a real link, or a code beside a copy button. It has no hooks, no state and no effects, so there is no "use client" on it: it renders on the server and ships zero JavaScript for what is a static picture, with a small internal cache keeping a re-rendering parent from re-encoding. errorCorrection defaults to M and is boosted automatically to the strongest level that still fits the same version, since the slack would otherwise be spent on padding — the code is the same size and survives more damage. minVersion holds the size still for a payload that changes while it is on screen, like a rotating token, which would otherwise resize the code mid-scan. A payload too long to fit throws from encodeQr — a truncated code still scans and hands back the wrong string — while the component renders a fallback instead of taking the layout down. Two payload builders come with it, both for the traps rather than the strings: wifiPayload escapes the ; : , and backslash that a generated password contains and that otherwise truncate the network details silently, and quotes an all-hex value so it is not read as a raw key; otpauthUri puts the issuer in both the label prefix and the query parameter, because different authenticator apps read different ones, and escapes the two halves of the label separately so the colon separator survives while a + or a space in the address does not break it. encodeQr and qrPath are exported for a code you want to rasterise on a server, put on a label, or draw yourself. Within pulld it completes the two-factor setup screen alongside otp-input and recovery-codes, and sits beside copy-button and copy-field, which are what you put next to it for everyone who cannot scan it.
A star rating that works both ways: as an input that collects a score, and read-only as a display of one. Reach for it wherever a number between 0 and 5 is really a row of stars: the "rate this" step after a purchase, delivery or booking; a product, app-store or seller review form; a CSAT or satisfaction question at the end of a support ticket or chat; a post-call or post-session feedback prompt; a difficulty, quality or priority score on an internal form; and, in read-only mode, the average beside a listing, a product card, a search result, a testimonial or a review summary (an average like 3.7 fills 70% of the fourth star, so the display is not rounded to a whole one). Common asks it answers: "star rating", "rating component", "rating input", "react star rating", "five star rating", "half star rating", "review stars", "star rating readonly", "average rating display", "feedback rating component", "shadcn rating", "shadcn star rating", "react-rating alternative", "rate this product component", "CSAT stars". Official shadcn/ui has no rating or star item of any kind — its slider is a range control with a thumb on a track, which is a different shape of answer — so the usual fallback is a row of buttons with no shared value semantics, and that is what makes it inaccessible. This one is a real slider: focusable with a focus-visible ring, arrow keys raise and lower the score in either axis, Home clears it to 0 and End maxes it out, and it exposes aria-valuemin/valuemax/valuenow plus a spoken aria-valuetext ("3.5 out of 5 stars") so a screen reader announces the score rather than counting buttons; read-only mode drops the slider role and renders as a labelled image instead, which is the correct semantic for a number you cannot change, and the individual stars stay aria-hidden either way because the value is announced once, not five times. Set allowHalf to take half stars — click the left half of a star, or step by 0.5 from the keyboard. Works controlled or uncontrolled through a plain number value with onValueChange (a number, not a string, so there is nothing to parse), forwards a ref, and posts through a hidden input in native forms via `name`. `max` changes the number of stars for a 3- or 10-point scale, `size` sets the pixel size, and `disabled` keeps the slider semantics while refusing input. Theme-aware via shadcn tokens — filled stars use the primary color, so it follows light and dark mode — with lucide-react as its only dependency, for the star icon.
One horizontal bar that shows how a whole is divided up, with a legend that names every part — the GitHub-style language / storage bar. Reach for it whenever the question is "what is this made of?" rather than "how far along is it?": disk or storage usage broken down by file type, a plan or quota bar (seats used, API calls, build minutes, bandwidth), a budget or spend breakdown by category, traffic by source or device, test results split into passed / failed / skipped, a portfolio or vote split, tickets by status, or a repository language bar. Common asks it answers: "stacked bar component react", "percentage breakdown bar shadcn", "storage usage bar", "disk usage breakdown", "quota / capacity bar", "segmented progress bar", "share of total bar", "distribution bar", "usage meter with legend", "percentages that add up to 100". Pass `parts` as `{ label, value }` objects in any unit you like — bytes, requests, dollars — and only the ratios are used. Add `total` to switch from "parts of a whole" to "used out of a capacity": the gap is drawn as empty track and listed as its own row (rename it with `remainderLabel`, or pass `null` to draw it without listing it). `precision` adds decimals, `formatValue` puts the raw figure next to each share, `showLegend={false}` keeps the legend for screen readers only, and each part takes a `className` for its colour (the default is a ramp of your primary colour, which is theme-aware in any shadcn project; pass `bg-chart-1`…`bg-chart-5` or your own classes for distinct hues). It handles the parts a hand-rolled version gets wrong. The percentages are apportioned by largest remainder rather than rounded one at a time, so three equal parts read 34 / 33 / 33 instead of 33 / 33 / 33 and the column always totals exactly 100. A part too small to round to a whole percent reads "<1%" rather than the lie "0%", and a part that is nearly but not quite everything reads ">99%" rather than "100%". Tiny slices keep a two-pixel minimum so they stay visible without stealing width from the rest, while a part worth exactly zero draws nothing at all and is still listed. Negative, NaN and Infinity values count as zero instead of collapsing the layout. The legend names and quantifies every part, so nothing is carried by colour alone (WCAG 1.4.1) and the bar itself is aria-hidden. No hooks and no clock: it renders inside a React server component with no "use client" of its own, ships no client JavaScript, and produces identical markup on the server and in the browser. `ratioPercents` is exported for the same figures in a table or tooltip. Official shadcn/ui has nothing for this: progress is a single value with no parts, and chart is a Recharts wrapper for plotted series rather than one inline bar with no dependencies.
Long text clamped to a few lines with a Show more / Show less toggle that appears only when the text is genuinely too long. Use it wherever text is usually short but occasionally is not: product and marketplace listing descriptions, comments, reviews and replies, user bios and profile blurbs, release notes and changelog entries, incident and error detail, log lines, AI answers and summaries, job posts, FAQ answers, and long cells in a card or table. Common asks it answers: "read more button", "show more / show less", "expandable text", "truncate text with a show more link", "line clamp with toggle", "collapsible paragraph", "see more link", "clamp description to 3 lines", "react-show-more-text alternative", "text truncation with expand". shadcn/ui ships nothing for this, and its collapsible is a different thing — a generic open/close container whose trigger is always there and which does no clamping — so the genuinely awkward part is left to you: deciding whether the toggle should exist at all. This measures the rendered text and renders the control only when the clamped box actually overflows, so a list of mostly-short entries does not sprout a pointless "Show more" under every one of them. It re-measures when the column resizes and the text rewraps, and again once web fonts have loaded, because a clamped box keeps its height while the line count underneath it changes; an element that is off screen in a closed tab or accordion measures zero, which it treats as "unknown" rather than "it fits", so the toggle is not dropped while the text is out of view. The clamp is applied as inline style rather than Tailwind's line-clamp-N utility, because `lines` is a runtime value and a dynamic `line-clamp-${n}` class is invisible to Tailwind's scanner — it would work in dev and silently vanish from the production build. Accessibility is where the hand-rolled version usually goes wrong: the full text always stays in the DOM and is only clipped visually, so screen readers read all of it and find-in-page still reaches it, instead of the usual text.slice(0, 200) that destroys the content for everybody; the control is a real button carrying aria-expanded and aria-controls pointing at the text. Clipped is not hidden, so a link inside the invisible part is still in the tab order — focus landing there expands the block rather than letting the browser scroll the clamped box and shear the text mid-line. Collapsing pulls the block back into view when it has already scrolled off the top, so the reader is not dumped further down the page. Uncontrolled by default; pass expanded and onExpandedChange to drive it from an "expand all" control. Styled with shadcn tokens (ring, muted-foreground) so it follows light and dark themes, and ships with no dependencies beyond your own cn util.
The sheet of two-factor backup codes, with the three ways off the screen that people actually use: copy, download as a .txt, and print. Reach for it wherever an account hands someone a set of one-time codes to keep — finishing two-factor or MFA enrolment after scanning the authenticator QR, the "View recovery codes" panel in a security settings page, regenerating a set after a lost phone, passkey and WebAuthn fallback codes, seed or backup phrases handed over once, and the onboarding step that will not let you continue until you confirm you have saved them. Common asks it answers: "recovery codes component", "backup codes UI", "2FA recovery codes react", "MFA backup codes screen", "one-time codes list", "download recovery codes txt", "print backup codes", "copy recovery codes", "GitHub-style recovery codes", "show recovery codes once", "regenerate backup codes UI", "shadcn recovery codes", "strike through used backup code". Official shadcn/ui has nothing for it — no recovery, backup-code, download or print item anywhere in its sixty-odd components — so an agent asked for this screen writes it inline, and the inline version is where the whole thing quietly stops working. Print is the worst of it. Everyone writes onClick={window.print()}, which prints the page rather than the codes: the nav, the sidebar and the rest of the settings form come along, and because a freshly issued set is nearly always shown inside a scrolling dialog, the printed sheet is clipped to whatever part of that dialog happened to be scrolled into view. Half the codes are missing, on paper that looks finished, and nobody finds out until the day they need them. This prints a document of its own instead — a titled, dated sheet with the codes laid out so none of them straddle a page break — through a hidden iframe that is 0x0 rather than display:none (a frame that is not displayed prints a blank page), whose srcdoc is set before insertion so the only load event is the sheet's rather than the initial about:blank, and which is torn down on afterprint rather than on the next line, because print() blocks in Chrome and Firefox but returns immediately in Safari, where removing the frame would cancel a dialog still open. The sheet is stated in black on white on purpose: browsers drop background colours when printing but keep text colours, so a dark-mode card sent to a printer comes out as pale grey on white and is close to unreadable. The download half has its own two: the anchor is put into the document before it is clicked, because Firefox ignores a click on an element outside the tree, and the object URL is revoked afterwards — an un-revoked one keeps its blob, which is to say the recovery codes, alive and addressable for the life of the document — but revoked on a later task, since releasing it in the click's own task cancels the download. The file is written with CRLF endings so Windows editors do not render it as a single line; the clipboard gets plain LF and the bare codes with no heading, because it is being pasted into a password manager's notes field. Spent codes are struck through and, because a line through text is a paint decision that reaches nobody using a screen reader, also labelled in words — and they are left out of every export, since a saved file padded with dead codes is the right length and so is worse than no file at all; the header says how many are left whenever any have been spent. role="list" is put back by hand because Safari drops list semantics from a list-style-none <ul>, which would take away the one number that matters here. Pass codes as plain strings for a fresh set or as {code, used} for a set being reviewed later; onExport fires on copy, download or print, which is the signal to unlock your "I have saved these" button. normalizeCodes, formatCodesText, buildPrintDocument, downloadTextFile and printDocument are exported for reuse. Nothing renders a date, so it server-renders without a hydration mismatch — the timestamp is taken when a button is pressed. Composes pulld copy-button; every colour is a shadcn token, so it follows light and dark.
The small inline "Saving… / Saved 2 minutes ago / Couldn't save · Retry" indicator that sits beside an autosaving surface, and the announcement that goes with it. Use it wherever edits persist in the background instead of behind a Save button: a document, note or rich-text editor (Tiptap, ProseMirror, Lexical, Slate, Quill), a code editor pane (Monaco, CodeMirror), a settings, profile or account page that saves on blur, a draft post, email or message composer, a form with debounced autosave, a spreadsheet-style inline-edit table, a kanban card or CRM record, a form builder, page builder or design canvas, a collaborative document backed by a CRDT (Yjs, Automerge, Liveblocks), and anything wired to a background mutation — a TanStack Query mutation's isPending and isError, a Next.js server action behind useActionState or useFormStatus, react-hook-form with autosave, tRPC, SWR, Convex, Firebase or Supabase writes. Common asks it answers: "autosave indicator", "saving spinner next to the title", "all changes saved", "changes saved automatically", "draft saved status", "last saved timestamp", "saving saved error state", "Google-Docs-style save state", "Notion-style saving indicator", "how to show saving/saved/error", "autosave status react", "save state component", "shadcn autosave indicator", "shadcn save status". Official shadcn/ui ships nothing for this, and the parts that look close are all a different shape: sonner and toast pop a transient message after an action and then leave, so nothing on screen still says the document is saved a minute later; spinner covers a single in-flight request and has no resting, saved or failed state; badge renders a label but carries no state machine, no relative time and no live region; and field and message report validation on a control rather than the fate of a background write. You pass one `status` prop (idle | saving | saved | error). idle renders nothing visible, so it can be rendered unconditionally and simply mirror your mutation state rather than being mounted and unmounted around it. Pass `savedAt` and "Saved" is followed by a live relative timestamp that keeps itself fresh — it composes the time-ago component instead of freezing a string that goes stale while the tab sits open, which is the first thing a hand-rolled one gets wrong. Pass `onRetry` and the error state grows a Retry button. Every label (savingLabel, savedLabel, errorLabel, retryLabel) is overridable for i18n. Accessibility is the fiddly part, and it is the reason to take this rather than write it. The wording lives in an always-mounted role="status" region, so the very first transition is actually announced — a region that mounts at the same moment as its text is registered too late and its first "Saving…" is dropped silently, which is exactly what conditional rendering produces. The ticking timestamp and the Retry label sit outside that region on purpose: inside it, the timer would make the page announce "Saved 3 minutes ago" every minute, unprompted, for as long as the tab is open. And it stays polite rather than assertive even for failures, because aria-live is honoured at registration time and a failed autosave should not cut across someone mid-sentence. Distinct from toast, which is a transient notification after an explicit action, and from spinner and loading-button, which cover one request in flight: this is the persistent, resting status of a background save. Styled with shadcn tokens (muted-foreground, destructive, ring) so it follows light and dark themes; it composes time-ago and uses lucide-react for its icons.
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.
A scrolling box whose edges fade out while there is more content past them, and stop fading the moment there is not — the cue that tells a reader a wide table has columns off to the right, or that a panel continues below the fold. Reach for it wherever a box scrolls inside a page that does not: a wide data table or admin grid seen on a laptop or a phone, a horizontal tab strip, filter-chip row or category rail, a card carousel, a code block or build log that scrolls sideways, a long terms-and-conditions or changelog panel, the body of a modal or drawer, a sidebar nav taller than the viewport, a chat or comment pane, a dashboard table inside a card, and any responsive table that overflows on small screens. Common asks it answers: "scroll shadow react", "fade edges of scrollable div", "shadow when content overflows", "indicate more content horizontally", "horizontal scroll indicator for table", "responsive table overflow indicator mobile", "detect if an element is scrollable react", "check if a div has overflow javascript", "is content overflowing react hook", "scrollWidth vs clientWidth react", "useOverflow hook", "show a gradient only when scrollable", "fade out overflow tailwind", "mask-image scroll fade", "NextUI ScrollShadow alternative", "Mantine ScrollArea shadow alternative", "shadcn scroll area shadow", "shadcn horizontal scroll fade", "scroll hint react", "scrollable region focusable axe", "keyboard scroll a div", "スクロールできることを示す影". The thing being solved is that overflow: auto will not tell you whether it is overflowing. A scrollable box looks exactly like a box that ends there, and the browser offers no hook for the difference — there is no :overflowing selector, no event, nothing in CSS at all. So a cut-off table reads as a complete table, and people file bugs about missing columns that were on screen the whole time. The only way to know is to measure scrollWidth against clientWidth and keep re-measuring, which is why this is a component and not three utility classes. Official shadcn/ui has none of that machinery. Grepping all sixty-three of its registry entries (sixty-two fetchable; questionnaire is listed but 404s), scrollWidth, clientWidth, offsetWidth, scrollLeft, scrollTop, ResizeObserver and MutationObserver are every one of them a zero hit. The word overflow appears in twenty-two components and is a Tailwind class each time — something that clips or scrolls, never a measurement of whether anything is spilling. scroll-area itself is a forty-nine-line Radix wrapper that restyles the scrollbar: no overflow detection, no edge shading, and one tabIndex in the whole registry (in sidebar). The version everyone writes first listens for scroll alone, and that is precisely backwards. It means the fade is missing until you scroll, and the moment the fade earns its keep is the one before anybody has touched the box: a table that arrives from a fetch already too wide, a sidebar opening and squeezing the page, a filter that adds a column, a details row expanding, a panel inside a tab that was display:none when it mounted. It looks right in development, where you scroll the thing you just built, and it is wrong on arrival for everyone else. So the position is re-read from five other places besides the scroll event, each covering a way the answer changes without one: the box being resized (which also covers it becoming visible, since that is a resize from zero), a child changing its own size, nodes added or removed anywhere inside, an image or iframe finishing loading (caught in the capture phase, since load does not bubble), and a web font swapping in and re-flowing every line. All of them funnel through one requestAnimationFrame, so a burst of mutations costs a single measurement, and state changes only when one of six booleans does — scrolling a long table end to end re-renders twice, not once a frame. Two measurement bugs are fixed that survive most rewrites. The first is right-to-left: the CSSOM puts scrollLeft at 0 at the initial position of an RTL scroller and runs it negative going left, so a fresh Arabic or Hebrew table reports scrollLeft === 0 with half its columns hidden off to the left — and every implementation that reads 0 as "nothing behind us" paints the fade on the wrong side, on first paint, where nobody is looking for it. The writing direction is resolved once, and only when there is horizontal overflow to resolve it for. The second is sub-pixel: layout is fractional while scrollWidth and clientWidth are rounded integers, so a box scrolled fully to the end lands a few tenths of a pixel short and a `hidden > 0` test leaves the end fade painted over the last column forever, on exactly the screens the author does not own. A one-pixel threshold settles it, and it is a prop. The fade is a mask on the content, not a gradient laid over it. An overlay has to be painted in the page background colour to look like a fade, so it has to be told what that colour is — and it is then wrong inside a card, wrong on a striped table, wrong over an image, and wrong in dark mode the day someone adds one, because the gradient stop was hardcoded once and never re-checked. A mask makes the content itself fall away, which is correct on every background without being told about any of them. When a box fits, no mask is emitted at all rather than an all-opaque one, since masking costs a stacking context and a composited layer; and when both axes scroll the two gradients are combined with mask-composite: intersect, because the default is add and two layers each opaque down their own middle would union into a mask that fades nothing but the four corners. It also takes a tab stop only while it actually scrolls. A div with overflow: auto and no focusable content inside cannot be reached and therefore cannot be scrolled without a mouse — a plain table of text or a wide code block is simply unavailable to a keyboard, which is what axe reports as scrollable-region-focusable. The usual fix is a permanent tabIndex={0}, which buys that at the cost of a dead tab stop on every one of these boxes that happens to fit; since the overflow is already being measured, the stop can exist exactly when it is useful. Pass aria-label and the box is announced as a named region as well, so a screen reader user is told what they have landed in rather than an anonymous group. The focus ring is drawn on the wrapper, outside the mask, because a ring on the masked element would fade out along with the content at the very edges it is meant to trace. The API is orientation ("horizontal", "vertical" or the default "both", which fades whichever axis turns out to scroll), size for the fade length, threshold, focusable, viewportRef and viewportClassName for the scrolling element itself, onEdgesChange, and data-more-top / -right / -bottom / -left plus data-scrollable on the wrapper so you can hang an arrow button or a shadow of your own off CSS. The unused axis is set to hidden rather than left visible, because CSS promotes visible back to auto as soon as the other axis is not, and a horizontal scroller written as overflow-x-auto alone grows a vertical scrollbar the first time a cell wraps. The wrapper is a column flex box so that a max-height put on it actually reaches the scroller instead of spilling, and it shrinks to zero in both axes so the widest table cell cannot dictate the width of the page around it. useScrollEdges, scrollShadowMask and the pure readScrollEdges are exported for a scroller you lay out yourself. Within pulld it is the piece that goes around virtual-list, table-like content and code-block; it shares its measure-then-observe discipline with scroll-progress, which reads how far down a page a reader is rather than what is hidden past an edge, and it is distinct from infinite-scroll, which loads more rows when you reach the end where this only says that an end is not yet reached. One file, no dependencies at all, and no colour of its own — the fade is the absence of paint, so light and dark follow for free.
Search field with a leading magnifier icon and a trailing clear (✕) button that appears as soon as there is text, empties the field, and puts focus back so typing can continue. Use it above filterable lists and data tables, in sidebars and settings pages, over dropdown and combobox options, for docs and help search, for admin record lookup, and anywhere a "/" shortcut focuses a search box. Common asks it answers: "search input", "search bar", "search box", "filter input", "clearable input", "input with a clear button", "search field with icon", "type to filter a list", "table search box", "searchbar component". shadcn/ui has no search field: its input is a bare styled <input>, and its input-group is a layout kit of six parts (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) that pulls in button, input and textarea and hands you slots to hang your own icon and clear control in — you still write the clear button, the show-it-only-when-there-is-text rule, the refocus, and the event plumbing. This is that already assembled, in one import. The part that is easy to get wrong is clearing. Assigning to the input's value does not make React's onChange fire, so a hand-rolled clear button empties the box while the list behind it stays filtered on the old query. This writes through the native value setter and dispatches a bubbling input event, so onChange fires for controlled and uncontrolled usage alike and whatever filtering it drives actually updates. It also hides the WebKit search-cancel button so there is not a second ✕ beside the first, keeps the icon out of the accessibility tree and out of pointer events, gives the clear control a screen-reader label, and passes only one of value/defaultValue through so React never warns about a field switching between controlled and uncontrolled. The clear button is deliberately left out of the tab order, so Tab moves on to the next field instead of into a control that duplicates select-all-and-delete. Standard input props and a forwarded ref pass straight through, so a "/" hotkey can focus it. Theme-aware via shadcn tokens; depends only on lucide-react.
A row of 2–4 mutually exclusive choices drawn as one moving pill on a shared track — the iOS-style segmented control, and what most dashboards use to switch a view or a range without navigating anywhere. Reach for it wherever a single setting has a handful of choices that all fit on screen at once: List/Grid/Board, Day/Week/Month or 24h/7d/30d above a chart, Light/Dark/System, °C/°F, Monthly/Yearly on a pricing page, Newest/Oldest, All/Active/Archived, Preview/Code on a docs example, Table/JSON on a response viewer. Common asks it answers: "segmented control", "segmented button", "iOS segmented control", "pill toggle", "toggle switcher", "view switcher", "time range switcher", "chart period selector", "sort or filter toggle", "unit toggle", "tabs without panels", "Ant Design Segmented", "MUI ToggleButtonGroup" — the control usually faked with a row of buttons and a useState. How it differs from the neighbours official shadcn/ui ships: tabs and toggle-group each pull in a Radix package (@radix-ui/react-tabs, @radix-ui/react-toggle-group), and button-group is a layout wrapper with no selection of its own. This is one file with no dependencies, and it is a real radio group — role=radiogroup on the track, role=radio and aria-checked on every segment — so assistive technology announces one setting with a selected option among several rather than a row of unrelated buttons. Tabs additionally owns panels and the tab/tabpanel relationship, which is the wrong contract when the choice only filters or reframes data already on the page, and a switch only covers two states. The keyboard follows the radio pattern rather than the button one: arrow keys (left/right and up/down) move and select in a single press and wrap around the ends, Home/End jump to the first and last usable segment, disabled segments are stepped over instead of trapping focus, and a roving tabindex keeps the whole group one tab stop with the selected segment as the entry point. Also: per-segment disabling as well as a whole-group disabled state, controlled or uncontrolled through a string value with onValueChange, a focus-visible ring, and shadcn tokens throughout so it follows the theme in light and dark.
A share control that opens the device's native share sheet and quietly degrades to copying the link where that sheet does not exist. Reach for it wherever a page is worth passing on: an article or blog post, a product or listing, an invite or referral link, a public dashboard or report, a job posting, an event page, a receipt or order confirmation, a shared document or file link, a playlist or video, a profile, a support ticket a user wants to forward, or a generated QR code's target URL. Common asks it answers: 'share button react', 'web share api react', 'navigator.share button', 'native share sheet shadcn', 'share to whatsapp twitter react', 'share button with copy fallback', 'react-share alternative', 'shadcn share component'. shadcn/ui has nothing that touches navigator.share — not in button, not anywhere in its sixty-odd components — so this is written inline every time, and the inline version gets three things wrong that only show up on someone else's device. First, it assumes the API is there. navigator.share is absent on desktop Firefox, absent on any insecure origin, and present or missing on desktop Chrome depending on the OS, so the button has to have a real answer for 'no sheet' rather than throwing: here an unsupported browser, a blocked payload, or any non-cancel rejection falls through to navigator.clipboard.writeText and the label says 'Link copied', the same feedback pulld copy-button gives. Second, it awaits something first. navigator.share only resolves while the click is still the active user gesture, so building the URL or fetching a short link before calling it makes the share reject with NotAllowedError on a button that worked fine in development; this component calls share synchronously as the first thing in the handler and takes an already-resolved url prop, so pass a URL you have, not a promise. Third, it reports a cancel as an error. Dismissing the sheet rejects with AbortError, and a naive catch shows 'Sharing failed' to someone who simply closed it — here AbortError resets the button silently and reports 'cancelled' to onShare instead. The API: url (defaults to the current page URL, read at click time), shareTitle and shareText for the sheet (the native title attribute stays a tooltip), children for the resting label, timeout (default 2000ms) for how long the result state stays, and onShare(outcome) with 'shared' | 'copied' | 'cancelled' | 'failed' for analytics. Feature detection never changes what is rendered, only what the click does, so the server and the first client render agree and there is no hydration mismatch. type='button' so it will not submit a surrounding form, the result is announced through an sr-only aria-live region rather than through the icon swap, both icons are aria-hidden, and the reset timer is cleared on unmount. Styled with shadcn tokens (input, accent, ring) so it follows light and dark mode, className merges rather than fights, and the focus ring is focus-visible. One file, two lucide icons, no share library.
The box at the bottom of a form where somebody signs with a finger, a mouse or a stylus — and, beside it, the field where somebody who cannot draw types their name instead. Reach for it wherever a screen asks for assent that is meant to bind: a delivery or handover receipt, a rental or equipment checkout, a treatment or research consent form, a waiver and liability release, a visitor or contractor sign-in at a front desk, a timesheet or job-completion sheet a technician gets signed on a tablet, a lease or invoice approval, a school permission slip, and the light end of e-signature where a full contract platform is far more than the job needs. Common asks it answers: "signature pad react", "react signature canvas", "draw signature component", "e-signature input react", "sign here box react", "capture signature on tablet", "signature-pad alternative", "react-signature-canvas alternative", "shadcn signature pad", "shadcn signature input", "canvas drawing react component", "why is my canvas blurry", "canvas blurry on retina", "devicePixelRatio canvas react", "canvas high dpi scaling", "smooth line drawing canvas", "canvas drawing looks jagged", "pointermove skipping points", "getCoalescedEvents react", "line breaks when mouse leaves canvas", "setPointerCapture drawing", "canvas drawing scrolls page on mobile", "touch-action none canvas", "signature to png data url", "export canvas as svg", "trim whitespace around signature", "transparent signature png invisible", "accessible signature field", "signature pad screen reader", "canvas accessibility alternative", "署名 パッド react", "サイン 入力 canvas", "canvas がぼやける retina". Official shadcn/ui has nothing to build this from, and the measurement is not close: fetching all sixty-three registry entries today (sixty-two are fetchable — questionnaire is listed and 404s on both style tracks) and grepping 240 KB of source, getContext, toDataURL, toBlob, pointerdown, pointermove, setPointerCapture, getCoalescedEvents, devicePixelRatio, beginPath, lineTo, quadraticCurveTo, touch-action and signature are every one of them a zero hit. The only match for canvas at all is ten occurrences in sidebar, and every one is the Tailwind variant name offcanvas. There is no component in official shadcn that draws — not one — so an agent asked for a signature field builds it from a bare canvas and a mousemove listener, and the four things that make this hard are exactly the four it will get wrong. The first is that a canvas has two sizes and the wrong one is the obvious one. The CSS size is how big the element looks; the width and height attributes are how many pixels actually exist, and they stay at 300x150 no matter what the stylesheet says. Left alone, a phone at 3x renders the box at a third of its own resolution and scales the result up, so the strokes come out soft — and a signature is a thin line whose weight and wobble are the whole of what identifies it, which makes this the one component where blur is not cosmetic. The attributes are set to the CSS size times the device pixel ratio, capped so a 4x screen does not allocate sixteen pixels of memory per CSS pixel, and the context is set back to CSS coordinates with setTransform rather than scale — scale multiplies into the transform already there, so a pad that survives two resizes draws at 4x and the signature walks off the box. The second is that pointermove is not the pointer. One move event is delivered per animation frame, but the digitiser sampled the pen many times inside that frame and the browser keeps the ones it skipped. A quick signature is where it shows: at 60Hz a fast flick is four or five points and comes out as a zigzag, while getCoalescedEvents returns the twenty that were really seen. Safari has been observed returning an empty list, so the event itself is the fallback rather than the assumption. Sampling alone is still not enough, because the corners in a joined-up polyline are in the sample rate rather than in the hand: each sample becomes the control point of a quadratic and the curve runs through the midpoints between them, so the line is tangent to the path the hand took and has no corners of its own. During a stroke only the newest segment is drawn, so a long signature costs the same per sample as a short one. The third is that a canvas is a blank to a screen reader, and no aria-label fixes it — the label names the box, and the task is to make a mark inside it. Drawing is a pointer gesture, so a pad that only draws is a form that a keyboard, switch or screen reader user cannot complete, on precisely the documents where being unable to complete it has consequences. A typed full name is the equivalent that is already recognised in practice, so it sits beside the box as a real labelled field rather than as a fallback bolted on: either path produces a value and an image, typing renders in a script face on the canvas so a sighted user sees the mark too, and the two are mutually exclusive because there is one signature. required is handled the same way round: it goes on the typed field while nothing is signed and lifts the moment something is drawn — never on the hidden input, which is barred from constraint validation outright, so the attribute parses, the browser ignores it, and the form submits unsigned. The fourth is that the line breaks when the hand leaves the box, which is where the descender of a real signature goes. setPointerCapture keeps the events coming until the pointer lifts, wherever that happens; the capture is checked before being released, because releasing one that pointercancel already took throws; a second finger landing mid-stroke is ignored rather than allowed to overwrite the stroke in progress; and touch-action is none, or the first downward stroke on a phone scrolls the page instead of drawing while the browser waits to find out which was meant. Strokes are kept as data, not as pixels, which is what makes the rest work. Undo is a slice. A resize — a sidebar opening, a tab becoming visible, a container query firing, none of them a window resize — is observed with a ResizeObserver and replayed, where a pixel-only pad loses the signature to the attribute assignment that resizes it. And the export is resolution-independent: signatureToSvg emits a real SVG document, because a signature is stored small and shown large, printed onto a contract or scaled into a PDF, and a raster of a 160-pixel box has one resolution forever. trim crops to the ink so a stored signature is not mostly empty box, with the bound taken over the Bézier control points — a quadratic stays inside the triangle of its own three, so it is exact without solving the curve — plus half the pen width, without which the trim slices the outermost stroke in half. toDataURL is there for the APIs that want a raster, and its background is documented rather than assumed: a transparent PNG of black ink is invisible the moment it lands on anything dark. Every string that reaches the SVG is escaped, since a name is free text and a document built by concatenation is the oldest bug there is. The API: value or defaultValue as a discriminated { type: "drawn", strokes, width, height } or { type: "typed", name } — the drawn form carries the box it was drawn in, because strokes are CSS pixels and without the box they cannot be laid out again on the receipt screen that shows them later. Plus onChange, penColor which defaults to the theme's own text colour, penWidth, height, maxPixelRatio, disabled, allowTyped, required, and name to post the signature through a plain HTML form as an SVG data URL. The ref exposes clear, undo, isEmpty, getValue, toSVG and toDataURL for a submit handler. strokeGeometry, strokePathData, signatureBounds, isSignatureEmpty, signatureToSvg and backingSize are exported as pure functions, so a stored signature can be rendered on a server that has no canvas at all. Within pulld it is the first component that draws: file-dropzone takes an image in and upload-list lists what arrived, but nothing until now made a mark. It sits next to type-to-confirm, which is the other way a screen asks somebody to mean it — typing a phrase to authorise a destructive action, where this captures assent that gets stored and shown back. One file, zero dependencies, not even an icon, and every colour is a shadcn token, so light and dark follow on their own.
A URL slug field that fills itself in from a title and then gets out of the way. Use it wherever a record needs a URL: the permalink or slug field in a blog post editor or CMS admin form, a page or docs route segment, a product handle, a workspace or team URL, a category or tag slug, a public profile handle. Type a title, watch the slug appear as kebab-case, and edit it whenever you want — this is the part hand-rolled fields get wrong. It keeps deriving only while the field still holds exactly what it generated, so editing the slug by hand, or loading an existing slug from your database, stops the derivation for good: renaming a published post cannot silently change its URL. Leave the field empty and blur, and it goes back to following the title. Every keystroke is sanitised in place — lowercased, spaces and punctuation collapsed to a single hyphen (or underscore), accents folded away — while the caret stays exactly where you were typing, which is what breaks when you naively assign a transformed value back to a controlled input. Unicode is handled rather than mangled: NFKD folding turns "Café au lait" into cafe-au-lait, "Łódź" into lodz, and the letters decomposition leaves whole are spelled out ("Straße" becomes strasse, not strae). Pass allowUnicode to keep the title's own script instead — without it a Japanese, Chinese, Korean, Greek, Cyrillic, Hebrew or Arabic title slugifies to an empty string, and with it combining marks stay attached to their letter, so がっこう does not quietly become かっこう. maxLength cuts a generated slug back to a whole word rather than mid-syllable, apostrophes disappear instead of splitting words ("don't panic" becomes dont-panic), and pasting a full URL takes just its last path segment. Zero dependencies, one import, shadcn tokens, optional prefix like example.com/blog/ wired to the input with aria-describedby. Official shadcn/ui has no slug or permalink field — its input is a bare element and input-group is an assembly kit with no logic in it — and a slugify npm package solves the string, not the field: the caret, the do-not-stomp rule and the typing-in-progress state are what this component is.
Drag-to-reorder list: grab a row's grip handle and drop it in a new place — and do the same thing from the keyboard, which is the half that is normally missing. Reach for it whenever the order itself is the data: reordering tasks or a to-do list, ranking priorities, choices or search results, arranging table columns, form fields or a form builder's questions, dashboard widgets, nav and sidebar links, playlist tracks, an image or gallery order, quiz question order, steps in a workflow, checklist or recipe, saved filters and views, a queue of jobs, and the cards inside one kanban column. Common asks it answers: "sortable list react", "drag and drop list react", "reorderable list", "drag to reorder", "drag handle list", "reorder items react", "draggable list order", "move item up and down", "shadcn drag and drop", "shadcn sortable", "sortable list without a library", "drag and drop with no dependencies react", "dnd-kit alternative", "@dnd-kit/sortable simpler", "react-beautiful-dnd replacement", "react-beautiful-dnd is deprecated what now", "react-sortable-hoc alternative", "SortableJS react", "framer-motion Reorder alternative", "accessible drag and drop react", "keyboard accessible reorder list", "drag and drop is not keyboard accessible", "screen reader drag and drop announcements", "aria-live drag and drop", "roving tabindex list", "html5 drag and drop does not work on touch", "drag to reorder on mobile", "reorder list with different row heights", "next.js drag and drop list", "並び替え ドラッグ react". Official shadcn/ui ships nothing that reorders, and it is not close: across all sixty-three of its registry entries (sixty-two fetchable; questionnaire is listed but 404s), sortable, draggable, dnd, reorder, dragstart, onDragStart and pointerdown are every one of them a zero hit — and so are aria-live and roving, which is the other half of the problem. So this gets hand-rolled each time, and the keyboard is what gets dropped, because a mouse-only reorder looks finished. Here the whole interaction works without a mouse: Tab reaches the list once through a roving tabindex, arrow keys walk it, Space or Enter picks a row up, arrows move the picked-up row, Space or Enter drops it, Escape puts it back where it started, and each step is spoken through an assertive live region ("Picked up Design review. Position 2 of 5."). Every announcement — the handle label, the instructions, and the grabbed, moved, dropped and cancelled sentences — is an overridable function, so it translates. Dragging is plain pointer events: no dnd-kit, no react-dnd, no HTML5 drag-and-drop. That is what makes touch work at all (the HTML5 API has never fired on a phone), and it is why rows of different heights land exactly where they look like they will — every row is measured once at the start of a drag and displaced with transforms rather than guessed at from a uniform row height. Controlled: pass items ({ id, label } plus whatever else you carry) and persist the array onReorder hands back; renderItem draws the row body beside the handle and is told the row's index and whether it is being dragged or grabbed, and itemClassName styles the row. Within pulld it is the ordering counterpart to upload-list and bulk-action-bar, which act on rows rather than arrange them, and to tree-view, which shows a hierarchy rather than a sequence. One file, and its only dependency is lucide-react for the grip icon.
One sortable column heading for a table you already have: press it and the rows re-order, press it again for the other direction, press it a third time and the table goes back to the order it arrived in. Reach for it wherever a list of rows is long enough that people want it arranged their own way — an admin users or accounts table, an orders, invoices or billing-history list, search results, a dashboard's data table, a logs or audit-trail view, a file or document browser, a products or inventory grid, a leaderboard or ranking, a tickets/issues queue, transactions, and any report with a date, size, count or amount column. Common asks it answers: "sortable table header react", "table column sorting react", "click column header to sort", "sort table by column react", "shadcn sortable table", "shadcn table sorting", "shadcn data table sort without tanstack", "tanstack table alternative for just sorting", "react-table is too much for one column", "sort indicator arrow table header", "three state sort asc desc none", "how to clear sorting in a table", "reset table sort to default order", "aria-sort react", "accessible sortable table", "screen reader table sorting", "aria-sort not announced", "two columns both say ascending", "table header button accessibility", "th onclick not keyboard accessible", "sort numbers in strings correctly", "Item 10 sorts before Item 2", "natural sort react", "localeCompare numeric true", "Intl.Collator sort table", "accented names sort to the bottom", "null values first when sorting descending", "empty cells at top when sorting", "sorting mutates state array react", "table not re-rendering after sort", "テーブル ソート 見出し react", "並び替え カラム". Official shadcn/ui gives you nothing to start from here, and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 211,725 bytes of source, aria-sort, ariaSort, onSort, sortable, sortBy, sortDirection, ascending, descending, localeCompare, Intl.Collator, toSorted, ArrowUpDown and ChevronsUpDown are every one of them a zero hit. Its table component is 2,859 bytes of six styling wrappers in which the word sort never appears once. So every sortable table gets hand-rolled, and three things go wrong each time. The first is that the sort has two states when it needs three. Flipping between ascending and descending quietly takes something away: once a column has been pressed, the order the table came in — almost always the meaningful one, newest first, or a relevance rank the server computed — cannot be got back. There is no third press, no button that says stop sorting, and reloading the page is the only way out, which is exactly what people do. Here the cycle is first direction, opposite, gone, and gone hands back null so you render the rows as they came. Which direction comes first is per column, because ascending is the wrong first guess for a date, a size, a count or a score, where the first press is meant to mean newest or biggest and answers with the oldest and smallest instead. The second is aria-sort, which is not an attribute saying a column can be sorted — it says how the table is ordered right now, so it belongs on exactly one header at a time and reads none on the others. Held as a flag on each header, the failure always has the same shape: pressing a second column sets the new one and forgets to unset the old, and a screen reader is told two different columns are each sorting the table, which looks perfectly fine on screen. This takes the whole table's sort state as one value and derives each header's share of it, so that state cannot be represented. It also puts the attribute on the <th> rather than on the button inside it: aria-sort is defined for columnheader, and on a role=button it is silently dropped — it validates, it looks done, and it announces nothing. The third is the hit area. Putting onClick on a <th> gives a table that sorts with a mouse and for nobody else: nothing to tab to, nothing answering Enter or Space, and a cell that gives no hint it does anything, because a <th> is not interactive and no amount of ARIA makes it so. The heading here is a real button stretched across the cell — the cell gives up its padding to it — so it gets the keyboard, the focus ring and the announcement for free, and the pressable area matches the thing that looks pressable. Sorting a table also announces nothing on its own: the rows are replaced, focus has not moved, and aria-sort changing on an element is not an event any reader speaks. So a press puts "Table sorted by Name, ascending." through a polite live region — from the header that was actually pressed, not from the one that just lost the sort — and clears it again a moment later, since a live region inside a header cell would otherwise become part of that cell's own content and be read back on every future visit. The comparison is exported separately as sortRows, compareValues and isBlankValue, because the browser and the server that paginates the same table have to agree on what the order is. Descending is the negated comparison, never the ascending result reversed: reversing floats every empty cell to the top the moment the arrow flips, and scrambles the rows that tied on the way up, so the same three Pending rows appear in a different order each way and the table reads as if it is shuffling itself. Blank cells — null, undefined, empty string, NaN, an invalid Date, but not 0 and not false — stay at the bottom in both directions. Text goes through Intl.Collator with numeric ordering, so Item 2 comes before Item 10 and Ångström does not land below Zulu where nobody scrolls; dates compare as instants and booleans as false-then-true. It never sorts the array you pass it, which is the bug that renders nothing at all, and it is meant to be run over the rows the server sent rather than the ones already on screen — sorting what is displayed lets the previous sort survive inside every group of ties, so the table depends on the order of presses rather than on its state. A disabled heading keeps its tab stop and ignores presses instead of taking the disabled attribute, which would drop it out of the tab order and lose a keyboard user's place the moment the table starts loading. align="end" right-aligns a numeric column and moves the arrow to the label's left so the heading stays flush with the figures beneath it, and the unsorted double-chevron is visible at rest rather than on hover, because hover does not exist on a phone and it is the only thing saying the column can be sorted at all. useSortHeader, nextSortState and ariaSortFor are exported for a header laid out as divs or one that already holds a filter menu. Within pulld it is the table counterpart to sortable-list, which drags rows into an order a person chooses rather than computing one, and it sits beside scroll-shadow and virtual-list, the other two pieces a long table wants. One file, one column, your <table> stays yours, and its only dependency is lucide-react for the arrows.
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.
A press-to-dictate microphone button that fills a text field by voice, and stops claiming to listen the moment the browser has stopped. Reach for it beside any field somebody would rather speak than type: a search box, a comment or reply box, note and journal fields, a message composer, contact and support forms, meeting and consultation notes, inspection and field-service reports filled in on a phone with gloves or dirty hands, delivery and warehouse notes, clinical and veterinary notes, incident and maintenance logs, recipe and shopping lists, long-form description fields in a CMS or listing flow, translation and language-practice inputs, and anywhere dictation is the accessible alternative for someone who cannot comfortably type — motor impairment, RSI, a broken wrist, or simply a phone in one hand. Common asks it answers: "speech to text react", "voice input component", "dictation button", "react speech recognition component", "microphone button for input field", "web speech api react hook", "voice typing textarea", "speech to text search box", "react-speech-recognition alternative", "useSpeechRecognition hook", "voice dictation shadcn", "shadcn microphone input", "talk to type react", "record voice fill form", "webkitSpeechRecognition react". Official shadcn/ui has nothing for this and no combination of its parts reaches it: input and textarea are the fields themselves and know nothing about audio, button is a button, and there is no speech, microphone or recording primitive anywhere in the library. Distinct from every other -input component in this registry — otp-input, tag-input, masked-input, phone-input and the rest are the field, while this one sits beside a field you already have and writes into it, so it composes with any of them. The component turns on one fact that every hand-rolled version gets wrong: recognition ends by itself. The browser stops a session after a stretch of silence, and after a while regardless, and the only thing it tells your code is an end event. A button that tracks the boolean its own click set therefore keeps a pulsing red dot and the word Listening over a microphone that was handed back a minute ago, and the user keeps talking into nothing. Here every visible state is driven by the platform's own start, end and error events, the setting the user asked for is kept separate from whether audio is actually being captured (aria-pressed carries the first, data-active the second), and continuous mode starts a fresh session when the browser ends one — under a budget, so a machine with the microphone muted or a laptop that has gone offline cannot turn that into a hot loop, and so a genuine pause in the middle of a paragraph costs nothing. The restart also resets the result cursor, because a new session numbers its results from zero and a cursor carried over from the last one silently swallows the first words after every pause. Interim results are kept strictly out of the value: the service rewrites its guess as more audio arrives, so writing it into the field the user is editing changes their content under them and fills their undo history with words nobody typed — the guess is shown beside the button as a preview and only confirmed text is ever appended. Appending is its own small problem and is solved and exported as appendTranscript, because value + transcript welds every chunk onto the previous word and padding unconditionally with a space yields a stray gap before a dictated full stop. Feature detection reads the prefixed constructor as well as the standard one, since webkitSpeechRecognition is the spelling the browsers that actually ship this expose, and a missing API is a first-class state with its own copy rather than a dead button. The not-allowed error is untangled rather than taken at face value: it means four different things — an http origin, an iframe without allow="microphone", a stored block, and a prompt closed without an answer — and only the third is worth sending someone to their site settings for, so the other three say something true instead, and a permission changed in those settings is picked up live through the Permissions API change event rather than staying dead until a reload. Stop asks the service to deliver what it is still holding instead of aborting and losing the last thing that was said, and unmounting detaches the handlers and aborts, so navigating away inside a single-page app cannot leave the recording indicator lit with no control left that could turn it off. The recognition language is resolved from the document rather than left to the user agent, which is the one setting whose behaviour is not defined across browsers. Worth knowing before shipping it somewhere sensitive: the specification permits the audio to be sent to a remote service — the existence of a network error code is the platform admitting as much — so a dictated field may be data that has left the device. Ships as a hook (useSpeechInput) plus a button, controlled by pairing value with onValueChange or left to report through onTranscript, marked aria-disabled rather than disabled so a refusal stays reachable and can explain itself, with a permanently mounted polite live region for status and failures. Styled entirely with shadcn tokens (input, accent, ring, muted-foreground, destructive), so it follows light and dark mode, and its only dependency is lucide-react for the icons.
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.
The single-number tile at the top of a dashboard: a label, one big value, and an optional percentage change with an up or down arrow — green when the number moved the right way, red when it did not. Use it wherever a screen opens with a row of headline figures: an analytics or metrics dashboard, an admin overview, a KPI or scorecard row, a billing and usage summary, a SaaS home screen, a revenue or traffic report. Common asks it answers: "stat card", "metric card", "KPI card", "metric tile", "summary card", "big number card", "dashboard stat tile", "stats card react", "shadcn dashboard cards", "tailwind stat card", "number card with percentage change", "card with trend indicator", "percentage change badge", "revenue card with trend arrow", "analytics summary cards", "stats row", "show total users with growth", "MRR / ARR card", "active users card", "conversion rate card", "Next.js dashboard stats", "Stripe/Vercel-style dashboard tiles". shadcn/ui ships card as an empty container with no notion of a metric, so the value typography, the delta colouring and the arrow are hand-rolled on every dashboard. Pass `label`, `value` and optionally `delta` (a number: positive renders the up arrow, negative the down arrow, and omitting it renders no delta at all) plus a `hint` line for the comparison period, e.g. "vs. last month". `value` is a ReactNode, not a string, so a pre-formatted currency or an Intl.NumberFormat result drops straight in and the component never guesses at your locale or currency. The direction is not left to colour alone: the arrow is aria-hidden and an sr-only "Up"/"Down" is spoken before the number, so the tile still means something to a screen reader and to a red-green colour-blind reader, which a bare green percentage does not. Composes into a responsive grid to form the stats row, and pairs with gauge and progress-ring when the figure is a ratio rather than a total. Styled with shadcn tokens (card, muted-foreground) with an explicit dark-mode pair for the delta colours; lucide-react is the only dependency. One thing it deliberately does not decide for you: `delta` is read as "up is good", so a positive number is always green. For a metric where rising is bad — churn, bounce rate, error rate, p95 latency, refunds, open incidents, cost per acquisition — pass the change negated (a 2-point rise in churn as `-2`) or wrap the card, rather than expecting it to know which way your metric should move. Distinct from feature-card, which sells a capability with an icon and copy: this one carries a live number.
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.
A table of contents for the page the reader is on, with the section they are currently reading highlighted as they scroll. Use it for the "On this page" rail beside documentation and guides, API references, changelogs and release notes, long blog posts and tutorials, handbooks, legal and policy pages, and reports. Common asks it answers: "table of contents component", "toc sidebar", "on this page nav", "scrollspy", "scroll spy in React", "highlight the active heading while scrolling", "docs right rail", "anchor link navigation", "in-page navigation", "sticky table of contents", "MDX toc", "react-scrollspy alternative". shadcn/ui ships nothing for this, and its navigation-menu and sidebar are for moving between pages, not around one. You pass the headings in as items — the shape rehype-slug, MDX and Contentlayer pipelines already hand you — so the list is rendered on the server and the links work before, and without, JavaScript; only the highlight needs the client. The awkward part is deciding which heading counts as current, and this fixes the three ways a hand-rolled one gets it wrong. The last section is normally shorter than the viewport, so its heading never reaches the activation line and the final entry can never light up — reaching the bottom of the scrollable area selects the last heading, because there is nothing further to read. A section stays current while it is being read rather than only while its heading is on screen, which is where an IntersectionObserver checking is-it-visible goes blank on any section taller than the window. And clicking an entry starts a scroll lasting hundreds of milliseconds, during which every heading it travels past would light up in turn, leaving the entry you clicked as the one thing not highlighted; the list holds your choice until the scroll settles, and hands control straight back if you grab the page mid-flight. offset clears a sticky site header, both for where a click lands and for where the current section begins, since the browser's own fragment jump puts the heading underneath it. It re-measures on resize and once web fonts have loaded, follows a nested scroller when the app shell scrolls an inner element instead of the window, and honours prefers-reduced-motion. The active entry is marked with aria-current="location" rather than colour alone, so it is announced and not merely seen; because clicking has to preventDefault to apply the offset, focus is moved to the heading the way the browser would have, so a keyboard reader lands in the section instead of carrying on down the contents. Modifier and middle clicks are left alone, so opening a section in a new tab still works.
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.
The one-button light/dark switch you drop in a navbar, header or settings row — click it and the whole app flips theme, and the choice survives a reload. Reach for it on any site that has a dark mode at all: the header of a marketing or docs site, an app shell or dashboard sidebar, a settings or appearance page, an admin panel, a developer tool or playground, a blog, and the top-right corner of more or less every React starter. Common asks it answers: "dark mode toggle", "theme toggle button", "light dark switcher", "dark mode toggle react", "theme switcher react", "toggle dark mode tailwind", "tailwind dark class toggle", "sun moon toggle button", "dark mode without next-themes", "next-themes alternative", "theme toggle shadcn", "shadcn dark mode toggle component", "shadcn mode toggle", "nextjs dark mode toggle", "vite react dark mode", "react router dark mode", "astro dark mode toggle", "remember user theme localStorage", "respect prefers-color-scheme", "system theme default dark mode", "dark mode flash on page load", "FOUC dark mode nextjs", "hydration mismatch dark mode", "window is not defined dark mode", "toggle dark class on html element", "dark mode toggle without provider", "dark mode toggle accessibility aria-pressed", "ダークモード 切り替え react", "テーマ切り替えボタン", "ダークモード 初回表示 ちらつき", "システム設定に追従 ダークモード". shadcn/ui has no installable toggle, and the measurement is unambiguous: across all sixty-three registry entries (sixty-two fetchable today) the only component that touches theming at all is sonner, which calls next-themes' useTheme to decide what colour to paint a toast — it reads the theme, it does not switch it. The official dark-mode guide is a guide: it hands you a next-themes provider to wire up and a dropdown to assemble yourself, so the button itself is written by hand every time. This is that button — one file, no provider, no context, no next-themes, no extra package. It toggles the `dark` class on the html element, which is exactly what Tailwind's class dark mode and the shadcn tokens already read, so it works with the theme you have rather than introducing another one. On first load it reads the saved choice from localStorage and falls back to the OS `prefers-color-scheme`, so a first-time visitor gets their system theme and a returning one gets their own; every later click writes the choice back. That first read happens in an effect rather than during render, because `window` does not exist on the server and an inline branch would either crash SSR or hydrate to different markup than it sent — which is the `window is not defined` / hydration-mismatch pair that the hand-written version hits first. It also means the theme is applied just after first paint, so add the usual one-line script in your document head if you need to kill the flash on a static page. The Sun/Moon swap is done with the `dark:` variant rather than JS state, so the icon matches the document even if something else on the page changes the theme. It is a real button that forwards every button prop (className, id, onClick, disabled), carries an aria-label and an aria-pressed that reflects the current mode, hides both icons from screen readers, and has a focus-visible ring; styling uses shadcn tokens (accent, muted-foreground, ring, border) so it matches your other icon buttons. Within pulld it sits with the other things that live in a header: kbd and keyboard-shortcuts, command-palette, network-status and announcement-bar. lucide-react is the only dependency.
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.
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.
A vertical timeline: a list of events drawn as dots on a connecting line, each with an optional timestamp, title, description and icon. Reach for it wherever the content itself is "what happened, and in what order": an activity feed on a record, profile or dashboard; an audit, security or admin history log; a changelog or release-notes page; order, shipment and delivery tracking; a deploy, build or CI/CD run log; incident updates on a status page; a support ticket's history; approval and review trails; the event stream on an order, invoice or subscription; notification history; a product roadmap; and the experience or education list on a resume or "about" page. Common asks it answers: "timeline component", "vertical timeline", "activity feed", "activity timeline", "history log component", "audit log UI", "changelog timeline", "release notes timeline", "order tracking timeline", "shipment tracking UI", "deploy history list", "event stream component", "status timeline", "shadcn timeline", "shadcn activity feed", "react vertical timeline alternative", "react-chrono alternative", "MUI Timeline equivalent", "antd Timeline equivalent", "resume timeline". shadcn/ui ships no timeline: fetching the source of all 63 items in its registry and grepping them turns up no timeline, no <time> element and no connector line anywhere. The nearest things it has are marker — one annotation row of an icon beside muted text, with a variant that rules a line across it, the "New messages" divider in a chat — and item, a kit for laying out a single row; neither stacks events on a shared line nor carries a time. It is also not step-indicator, the other dots-on-a-line component here: a step indicator counts position through a sequence that is known in advance and still to be finished, while a timeline is the record of what already happened and has no current step. Pass an items array (title, optional time, description, icon and a colour accent). The timestamp renders as a semantic <time> element with a machine-readable dateTime, so it is legible to a reader and to a crawler; the connecting line and the decorative dots are aria-hidden, so a screen reader hears the events rather than the ornament; and each marker takes a per-item colour (muted, primary, success, warning, destructive), so status like succeeded, failed or pending can be flagged without reaching for a second component. Pass an icon to render an icon badge instead of a plain dot. It is a pure display component with no state, so it renders inside a server component with no "use client" and costs nothing on the client. Theme-aware via shadcn tokens with dark mode; no dependencies (bring your own icons).
A time zone picker: every IANA zone the browser knows, grouped by region and labelled with the UTC offset it is actually on — "New York (UTC-04:00)", "Kolkata (UTC+05:30)", "Chatham (UTC+12:45)". Reach for it wherever an app has to store which zone a time is meant in: the "Your time zone" row in profile, account or notification settings; a workspace or organisation default for a distributed team; scheduling and booking flows where the two parties are in different places; meeting, event and webinar creation; availability and working-hours editors; quiet hours and do-not-disturb windows; shift rosters and on-call rotations; the zone a cron job, scheduled report, digest email or CI job is read in; billing and invoice cycle boundaries; the "display times in" control on a dashboard, log viewer or analytics report; and any form that already collects a date and needs to know which midnight it meant. Common asks it answers: "timezone picker", "timezone select", "time zone dropdown", "timezone selector react", "IANA timezone select", "select timezone component", "list of timezones react", "timezone select with UTC offset", "shadcn timezone picker", "shadcn time zone select", "react-timezone-select alternative", "timezone combobox", "choose timezone for scheduling", "user timezone setting component". Official shadcn/ui has nothing for this and no combination of its parts gets there: select, native-select and combobox are empty controls that know no zones, and calendar and date-picker choose a day and never say which zone that day is counted in. The component here is the data and the labelling, not the control. The zone list comes from `Intl.supportedValuesOf("timeZone")`, so it is the runtime's own tzdata — 418 zones on current browsers — and it ages with the browser instead of with a package you have to remember to bump. UTC is added explicitly, because that call omits it on several runtimes and it is the one zone a scheduling or logging UI is most likely to want. Offsets are read through `Intl` at a reference date rather than computed by subtracting two Dates, which is what keeps the zones that are not on a whole hour honest: India at +05:30, Chatham at +12:45, Marquesas at -09:30. And because an offset is a property of the date and not of the zone — Berlin is +01:00 in January and +02:00 in July — `referenceDate` moves the whole list to the instant being scheduled, so a picker for a meeting in three months does not label its options with today's daylight saving. It renders a native `<select>`, so keyboard support, the mobile wheel and form submission come from the platform rather than from a listbox reimplementation, which for a list this long is the difference between usable and not. That choice decides the labels too: a native select's only search is type-ahead, and labelling the options "(UTC-04:00) New York" the way most pickers do points all 418 entries at "(" and throws the feature away — so the city comes first, the offset trails in parentheses, and each region group is sorted alphabetically, in the same order type-ahead walks. Three failure modes it settles that only show up in production. The option list is built after mount, never during the server render, because the zone list, the tzdata behind the offsets and "now" are all properties of the machine — rendering them on both sides is a hydration mismatch on a page that was otherwise deterministic; before mount the field renders the current value under its raw id, so it is still present and submittable. The select is controlled internally even when the caller leaves it uncontrolled, because replacing the children of an uncontrolled select drops the DOM's selection and the field would silently reset on hydration. And a value the list does not contain is added back as its own option — the runtime offers canonical ids only, so a legacy form saved years ago is absent (current runtimes still answer to "US/Pacific" but do not list it), as is any zone picked before a narrowed list was narrowed instead of letting the select fall to its first entry and read as though the user had picked Abidjan. Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`), always emitting the IANA id and never a display label; `placeholder` adds an empty first option that `required` still rejects; `timeZones` narrows the list to the places a product actually operates in; and `getLocalTimeZone()` is exported for seeding the field with the visitor's own zone from an effect rather than from a render the server also runs. Labelled for assistive technology either way: it falls back to an accessible name only when no `aria-label`, `aria-labelledby` or `id` says one already exists, so a visible `<Label htmlFor>` is never overridden. Styled entirely with shadcn tokens (input, ring, muted-foreground), so it follows light and dark mode, and it ships zero dependencies — no timezone package, no icon package, one file.
A complete toast / notification system in one file: call `toast()` — or `toast.success` / `.error` / `.info` / `.warning` / `.loading` / `.promise` — from anywhere in your app, and render a single `<Toaster />` at the root. No provider, no context, nothing to wire up. Use it for save and delete confirmations, form submission results, copy-to-clipboard feedback, async job status, optimistic updates that may fail, undo prompts, rate-limit and validation errors — any "it worked" or "it failed" message that should not interrupt what the user is doing. Common asks it answers: "toast notification react", "toast component shadcn", "snackbar component", "notification popup", "flash message", "alert toast", "undo toast with action button", "promise toast for async requests", "loading toast that turns into success", "toast without a provider", "toast from outside a component", "sonner alternative", "react-hot-toast alternative", "react-toastify alternative", "notification system react", "show a message after form submit". `toast.promise(request, { loading, success, error })` moves one toast through all three states in place, so an async call needs a single line instead of a chain of manual dismissals — `success` and `error` also accept a function, so the final message can quote the resolved value or the thrown error. Every toast takes a `description`, an `action` button (the undo affordance), a `duration` where `Infinity` pins it until dismissed, and an `id` you can reuse to update a toast already on screen. `<Toaster />` takes any of six positions. The queue lives outside React through useSyncExternalStore, so a toast can be fired from an event handler, a fetch or axios interceptor, a route guard, or a plain module — the places a hook-based API cannot reach, and the usual reason a toast library ends up wrapped in a context that has to be threaded everywhere. The details a rushed implementation drops: auto-dismiss pauses while the pointer is over the stack or focus is inside it, so a toast cannot vanish mid-sentence or while a keyboard user is reaching for its action button, and it stays paused across a promise's loading → success swap. Errors announce assertive and everything else polite, so a failure is not queued behind three success messages. Swipe-to-dismiss on touch, and enter / exit animations that respect prefers-reduced-motion. Official shadcn/ui offers two, and both hand you a package to carry: its toast is built on @radix-ui/react-toast and only works once you have mounted a ToastProvider and a ToastViewport and threaded its useToast hook to every caller, and its sonner is a wrapper around the sonner package plus next-themes. This is one file you own and can edit, styled with your own theme tokens, with no provider to mount and no context to thread, and lucide-react — already present in a shadcn project — as its only package import.
The nested list you can open, close and walk with the arrow keys: a file explorer or file tree, a folder or directory tree, a category or taxonomy picker, an org chart, an API-schema browser, a docs sidebar with nested sections. Common asks it answers: "tree view", "tree component", "file tree", "folder tree", "directory tree", "file explorer sidebar", "nested list with expand/collapse", "collapsible tree", "expandable folder list", "VS Code-style explorer", "category tree", "org chart tree". shadcn/ui ships no tree of any kind — its collapsible is one open/closed section and its sidebar nests menus without the tree semantics — so this gets hand-rolled every time, and the part that gets dropped is always the keyboard. Pass a `data` array of `{ id, label, children?, icon? }`: a node with a `children` array is a parent (an empty array is an empty folder, which still opens), a node without one is a leaf. Open state and selection are each controlled (`expandedIds` / `selectedId` plus `onExpandedChange` / `onSelect`, which hands you the whole node) or uncontrolled (`defaultExpandedIds` / `defaultSelectedId`), so it drops into a router-driven sidebar or runs on its own. It is the real ARIA tree pattern, not a pile of nested collapsibles: role=tree / treeitem / group with aria-expanded, aria-selected and aria-level/posinset/setsize, and a roving tabindex so the whole tree is one Tab stop instead of one stop per row. Up/Down walk only the rows actually on screen, Right opens a parent and then steps into it, Left closes it or jumps out to the parent, Home/End hit the ends, Enter/Space select, and type-ahead jumps to the next row starting with what you typed (repeat a letter to cycle). Three details that are easy to get wrong are handled: closing a subtree that contains the focused row hands focus back to the row being closed instead of dropping it on <body>; the row is named by its own label via aria-labelledby, because a treeitem owns its child group and a name computed from contents would read the entire subtree as one row's name; and the disclosure arrow is a click target rather than a nested <button>, since a treeitem must not contain its own focusable elements. Renders folder/file icons by default (`showIcons={false}` for category or org trees), `indent` sets the per-level offset, and per-node `icon` overrides a single row. Styled with shadcn tokens (accent, muted-foreground, ring) so it follows light and dark themes; lucide-react is the only dependency, with no Radix and no state library. Distinct from command-palette, which is a flat searchable launcher: this is for structure you navigate rather than a name you already know. Distinct from json-viewer, which takes the parsed JSON value itself and renders its keys, types and entry counts: reach for that one to display a payload you did not author, and this one when you have your own hierarchy to express as `{ id, label, children }` nodes.
The confirmation step in front of an irreversible action: the user has to type the resource's own name ("acme-prod") before the destructive button turns on. Use it wherever a misclick would be unrecoverable — deleting a project, repository, workspace, organisation, cluster, database, or environment, removing a team member, revoking an API key, wiping data, cancelling a subscription, or any "danger zone" section of a settings page. Common asks it answers: "type to confirm", "type the project name to delete", "type DELETE to confirm", "confirm delete by typing name", "GitHub-style delete confirmation", "danger zone dialog", "destructive action modal", "disable the delete button until the name matches". shadcn/ui ships alert-dialog as an empty shell — the typed match, the disabled-until-it-matches wiring, and the announcement are left to you every time; this packages them into one drop-in that sits inside your existing dialog or card, so nothing here assumes which official components you have installed. Pass `phrase` (the name) and `onConfirm`; both sides are trimmed before comparing, so a pasted name that picked up a trailing space still matches, and an empty phrase never matches, which stops an untouched field from arming a delete. Set `caseSensitive={false}` to let "delete" pass for "DELETE", and mirror your mutation with `pending` to lock the field and swap the button label. The field opts out of autocomplete, autocorrect, autocapitalisation, and spellcheck — on a phone the first letter would otherwise be capitalised and an exact match made impossible to type. The button is genuinely disabled rather than aria-disabled, which screen readers skip: nothing is lost by that, because the label states what to type, the description explains that the button is waiting for it, and an always-mounted live region announces the moment it turns on (a live region inserted together with its text is not reliably announced, so one that appeared only on match would swallow that update). It renders as a real <form>, so Enter submits and, inside a dialog, focus lands on the field on open with no extra wiring. Styled with shadcn tokens (border-input, destructive, muted-foreground, ring) for automatic light/dark theming, with zero dependencies beyond your cn util. Distinct from confirm-button, which is a two-step click for cheap, reversible actions; this is the high-friction guard for the ones you cannot take back.
A leave guard for a form with unsaved changes: it holds the departure, asks whether the work should be discarded, and lets it through only when the answer is yes. Reach for it wherever a screen holds work that only exists in the browser: a long or multi-step form, a profile or settings page, a post, comment or article editor, an invoice, order or booking being filled in, a canvas, diagram or query builder, an upload that has been configured but not started, and any modal a user can wander out of. Common asks it answers: "unsaved changes warning react", "are you sure you want to leave this page", "beforeunload react hook", "useBeforeUnload", "prompt before leaving page react", "block navigation react router", "useBlocker alternative", "next.js unsaved changes warning", "next.js prevent navigation", "confirm before leaving form", "dirty form guard", "warn user before closing tab", "onbeforeunload not working", "react router prompt replacement", "shadcn unsaved changes dialog", "discard changes dialog", "route change confirmation react". The reason to install one rather than write four lines is that the four lines guard the wrong door. beforeunload fires when the browser unloads the document — closing the tab, reloading, typing a new URL, following a link off the site. Clicking a next/link or a React Router Link is none of those: the document stays exactly where it is, the router swaps what is rendered, and the half-filled form is gone without the browser ever being consulted. So the guard everyone ships protects the tab button and lets every single in-app route change walk straight past it, which is the way people actually lose the work. beforeunload is not straightforward on its own terms either. The spec settled on preventDefault() while older engines only look at returnValue having been set, so a handler that does one of the two is silently dead in some browsers. Custom wording is discarded by every current browser, so the message you actually control is the in-app one. And nothing fires at all on a page the user has never clicked or typed into, because browsers require sticky activation before they will interrupt a departure. This component does the browser half correctly and then covers the two departures it cannot see, each with the mechanism that is right for it. Link clicks are caught by a capture-phase listener on document, which runs before React's root listener and therefore before any router's handler; both preventDefault() and stopPropagation() are called, because routers differ on whether they check defaultPrevented and stopping the event short of the React root is true of all of them. Answering "discard" replays the original click on the same anchor, so the router handles it exactly as it would have — no full page reload, no reimplementation of routing. Back and forward are caught with the Navigation API's navigate event, the only thing in a browser that can actually refuse a traversal: popstate is announced after the history entry has already changed, and the usual workaround of pushing a sentinel entry to have something to pop leaves a duplicate entry and a back press that does nothing for the rest of the session. That trade is refused here rather than hidden, so the reach is stated plainly: where window.navigation is missing, back is not intercepted, and the way to cover it — along with navigation your own code starts — is the guard prop, which takes a blocker from your router (React Router's useBlocker drops straight in) and stands the built-in interception down while keeping the browser-level warning. Just as deliberate is what is never held: a router.push the app itself makes, because the redirect after a successful save is exactly that and blocking it traps someone on a form they have already submitted; cross-origin links, which are a real unload and which beforeunload already covers; hash links, downloads, target="_blank", and cmd, ctrl, shift or middle clicks, every one of which leaves this page where it is. Official shadcn/ui has nothing in this area — beforeunload, unsaved, dirty and blocker appear in none of its sixty-three components, and it ships no navigation blocking of any kind. Within pulld it is distinct from confirm-button, which guards a destructive action somebody deliberately clicked; this one interrupts a departure nobody thought of as destructive. It pairs with save-status and form-error-summary on the same screen. The dialog is an alertdialog because it interrupts rather than being asked for, and focus lands on "keep editing" rather than on discard, so an Enter press already on its way to the page cannot answer with the destructive choice; Escape does the same as keep editing, and there is no third way out that would leave the navigation in limbo. Once a departure has been answered the browser stops asking the same question, and starts again the moment the user touches the page still holding unsaved work. useUnsavedChanges() is exported for a dialog of your own, useBeforeUnload() for the browser half alone, every colour is a shadcn token so it follows light and dark, and the whole thing is one file with no dependency beyond the icon.
The list of files under a dropzone or file picker — one row each with the file name, its size, a progress bar while it uploads, an error with a retry button when it fails, and an X to drop it from the queue. Use it on any screen that accepts files: an attachment picker, an image or avatar upload, a CSV/spreadsheet import step, a document or PDF upload, a bulk media drop, or an import wizard. Common asks it answers: "file upload list", "upload queue", "show selected files with progress", "file list with remove button", "upload progress bar per file", "attachment list", "retry failed upload", "Dropbox/Gmail-style upload rows". shadcn/ui ships nothing that tracks an upload: its attachment component renders a file that is already attached — media, title, actions — with no status, no progress and no retry, and its progress primitive is a single bar with no notion of a file, so the row layout, the byte formatting, the per-file progress and the failure affordance are hand-rolled every time. It pairs with the file-dropzone component, which hands you a File[] and deliberately stops there: this is the half that shows what happened to those files. Pass an `items` array of `{ id, name, size?, status, progress?, error? }` where status is pending | uploading | done | error; omit `progress` and the bar goes indeterminate for uploads with no known length, and an empty array renders nothing so you can mount it unconditionally next to your queue state. It is presentational on purpose and never uploads anything — you keep the requests, the concurrency, the cancellation and the retry policy, and pass `onRemove`/`onRetry` to get the buttons. Accessibility is where a queue usually goes wrong and this one is built around it: progress sits in a role=progressbar, which is not a live region, so a file crawling from 1% to 100% does not narrate every tick; instead an always-mounted role=status region announces only the rows that just finished or just failed, batched into one message per change; the first render is treated as the starting state, so a list that mounts with finished rows stays silent; and every remove/retry button carries the file name in its accessible name, because a column of buttons all called "Remove" is unusable without sight of the row. Sizes are formatted to KB/MB/GB with tabular numerals, long names truncate with a title tooltip, and it is styled with shadcn tokens (muted-foreground, destructive, primary, accent, ring) so it follows light and dark themes; lucide-react is the only dependency. Distinct from save-status, which is a one-line indicator for a single background save, and from progress-ring, which is one circular meter: this is the multi-file queue.
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.
A toggle that stops the screen going dark while somebody is looking at it but not touching it, and — unlike every version written by hand — keeps on being true about whether the screen is actually being held awake. Reach for it wherever the page is being read rather than operated: a recipe followed with both hands busy, a barcode, QR code or boarding pass held up at a till or a gate, sheet music or guitar tabs on a stand, a presentation or kiosk view, a workout, cooking or interval timer counting down, an inspection or picking checklist walked through on a phone, turn-by-turn directions, a live scoreboard or auction, a long article or PDF being read, a dashboard left up on a wall display, a video call or a camera feed. Common asks it answers: "keep screen awake react", "prevent screen from sleeping web", "screen wake lock react", "wake lock api react", "navigator.wakeLock react", "useWakeLock hook", "react-screen-wake-lock alternative", "NoSleep.js alternative", "stop phone screen turning off website", "keep display on pwa", "wake lock toggle shadcn", "shadcn keep screen on", "wake lock released when tab hidden", "wake lock stops working after switching tabs", "wakeLock request NotAllowedError", "navigator.wakeLock is undefined", "wake lock not working on http", "screen-wake-lock permissions policy iframe", "release wake lock on unmount", "画面が消えないようにする react", "スリープ防止 ウェブ", "スクリーンスリープ 無効化 react", "タブを戻すと画面が消えてしまう". Official shadcn/ui has nothing of the kind, and the measurement is not close. Fetching all sixty-three registry entries today — sixty-two are fetchable, questionnaire alone 404s, and nine of the newer ones are only served on the new-york-v4 style track, so a probe of new-york and default silently misses them — and concatenating the component sources themselves rather than their JSON envelopes gives 223,287 bytes. In it, wakeLock, WakeLock, requestWakeLock, WakeLockSentinel, screen-wake-lock, keepAwake, NoSleep, visibilityState and visibilitychange are every one of them zero hits. Official has no component that reacts to the tab being hidden at all, so an agent asked for this writes it from scratch, and the version it writes has one specific bug in it. The bug is a single `const [isOn, setIsOn] = useState(false)` flipped inside the click handler, which makes the toggle a claim about the lock rather than a reading of it. It demos perfectly, because a demo never leaves the tab. Then the user glances at a message and comes back — and the browser releases a screen wake lock the moment the document stops being visible, silently, with no callback and nothing in the console. The toggle still reads "Screen stays on"; the phone in their hands starts dimming on schedule. Nothing in the UI ever admits it. That is the failure this component is built around: the lock has to be taken again on the way back, and the way back is `visibilitychange`. So the state here is two things, not one. `isEnabled` is what the user asked for and it survives the hidden stretches; `isActive` says whether a sentinel is being held right now, and it is written only by the sentinel’s own `release` event — the event the browser fires when the tab is hidden, the window is minimised, the battery gets low or the OS simply takes it back. On the element they appear as `aria-pressed` and `data-active`, so `aria-pressed="true"` with `data-active="false"` is a readable, honest state rather than a lie. Note what this does not need, because it is the exact opposite of the Fullscreen API and the difference is load-bearing: `wakeLock.request()` does not require a user gesture. It requires the document to be visible. That is what makes the re-acquire possible at all — there is no click to hang it on when somebody switches back to a tab — and it is also why the request fails in places a click would have got through. Asking while hidden is a guaranteed NotAllowedError, so it is not asked; and a NotAllowedError that arrives because the tab went away mid-request is a race, not a refusal, so it is not reported — a version that surfaces it puts an error in front of the user every time they switch tabs. A refusal that arrives while the page is visible is the real thing — an iframe that was not granted `allow="screen-wake-lock"`, or a browser that has decided no — and there the setting goes back off and `onWakeLockError` fires, rather than retrying on a timer that would spin forever. TypeScript will not help you here and will in fact mislead you. Since TS 5.x, lib.dom.d.ts declares `readonly wakeLock: WakeLock` on Navigator — not optional — so `navigator.wakeLock.request("screen")` type-checks cleanly and then throws `TypeError: Cannot read properties of undefined` at runtime wherever it is absent. This is a secure-context API, so that includes every page served over plain http: the staging box on an internal IP, the phone opening your dev server by LAN address. Detection is written by hand with an `in` check, it never reaches the render — so the server and the first client render agree and nothing hydrates twice — and where the API is missing the control stays in the tab order with `aria-disabled` and an accessible name that explains itself, rather than taking the `disabled` attribute that would remove it from the tab order so nobody ever hears why. The rest is the lifecycle nobody gets to on the first pass. Only one request is ever in flight, because a hidden/visible flap can fire two before the first settles and every sentinel but the last would be leaked, held with nothing left pointing at it. A sentinel that arrives after the user has switched the toggle off is released immediately instead of kept. Unmounting hands the lock back, because the sentinel is owned by the document rather than by the component: navigating from the recipe to the checkout inside a single-page app would otherwise leave the screen pinned awake with no control left to turn it off. The API: `WakeLockToggle` takes `defaultEnabled`, `onLabel`, `offLabel`, `unsupportedLabel`, `iconOnly`, `onEnabledChange` and `onWakeLockError` — named that way rather than `onChange` and `onError` because both of those are native DOM attributes React defines on every element, and a prop by either name collides the moment the options are spread onto the button. `useWakeLock()` returns `{ isEnabled, isActive, isSupported, error, enable, disable, toggle }` for a control you lay out yourself, and `isWakeLockSupported()` is there when you would rather hide your own. Within pulld it sits with the other components that watch what the browser is doing behind the app rather than what the user is doing in it: idle-timeout is the mirror image, ending a session when nobody is there, network-status reports a connection that changed without being asked, countdown and save-status are the things most often on screen while this is on, and fullscreen-button is the other half of a kiosk or presentation view. Distinct from a CSS or meta-tag trick: this is the browser’s own screen wake lock, and it lets the display dim on the browser’s terms the moment the setting is turned off. One file, one dependency (lucide-react for the two icons), every colour a shadcn token, so light and dark follow on their own.
A week of opening hours in one editor: seven rows, each a switch plus an opening and a closing time, handing back a plain object keyed by weekday. Reach for it wherever a form asks *when in the week* rather than when on the clock — store, shop and restaurant opening hours; a support desk’s staffed window; delivery, pickup and collection slots; a shift roster or rota template; a staff member’s bookable availability; clinic, gym, salon, library and office hours; per-day quiet hours or do-not-disturb; and the days and times a scheduled job, digest or backup is allowed to run. It settles the three rules that hand-rolled versions get wrong. **A closed day is `null`, never `00:00`–`00:00`** — mix those two and “closed on Sunday” becomes indistinguishable from “open around the clock on Sunday”, the one mistake in this domain that reaches customers. **A closing time earlier than the opening time is the night, not a typo** — 22:00–02:00 is the bar that shuts at two, measured across midnight as 4h instead of being flagged invalid. **Equal opening and closing times mean the whole day**, so “open 24 hours” stays expressible without inventing a third state. Each row says in words which of the three it read, as you type. The week is ordered by data, not by hand: Sunday first in en-US and ja-JP, Monday in de-DE and fr-FR, Saturday in ar-EG, taken from `Intl.Locale`’s week info, with the day names from `Intl.DateTimeFormat` — or pin it yourself with `weekStartsOn`. The fourteen time fields are this registry’s `time-input`, so each one follows the reader’s clock (12- or 24-hour, the AM/PM wording, the segment order) and is typed with the keyboard rather than picked from a dropdown. “Apply to all” copies one day across the week; a day switched off and back on returns the hours that were typed instead of a default; `incompleteDays(value)` lists the days that are open but only half filled in, which is what to check before saving. With `name` set, a hidden input carries the week as JSON, so `null` survives a native form post — which no flat field encoding manages. Common asks it answers: “opening hours input”, “business hours picker”, “store hours editor”, “hours of operation form”, “weekly schedule input”, “day of week time picker”, “operating hours component”, “working hours editor”, “availability editor”, “weekly availability picker”, “shift schedule input”, “rota editor”, “open closed per day”, “per-day time ranges”, “overnight hours input”, “quiet hours per day”, “office hours editor”, “restaurant hours input”, “shadcn opening hours”, “shadcn business hours”, “react opening hours picker”, “react business hours component”, “business hours without a library”. shadcn/ui has no surface for this: its `calendar` answers a date on a month grid, `item` and `field` are layout kits for assembling a row yourself, and fetching the source of all 63 items in its registry and grepping them for the clock — `type="time"`, `hourCycle`, `hour12`, `dayPeriod`, `toLocaleTimeString`, `hour`, `minute` — returns nothing at all. Distinct from this registry’s `time-input`, which is the single field this one places fourteen of, and from `cron-expression`, which reads a cron string and explains when it fires rather than letting a person edit a week by hand. One span per day: a day with a midday break is two spans, and that is deliberately out of scope.