Registry Index

Search the catalog

Type to search items across every indexed registry, or pick a page.

ComponentReactSelf-contained

Slug Input

slug-input

A URL slug field that fills itself in from a title and then gets out of the way. Use it wherever a record needs a URL: the permalink or slug field in a blog post editor or CMS admin form, a page or docs route segment, a product handle, a workspace or team URL, a category or tag slug, a public profile handle. Type a title, watch the slug appear as kebab-case, and edit it whenever you want — this is the part hand-rolled fields get wrong. It keeps deriving only while the field still holds exactly what it generated, so editing the slug by hand, or loading an existing slug from your database, stops the derivation for good: renaming a published post cannot silently change its URL. Leave the field empty and blur, and it goes back to following the title. Every keystroke is sanitised in place — lowercased, spaces and punctuation collapsed to a single hyphen (or underscore), accents folded away — while the caret stays exactly where you were typing, which is what breaks when you naively assign a transformed value back to a controlled input. Unicode is handled rather than mangled: NFKD folding turns "Café au lait" into cafe-au-lait, "Łódź" into lodz, and the letters decomposition leaves whole are spelled out ("Straße" becomes strasse, not strae). Pass allowUnicode to keep the title's own script instead — without it a Japanese, Chinese, Korean, Greek, Cyrillic, Hebrew or Arabic title slugifies to an empty string, and with it combining marks stay attached to their letter, so がっこう does not quietly become かっこう. maxLength cuts a generated slug back to a whole word rather than mid-syllable, apostrophes disappear instead of splitting words ("don't panic" becomes dont-panic), and pasting a full URL takes just its last path segment. Zero dependencies, one import, shadcn tokens, optional prefix like example.com/blog/ wired to the input with aria-describedby. Official shadcn/ui has no slug or permalink field — its input is a bare element and input-group is an assembly kit with no logic in it — and a slugify npm package solves the string, not the field: the caret, the do-not-stomp rule and the typing-in-progress state are what this component is.

Live

Starting the live preview…

Runs in a sandboxed frame on a separate origin. Mounted with no props — some items need input to show anything.

@pulld

More from @pulld

A password strength meter for a sign-up, registration, change-password or reset-password form — the bar under the password field that says Weak or Strong, plus one line saying why. It scores how many guesses a password would survive rather than ticking off one uppercase, one number, one symbol: composition rules push people toward Password1!, which is guessed instantly, and reject correct horse battery staple, which is not. NIST SP 800-63B says the same — screen against known-bad passwords and let length do the work. It detects the things that make a password look random without being random: entries from a built-in list of the passwords that top every breach dump (folded through leet substitutions, so P@ssw0rd is found where password is, and unaffected by capitalisation), repeated characters, runs through the alphabet or the digits, runs along a keyboard row, and years and dates. The check hand-rolled meters always miss is userInputs: pass the email, username, display name or your product name and Acme2026! stops scoring as strong on acme.com — including the joined-up forms that separators hide, so Acme Co catches acmeco. Pass blocklist to add your own breach list on top; the built-in one is deliberately small, because a real one is megabytes and belongs behind an API. estimatePasswordStrength is exported on its own, pure and synchronous, so the same score that draws the meter can disable your submit button or drive a zod refine — no async, no 800 kB zxcvbn bundle, no dependencies at all. Accessibility is the other half: the bars are a role="meter" with aria-valuetext, and only the band name sits in the aria-live region, so a screen reader hears "Weak" once when the password crosses a band instead of being read to on every keystroke — which is what an aria-live wrapped around the whole widget does. The advice line is tied to the meter with aria-describedby instead — give the component an id and it derives one for the advice and wires it up; without an id the advice is still read, just in document order rather than together with the meter. Warnings and suggestions come back as stable codes with an overridable message table, so the meter translates. It uses no hooks, so it renders in a server component and needs no "use client" of its own. Official shadcn/ui has nothing for passwords — no meter, no blocklist, no scorer; its input is a bare element and field and input-group are assembly kits with no logic in them. It scores a password but does not collect one: the field it sits under is this registry’s `password-input`, which is the same bare element with the show/hide eye and its accessible naming already wired. Pass this component that field’s value and the two compose into the whole password row.

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.

