Ratio Bar
ratio-bar
One horizontal bar that shows how a whole is divided up, with a legend that names every part — the GitHub-style language / storage bar. Reach for it whenever the question is "what is this made of?" rather than "how far along is it?": disk or storage usage broken down by file type, a plan or quota bar (seats used, API calls, build minutes, bandwidth), a budget or spend breakdown by category, traffic by source or device, test results split into passed / failed / skipped, a portfolio or vote split, tickets by status, or a repository language bar. Common asks it answers: "stacked bar component react", "percentage breakdown bar shadcn", "storage usage bar", "disk usage breakdown", "quota / capacity bar", "segmented progress bar", "share of total bar", "distribution bar", "usage meter with legend", "percentages that add up to 100". Pass `parts` as `{ label, value }` objects in any unit you like — bytes, requests, dollars — and only the ratios are used. Add `total` to switch from "parts of a whole" to "used out of a capacity": the gap is drawn as empty track and listed as its own row (rename it with `remainderLabel`, or pass `null` to draw it without listing it). `precision` adds decimals, `formatValue` puts the raw figure next to each share, `showLegend={false}` keeps the legend for screen readers only, and each part takes a `className` for its colour (the default is a ramp of your primary colour, which is theme-aware in any shadcn project; pass `bg-chart-1`…`bg-chart-5` or your own classes for distinct hues). It handles the parts a hand-rolled version gets wrong. The percentages are apportioned by largest remainder rather than rounded one at a time, so three equal parts read 34 / 33 / 33 instead of 33 / 33 / 33 and the column always totals exactly 100. A part too small to round to a whole percent reads "<1%" rather than the lie "0%", and a part that is nearly but not quite everything reads ">99%" rather than "100%". Tiny slices keep a two-pixel minimum so they stay visible without stealing width from the rest, while a part worth exactly zero draws nothing at all and is still listed. Negative, NaN and Infinity values count as zero instead of collapsing the layout. The legend names and quantifies every part, so nothing is carried by colour alone (WCAG 1.4.1) and the bar itself is aria-hidden. No hooks and no clock: it renders inside a React server component with no "use client" of its own, ships no client JavaScript, and produces identical markup on the server and in the browser. `ratioPercents` is exported for the same figures in a table or tooltip. Official shadcn/ui has nothing for this: progress is a single value with no parts, and chart is a Recharts wrapper for plotted series rather than one inline bar with no dependencies.
More from @pulld
Submit button that shows a spinner, announces itself and refuses to fire twice while an async action is in flight. Set loading={true} for the duration of the request — form submit, save, sign-in, checkout, delete confirmation, "generate" in an AI app, or any handler that awaits fetch — and the button disables itself so a second click cannot send the request again; loadingText swaps the label for "Saving…" or "Charging card…" while it waits. Common asks it answers: "react button with loading spinner", "disable button while submitting", "prevent double submit", "async submit button", "pending state button shadcn", "button spinner while awaiting fetch", "form submit loading state". Official shadcn/ui has no loading state anywhere in this path: its button ships no loading, pending or busy prop, and its spinner is a bare spinning icon with an aria-label — pairing them, disabling the button, and keeping the two in step is left to you, and doing it by hand is where the double-submit bug comes from. This one carries aria-busy while pending, and the spinner it composes is pulld's, which puts the label in a polite live region rather than only on the icon, so the wait is announced instead of being a silent frozen button. Two things worth knowing before you drop it in a form: it defaults to type="button", so pass type="submit" explicitly when it submits a form, and disabling a focused button takes it out of the tab order — the live region is what carries the state to a screen reader once focus has moved. The spinner atom installs with it through registryDependencies; there is nothing else to add.
The list of files under a dropzone or file picker — one row each with the file name, its size, a progress bar while it uploads, an error with a retry button when it fails, and an X to drop it from the queue. Use it on any screen that accepts files: an attachment picker, an image or avatar upload, a CSV/spreadsheet import step, a document or PDF upload, a bulk media drop, or an import wizard. Common asks it answers: "file upload list", "upload queue", "show selected files with progress", "file list with remove button", "upload progress bar per file", "attachment list", "retry failed upload", "Dropbox/Gmail-style upload rows". shadcn/ui ships nothing that tracks an upload: its attachment component renders a file that is already attached — media, title, actions — with no status, no progress and no retry, and its progress primitive is a single bar with no notion of a file, so the row layout, the byte formatting, the per-file progress and the failure affordance are hand-rolled every time. It pairs with the file-dropzone component, which hands you a File[] and deliberately stops there: this is the half that shows what happened to those files. Pass an `items` array of `{ id, name, size?, status, progress?, error? }` where status is pending | uploading | done | error; omit `progress` and the bar goes indeterminate for uploads with no known length, and an empty array renders nothing so you can mount it unconditionally next to your queue state. It is presentational on purpose and never uploads anything — you keep the requests, the concurrency, the cancellation and the retry policy, and pass `onRemove`/`onRetry` to get the buttons. Accessibility is where a queue usually goes wrong and this one is built around it: progress sits in a role=progressbar, which is not a live region, so a file crawling from 1% to 100% does not narrate every tick; instead an always-mounted role=status region announces only the rows that just finished or just failed, batched into one message per change; the first render is treated as the starting state, so a list that mounts with finished rows stays silent; and every remove/retry button carries the file name in its accessible name, because a column of buttons all called "Remove" is unusable without sight of the row. Sizes are formatted to KB/MB/GB with tabular numerals, long names truncate with a title tooltip, and it is styled with shadcn tokens (muted-foreground, destructive, primary, accent, ring) so it follows light and dark themes; lucide-react is the only dependency. Distinct from save-status, which is a one-line indicator for a single background save, and from progress-ring, which is one circular meter: this is the multi-file queue.
A month picker: a year of twelve months as a grid, with arrows on the year, that hands back a plain "YYYY-MM" string — "2026-08" — and never a day or a time zone. Reach for it wherever the thing being chosen is the month itself: a billing or subscription cycle, the period on an invoice or a statement, a monthly report or export, the target month on an expense claim or a timesheet, payroll and accounting periods, budget and forecast months, a cohort in a retention table, the month a goal or OKR is scored in, "as of" month on a snapshot, the archive month on a blog or changelog, card expiry, and the period selector above an analytics dashboard, chart or ledger — usually paired as two of them for a from/to range. Common asks it answers: "month picker", "month year picker", "monthpicker react", "select a month component", "month and year select", "billing period picker", "monthly report period selector", "choose month for dashboard", "shadcn month picker", "shadcn calendar month only", "MUI DatePicker views month equivalent", "antd DatePicker picker=month equivalent", "react-datepicker showMonthYearPicker alternative", "YYYY-MM input". shadcn/ui has no month selection anywhere: its calendar is a react-day-picker wrapper that pulls in react-day-picker and date-fns and returns a Date for a day, and captionLayout="dropdown" adds month and year dropdowns for *moving* through that grid rather than for answering with a month; the Date Picker page is that same calendar inside a popover; select, native-select and combobox are empty controls that know nothing about months. Distinct from pulld date-input, which types a full date down to the day, and from pulld calendar-heatmap, which draws a year of days rather than choosing one of its months. The value is a calendar month rather than an instant, and the component holds that line: there is deliberately no Date accessor, because handing one back means having silently picked a day and a zone — the bug that starts a billing period on the last day of the previous month for everyone west of UTC. `toMonthValue(date)` reads local fields going in (the "toISOString().slice(0, 7)" one-liner is a month early for half the planet after 22:00), `parseMonthValue` gives back { year, month } and rejects anything that is not a bare month, and the strings sort and compare as they read. Month names come from `Intl.DateTimeFormat`, so the grid is already in the reader's language with zero dependencies — no date library, no icon package, one file. Both the locale and the "which month is now" marker are resolved after mount, so a server render and the browser's first paint agree instead of tripping a hydration mismatch, and passing `locale` skips the swap entirely. It is a real `role="grid"` with a roving tabindex — one tab stop for the whole year, then arrow keys inside it. Left and right step a month and cross into the neighbouring year at the edges rather than dead-ending in December, up and down move a row and follow the `columns` prop, Home and End go to January and December (rows here are a layout choice, not a calendar week), and PageUp/PageDown hold the month and walk the years. On an RTL page left and right follow the writing direction instead of running backwards. Every cell is named with the month spelled out and its year — "August 2026", localised — because "Aug" alone stops meaning anything once the arrows have moved, and the current month carries aria-current="date". `min` and `max` take the same "YYYY-MM" strings and stop the year arrows as well as the cells, `isMonthDisabled` handles scattered holes like closed accounting periods without locking the arrows, and unavailable months are marked with aria-disabled rather than disabled so they can still be reached and read instead of being invisibly skipped. Give it a `name` and it posts with a plain form or a server action through a hidden input. Uncontrolled, controlled, or controlled on the year alone; a value set from outside pulls the grid to that year so the selection is never off screen. Styled entirely with shadcn tokens (primary, accent, input, ring, muted-foreground), so it follows light and dark mode.
A week of opening hours in one editor: seven rows, each a switch plus an opening and a closing time, handing back a plain object keyed by weekday. Reach for it wherever a form asks *when in the week* rather than when on the clock — store, shop and restaurant opening hours; a support desk’s staffed window; delivery, pickup and collection slots; a shift roster or rota template; a staff member’s bookable availability; clinic, gym, salon, library and office hours; per-day quiet hours or do-not-disturb; and the days and times a scheduled job, digest or backup is allowed to run. It settles the three rules that hand-rolled versions get wrong. **A closed day is `null`, never `00:00`–`00:00`** — mix those two and “closed on Sunday” becomes indistinguishable from “open around the clock on Sunday”, the one mistake in this domain that reaches customers. **A closing time earlier than the opening time is the night, not a typo** — 22:00–02:00 is the bar that shuts at two, measured across midnight as 4h instead of being flagged invalid. **Equal opening and closing times mean the whole day**, so “open 24 hours” stays expressible without inventing a third state. Each row says in words which of the three it read, as you type. The week is ordered by data, not by hand: Sunday first in en-US and ja-JP, Monday in de-DE and fr-FR, Saturday in ar-EG, taken from `Intl.Locale`’s week info, with the day names from `Intl.DateTimeFormat` — or pin it yourself with `weekStartsOn`. The fourteen time fields are this registry’s `time-input`, so each one follows the reader’s clock (12- or 24-hour, the AM/PM wording, the segment order) and is typed with the keyboard rather than picked from a dropdown. “Apply to all” copies one day across the week; a day switched off and back on returns the hours that were typed instead of a default; `incompleteDays(value)` lists the days that are open but only half filled in, which is what to check before saving. With `name` set, a hidden input carries the week as JSON, so `null` survives a native form post — which no flat field encoding manages. Common asks it answers: “opening hours input”, “business hours picker”, “store hours editor”, “hours of operation form”, “weekly schedule input”, “day of week time picker”, “operating hours component”, “working hours editor”, “availability editor”, “weekly availability picker”, “shift schedule input”, “rota editor”, “open closed per day”, “per-day time ranges”, “overnight hours input”, “quiet hours per day”, “office hours editor”, “restaurant hours input”, “shadcn opening hours”, “shadcn business hours”, “react opening hours picker”, “react business hours component”, “business hours without a library”. shadcn/ui has no surface for this: its `calendar` answers a date on a month grid, `item` and `field` are layout kits for assembling a row yourself, and fetching the source of all 63 items in its registry and grepping them for the clock — `type="time"`, `hourCycle`, `hour12`, `dayPeriod`, `toLocaleTimeString`, `hour`, `minute` — returns nothing at all. Distinct from this registry’s `time-input`, which is the single field this one places fourteen of, and from `cron-expression`, which reads a cron string and explains when it fires rather than letting a person edit a week by hand. One span per day: a day with a midday break is two spans, and that is deliberately out of scope.
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.
An offline banner that is right about being offline: it tells someone the page has lost the network, and — the harder half — only tells them it is back once that has actually been verified. Reach for it wherever losing the connection loses work or misleads the reader: a long form, an editor or a checkout someone is mid-way through; a dashboard, wallboard or monitoring view whose numbers stop being true the moment the feed dies; a chat, inbox or collaborative document where silence reads as "nobody is talking"; a PWA, field app or point-of-sale used on a phone that drifts in and out of coverage; and any app that queues writes to flush on reconnect. Common asks it answers: "offline banner", "offline detection react", "network status component", "useOnline hook", "useNetworkStatus", "detect offline react", "navigator.onLine react", "connection lost banner", "reconnecting indicator", "internet connection detector", "react-detect-offline alternative", "shadcn offline banner", "no internet message component", "online offline event react", "captive portal detection", "heartbeat ping component". The reason to install one rather than write it is that the three-line version everyone writes is wrong, and wrong in the direction that matters. navigator.onLine does not report whether the internet works; it reports whether the machine has a network interface that is up. A laptop joined to a café or hotel access point whose portal has not been logged into reads online. So does one on Wi-Fi whose upstream has died, one behind a captive portal that answers for every server with its own login page, and one where DNS alone has stopped resolving. Every one of those is true while nothing whatsoever loads — so a banner built on that flag stays hidden through precisely the outages people complain about, and the window.addEventListener("online") that clears it fires when an interface came up, not when anything can be reached. The false direction is the trustworthy one: the browser is not wrong about having no interface at all. So this component believes false immediately and treats true as a claim to be checked, by actually asking the network for something. What that costs is kept honest, because a component that invents a request every few seconds forever is one people rip out. Mount does not probe — the page in front of the user arrived over the very network in question, and its own load is the freshest evidence there is. The online event starts a probe instead of being believed, and only the probe's answer clears the banner. The offline event lands immediately, with nothing to wait for. While unreachable, probes back off exponentially with jitter, because everyone whose access point rebooted starts their backoff on the same tick and would otherwise arrive back together at the worst possible moment — and they stop entirely when the interface itself is down, since there is nothing to ask and the event will say when there is. A hidden tab probes nothing at all and restarts on visibilitychange, which is also what catches the laptop that slept and woke up on a different network. Steady polling while everything is fine is off by default and there when a wallboard needs it. The probe itself is two details a hand-rolled fetch misses. It refuses to follow redirects, which is what turns a captive portal's 302-to-its-own-login-page back into the failure it is rather than a perfectly good 200. And it counts any HTTP response as reachable, a 404 or a 502 included: the question is whether packets get to a server and back, and a 404 answers it as well as a 200 does — which is why the default /favicon.ico is safe on a site that does not have one, and why a version checking res.ok reports such a site as permanently offline. A deadline is enforced too, because a black-holed connection does not fail, it hangs. Official shadcn/ui has nothing here: no offline, online or network item, and navigator.onLine, the online/offline events and any form of reachability check appear nowhere in its sixty-three components. Within pulld it is the detector, not another notifier — toast is the right home for "it worked / it failed" messages your code decides to send, while this one works out, on its own, whether the network is actually there. useNetworkStatus() is exported for a bar of your own design, or for pausing polling, disabling a submit button and flushing a queue on reconnect, and it hands back a check() to call the moment one of your own requests fails — a far better signal than any poll. checkReachable() and nextProbeDelay() are exported too. The wording sits in an always-mounted polite live region, because a live region inserted together with its text is not reliably announced and the banner would be silent for exactly the people who cannot see it. Every colour is a shadcn token, so it follows light and dark, and the whole thing is one file.
A 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 one-time passcode field split into separate digit boxes — the "enter the 6-digit code we sent you" screen. Reach for it on any challenge step where a short numeric code is typed or pasted: two-factor and multi-factor sign-in (2FA/MFA), an authenticator app's TOTP code, an SMS or phone verification code, an email confirmation code, a magic-link fallback code, account recovery, a step-up check before a payment or a destructive settings change, device or TV pairing, and PIN entry. Common asks it answers: "otp input", "one time password input", "verification code input", "6 digit code input", "enter code boxes", "2fa code field", "sms code input", "confirmation code input", "pin input react", "segmented code input", "react-otp-input alternative", "input-otp alternative", "otp input without dependencies", "shadcn otp input". Official shadcn/ui covers the same screen with input-otp, and the difference is the dependency: that one is a wrapper around the third-party input-otp package (plus lucide-react for its separator), so installing it adds a runtime dependency and hands the caret and paste behaviour to a library. This is the same boxed UI written out in a single file with no dependencies at all — reach for it when the project is keeping its dependency list short, when a package has to be vendored or audited before it can be added, or when you want the behaviour in code you can read and change rather than configure. Digits only, by design: the value is stripped to digits and truncated to `length` (default 6) on every path in, so a controlled parent, a paste and a keystroke cannot disagree about what is in the field. Pasting a full code into any box distributes it across the rest and lands the caret on the last filled one, typing auto-advances, Backspace clears and steps back, and the arrow keys, Home and End move between boxes. The first box carries autocomplete="one-time-code", so iOS and Android offer the code from the incoming SMS, and every box is inputMode="numeric" with pattern="[0-9]*" for a numeric keypad on mobile. Every box is labelled "Digit N of 6" and the group carries a name of its own, so a screen reader user is told where they are instead of hearing six unlabelled text fields. Works controlled (`value` + `onChange`) or uncontrolled (`defaultValue`), fires `onComplete` once when the last box fills — on the fill only, not on every later edit — and forwards a ref to the first box so a page can focus it on mount or from a shortcut. Passing `name` mirrors the joined value into a hidden input, so it posts with a plain HTML form, a Next.js server action, or React Hook Form without a controller. Themed with shadcn tokens, so it follows dark mode.