A single pricing plan card — the tier box on a pricing page, a plans-and-billing settings screen, an upgrade or paywall modal, a compare-plans table, or the subscription row in an account page. It takes a plan name, a big price with its billing period ($29 /mo), an optional one-line pitch, a feature list and a call-to-action, and several of them sit side by side to make the whole pricing table. Features are the part that is usually got wrong. Pass a string for an included feature or an object to mark one excluded; an excluded row is muted and struck through, and it also carries a screen-reader-only "Not included:" prefix — because text-decoration: line-through is not announced, so a purely visual strikethrough tells a sighted reader the feature is missing and tells a screen-reader user it is included. The check and minus icons are aria-hidden, since the text already says which is which. Set featured to mark the recommended tier: it draws a primary ring and a "Most popular" badge, and badge takes any other label (Best value, Current plan). headingLevel picks h2, h3 or h4 so three cards dropped into a section do not break the page outline — a pricing table is the classic place where a hardcoded h3 lands under the wrong h2. Against official shadcn/ui there is no pricing card to compare with, only pieces to assemble: card is a 1,828-character generic container with no price, plan, tier or feature vocabulary in it at all, item is a ten-part compound kit (ItemGroup, Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader, ItemFooter, ItemSeparator) that additionally pulls in separator and is built for list rows, and badge and button each drag in @radix-ui/react-slot. None of the four contains a single pricing term. This is one component with lucide-react as its only dependency, theme-aware through shadcn tokens in light and dark.

Inline two-step confirmation on the button itself: the first click arms it — the label swaps to "Confirm?" and the button turns destructive — and only the second click calls onConfirm. It disarms itself after a timeout (3s by default) and on blur, so a stray double-click, a scroll away, or a Tab out can never fire the action. Reach for it wherever a modal would outweigh the action: a delete or remove button in a table row, a card, a list item or a toolbar; removing a member or collaborator; revoking an API key, token or session; disconnecting an integration; clearing a cache or a log; unsubscribing; resetting a filter or a setting; discarding a draft; leaving a channel. Common asks it answers: "confirm before delete without a dialog", "two-step delete button react", "click twice to confirm", "are you sure button shadcn", "inline confirmation button", "destructive button with confirmation", "delete button in a table row", "undo-less delete confirmation". Props: `onConfirm` (fired only on the confirming click), `confirmText` for the armed label, `timeout` in milliseconds, plus everything else a `<button>` takes. It is one real `<button>` element — Enter and Space confirm it, `disabled` is honoured, your `className` is merged through `cn`, and it defaults to `type="button"` so an armed click inside a form cannot submit it by accident. The armed state is exposed as `data-armed` for styling and announced through a polite live region, so the change is never carried by colour and label alone. Theme-aware through the destructive tokens, no dependencies, and `"use client"` because it holds a state and a timer. What official shadcn/ui offers instead is alert-dialog: a modal that pulls in @radix-ui/react-alert-dialog, renders through a portal, traps focus and needs open state wired up — right for a page-level, consequential confirmation, heavy for a row-level one. Official button has a destructive variant but no confirmation behaviour at all. For an action severe enough that a second click is not enough — deleting a production project or an account — use type-to-confirm, which makes the user type the name first.

A star rating that works both ways: as an input that collects a score, and read-only as a display of one. Reach for it wherever a number between 0 and 5 is really a row of stars: the "rate this" step after a purchase, delivery or booking; a product, app-store or seller review form; a CSAT or satisfaction question at the end of a support ticket or chat; a post-call or post-session feedback prompt; a difficulty, quality or priority score on an internal form; and, in read-only mode, the average beside a listing, a product card, a search result, a testimonial or a review summary (an average like 3.7 fills 70% of the fourth star, so the display is not rounded to a whole one). Common asks it answers: "star rating", "rating component", "rating input", "react star rating", "five star rating", "half star rating", "review stars", "star rating readonly", "average rating display", "feedback rating component", "shadcn rating", "shadcn star rating", "react-rating alternative", "rate this product component", "CSAT stars". Official shadcn/ui has no rating or star item of any kind — its slider is a range control with a thumb on a track, which is a different shape of answer — so the usual fallback is a row of buttons with no shared value semantics, and that is what makes it inaccessible. This one is a real slider: focusable with a focus-visible ring, arrow keys raise and lower the score in either axis, Home clears it to 0 and End maxes it out, and it exposes aria-valuemin/valuemax/valuenow plus a spoken aria-valuetext ("3.5 out of 5 stars") so a screen reader announces the score rather than counting buttons; read-only mode drops the slider role and renders as a labelled image instead, which is the correct semantic for a number you cannot change, and the individual stars stay aria-hidden either way because the value is announced once, not five times. Set allowHalf to take half stars — click the left half of a star, or step by 0.5 from the keyboard. Works controlled or uncontrolled through a plain number value with onValueChange (a number, not a string, so there is nothing to parse), forwards a ref, and posts through a hidden input in native forms via `name`. `max` changes the number of stars for a 3- or 10-point scale, `size` sets the pixel size, and `disabled` keeps the slider semantics while refusing input. Theme-aware via shadcn tokens — filled stars use the primary color, so it follows light and dark mode — with lucide-react as its only dependency, for the star icon.

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. 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.

Long text clamped to a few lines with a Show more / Show less toggle that appears only when the text is genuinely too long. Use it wherever text is usually short but occasionally is not: product and marketplace listing descriptions, comments, reviews and replies, user bios and profile blurbs, release notes and changelog entries, incident and error detail, log lines, AI answers and summaries, job posts, FAQ answers, and long cells in a card or table. Common asks it answers: "read more button", "show more / show less", "expandable text", "truncate text with a show more link", "line clamp with toggle", "collapsible paragraph", "see more link", "clamp description to 3 lines", "react-show-more-text alternative", "text truncation with expand". shadcn/ui ships nothing for this, and its collapsible is a different thing — a generic open/close container whose trigger is always there and which does no clamping — so the genuinely awkward part is left to you: deciding whether the toggle should exist at all. This measures the rendered text and renders the control only when the clamped box actually overflows, so a list of mostly-short entries does not sprout a pointless "Show more" under every one of them. It re-measures when the column resizes and the text rewraps, and again once web fonts have loaded, because a clamped box keeps its height while the line count underneath it changes; an element that is off screen in a closed tab or accordion measures zero, which it treats as "unknown" rather than "it fits", so the toggle is not dropped while the text is out of view. The clamp is applied as inline style rather than Tailwind's line-clamp-N utility, because `lines` is a runtime value and a dynamic `line-clamp-${n}` class is invisible to Tailwind's scanner — it would work in dev and silently vanish from the production build. Accessibility is where the hand-rolled version usually goes wrong: the full text always stays in the DOM and is only clipped visually, so screen readers read all of it and find-in-page still reaches it, instead of the usual text.slice(0, 200) that destroys the content for everybody; the control is a real button carrying aria-expanded and aria-controls pointing at the text. Clipped is not hidden, so a link inside the invisible part is still in the tab order — focus landing there expands the block rather than letting the browser scroll the clamped box and shear the text mid-line. Collapsing pulls the block back into view when it has already scrolled off the top, so the reader is not dumped further down the page. Uncontrolled by default; pass expanded and onExpandedChange to drive it from an "expand all" control. Styled with shadcn tokens (ring, muted-foreground) so it follows light and dark themes, and ships with no dependencies beyond your own cn util.

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.