Building a whole Tailwind app, not just this component?
You just browsed 25 Tailwind button groups. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
A primary action fused to a caret that opens a real popover menu, tethered with CSS Anchor Positioning written as arbitrary properties — [anchor-name:--tbg01-anc] on the caret, [position-anchor:--tbg01-anc] top-[anchor(bottom)] on the menu. The menu lives in the top layer, so no overflow-hidden parent can clip it, and v4's open: + starting: variants animate it in and out. Zero JavaScript.
A four-way mode switch whose thumb travels an arc instead of a straight line, trailed by a blurred comet tail that lags behind on a slower curve. The motion path is an arbitrary property — [offset-path:path('M43,28C123,6,203,6,283,28')] — and each radio moves it with group-has-[#tbg02-3:checked]/seg:[offset-distance:66.666%]. A live status line under the control reports the active mode, still with no JavaScript.
A density switcher whose highlight morphs from one button to the next with the View Transitions API — the browser snapshots the old and new highlight (both named [view-transition-name:tbg03-thumb]) and tweens between them, no position math. State is pure ARIA: aria-checked:text-white and group-aria-checked:opacity-100 read the attribute a 25-line controller sets, with full arrow-key support.
The SaaS pricing switch, solved in markup: two radios drive a sliding pill, the prices on every plan card and the savings line beneath them — all through group-has-[#tbg04-y:checked]/card: variants on the elements that change. The inactive price is invisible, not just transparent, so screen readers only ever announce the price you can see. No JavaScript.
The most-searched button-group pattern: an iOS-style segmented control whose thumb springs between options. Radios carry state and keyboard support; the thumb slides with group-has-[#tbg05-3:checked]/seg:translate-x-[200%] on a spring curve written with v4's ease-[linear(…)]. Each segment shows a sub-value, and the whole control is pure Tailwind.
The universal split button — primary Save plus a caret menu — built on <details>/<summary>, which works in every browser with zero script. group-open/more: rotates the chevron and plays the menu's entrance (animate-tbg06-in), the marker is removed with list-none [&::-webkit-details-marker]:hidden, and each item shows its keyboard shortcut.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
A rich-text formatting toolbar: icon buttons with CSS-only tooltips (after:content-[attr(data-tip)]) that also appear on keyboard focus, real toggle state via aria-pressed:, and a sample paragraph that previews bold, italic and underline as you press them. A short controller adds toolbar arrow-key navigation and the toggles themselves.
Category filter pills of different widths with a single highlight that glides to whichever is checked — and resizes to fit it. The CSS original hard-coded every pill's pixel offset; this port lets CSS Anchor Positioning measure instead: the checked pill gets has-checked:[anchor-name:--tbg08-on] and the indicator sizes itself with w-[anchor-size(width)]. Change a label, add a count, swap the font — it still lines up.
A single-select icon toggle group — left, centre, right, justify — on hidden radios, so the arrow keys, form submission and screen-reader semantics are all native. The pressed state uses an inset shadow plus a gold indicator bar (has-checked:after:…), and a sample paragraph re-aligns itself live through group-has-[#tbg09-2:checked]/card:text-center.
A dark, glassy wallet picker: a responsive grid-cols-[repeat(auto-fit,minmax(9.5rem,1fr))] of wallet tiles, the recommended one wrapped in a true gradient border ([background:…_padding-box,…_border-box]) with a floating badge, and a hover that lifts the tile and reveals a connect arrow. A close button and a reassuring footer make it a complete modal.
A model picker for AI products: radio cards with a provider badge, capability chips, context window and an animated check. Each row sets one inline variable, --brand, and v4's variable shorthand does the rest — bg-(--brand) on the badge, has-checked:border-(--brand) on the row, and a color-mix() tint behind the selection — so every provider is selected in its own colour.
A dark segmented control with a polished-chrome thumb: an @property-registered angle spins the metallic gradient, a glint sweeps across it every few seconds, and the thumb squashes and stretches like mercury every time it lands. The CSS original only wobbled on the first move — this port gives each position its own squash keyframe, so the stretch replays on every change.
Dashboard tabs with a glowing underline that always matches each tab's exact width — because the underline row shares the tabs' columns through grid-cols-subgrid. The CSS original moved one beam between columns, which can't animate; this port gives every column its own beam segment, so the old one shrinks out while the new one grows in — a smooth hand-off, still with zero JavaScript.
Toggleable tag chips on hidden checkboxes, lit up with an acid-green neon glow when selected. Two genuinely useful additions run on pure HTML and CSS: a live “3 selected” count drawn with a CSS counter (checked:[counter-increment:tbg14] → after:content-[counter(tbg14)]), and a Clear button that is simply type="reset" on the surrounding form.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
A developer-tool split button — Deploy plus environment menu — where the menu wipes open with an animated clip-path and each item phases in from a blur on a staggered delay. Items now show coloured environment dots and their target, and production is flagged as protected, so the menu helps people pick the right place to ship.
Four format tiles — PNG, JPG, WebP, AVIF — where the chosen one fills with its own colour in a clockwise radial sweep. An @property-registered angle drives conic-gradient(from -90deg, var(--c) var(--tbg16-a), transparent 0) on a before: layer, and each tile's colour is one inline variable read by has-checked:border-(--c). Every tile now explains what the format is for.
A hardware-style transport panel: three keys drawn as dot-matrix LED displays that light up green, amber or red when selected. The dot grid is a tiled radial-gradient on before: (before:bg-size-[7px_7px]), scanlines sit on after:, and each key swaps one variable — has-checked:[--led:#3dff6e] — to re-colour dots, text and glow at once. Labels are set in Doto, a true dot-matrix face, and a status line reports the current mode.
A publish bar where the primary action is press-and-hold: holding fills the button left-to-right (an @property width animated on active:after:), letting go early cancels, and completing it flips the button to a green Published state via data-done:. The CSS original drew the fill but nothing ever happened; a 20-line listener on animationend now makes it a real confirmation pattern.
An icon-only view switcher (Grid, List, Gallery, Map) with an iridescent foil thumb: four hue bands driven by an @property hue drift, a mix-blend-color-dodge diffraction layer and a periodic white glare. A caption under the control spells out the current view, so the icon-only design no longer leaves anyone guessing.
A vertical navigation stack tilted into 3D, where the hovered (or keyboard-focused) card lifts toward you while its siblings sink back and dim. It's a showcase for v4's native 3D utilities — perspective-[760px], transform-3d, rotate-x-34, translate-z-[70px] — combined with group-has-[button:hover]/stack:not-hover:, which targets “every card except the one under the pointer” in one variant.
A floating action button that fans four actions out along an arc. A hidden checkbox holds open state; each satellite reads its own offset from inline --x/--y through v4's translate-x-(--x) shorthand, with staggered delay-* for a springy cascade. Closed satellites are now invisible, so they leave the tab order — the CSS original let keyboard users tab into four hidden buttons.
A cyberpunk audio-filter selector: picking a mode fires a 0.45s RGB-split glitch — cyan and magenta copies of the label (before:content-[attr(data-txt)], after:…) jitter apart while the button slices with stepped clip-path keyframes — then settles into a steady neon state. A live equaliser above the buttons reshapes itself for each filter, so the choice has a visible meaning.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Plan-selector pills ringed by a rotating conic-gradient border — slow violet for idle pills, a fast solar ring plus blurred halo for the chosen one. The border is the padding-box/border-box trick in a single arbitrary [background:…], spun by an @property angle. The CSS original's halo used z-index:-1 and vanished behind the page background; an isolate wrapper fixes it.
A reactor-control action row with satisfying click feedback: a radar shockwave expands from exactly where you clicked, the primary action breathes with an @property-animated glow, and a console line logs each command. The CSS original only rippled from the centre while the mouse was held (quick clicks barely showed); a tiny script now sets --x/--y and re-arms data-fire:after:animate-tbg24-wave on every press.
Date-range preset buttons lit by a warm spotlight that follows the cursor across the whole group — one glow spanning every button, because each reads the same pointer coordinates from --mx/--my. Selection is pure radios; the script only writes two variables. Below the presets, the resolved range (Oct 2 – Oct 8, 2026) updates as you choose, so users see exactly which dates they're getting.
FAQ
Frequently asked questions
How does the iOS-style segmented control slide its thumb between options without any JavaScript?
The thumb is one absolutely-positioned element sitting behind a row of visually-hidden radio inputs. Each visible segment is a <label> tied to a radio with for, so clicking a label checks its radio. A :has() rule on the container reads which radio is checked and sets the thumb's translate to that segment's offset — .group:has(input:nth-of-type(2):checked) .thumb{translate:100% 0}. Because the segments are equal width, each step is a clean percentage, and the spring comes from a transition with a slight-overshoot easing. The radios stay in the DOM (clipped with sr-only, never display:none) so they keep focus and arrow-key behaviour. The catch: if your segments are not equal width, a fixed percentage translate lands off-target — switch to the Anchor Positioning approach the pill-filter demo uses.
Why does my sliding highlight land in the wrong place when the buttons have different widths?
Equal-width segments let you move the indicator by a fixed percentage, but filter pills sized to their text — All, Electronics, Home & Garden — break that math, because a 25% step assumes four identical columns you no longer have. The pill-filter demo solves it with CSS Anchor Positioning: give the checked label an anchor-name via :has(), then position the single highlight against it with left:anchor(left) and width:anchor(size width). The indicator now measures the real checked button at whatever width it is, and the browser reflows it on resize for free. Without anchor support you would otherwise read offsetLeft and offsetWidth in JavaScript and write them to custom properties — so treat the anchor version as the enhancement and a measured-JS path as the fallback for older engines.
How do I keep an icon-only button group accessible when there's no visible text label?
Two rules the icon toolbar and alignment-toggle demos both follow. First, every icon button carries an aria-label ("Bold", "Align left"), because the glyph alone means nothing to a screen reader, and the decorative <svg> inside gets aria-hidden. Second, pick the control that matches the behaviour: a single-select group like text alignment or the view switcher is built on radio inputs, so the arrow keys move between options and exactly one stays checked — native radio semantics, no custom key handling. A multi-toggle such as the formatting toolbar uses real <button> elements with aria-pressed flipped per toggle. The CSS-only tooltips use after:content-[attr(data-tip)], but a tooltip is not a label substitute — a tooltip only appears on hover, so keep the aria-label so the control stays named for keyboard and screen-reader users who never hover.
How does the pricing toggle switch every plan's price between monthly and yearly with no script?
The monthly/yearly switch is two radio inputs above the pricing section. Each plan card renders both prices stacked, and a container-level :has() rule decides which shows: .pricing:has(#yearly:checked) .price-monthly{display:none}, with the inverse for the yearly figure. The sliding pill on the switch itself is the same translate-on-checked trick as the segmented control. Because state lives in a checked radio, it survives a form submit and needs no hydration. The common mistake is hiding the inputs with display:none — that removes them from the tab order and breaks keyboard use; clip them with sr-only instead so they stay operable. Also reserve equal height for both price nodes, or the card jumps when you switch — the two figures rarely render at the same height on their own, and an unreserved swap causes a visible layout shift.
How do the split-button dropdowns open their menu, and why won't mine close when I click away?
There are two no-JS bases here. The save-options demo wraps the caret menu in <details>/<summary>: open state is the element's own open attribute and the menu is styled off details[open]. The anchor-positioned demo uses the native popover attribute with a popovertarget trigger, tethered to the button with anchor-name. Your "won't close on outside click" bug is specific to <details> — it has no light-dismiss, so clicking elsewhere leaves it open. The popover API gives you light-dismiss and Escape-to-close for free, which is exactly why it is the better base for a real menu. If you must stay on <details>, you add a small outside-click handler — but that leaves the no-JS path, so prefer the popover version wherever its browser support reaches your users.
When should I use a segmented control versus a dropdown or a radio-card grid?
A segmented control — the iOS switch, alignment toggle, view switcher — earns its horizontal space only for two to five mutually-exclusive options you want visible at once, like billing period or grid-vs-list. Past five it overflows on narrow screens; beyond that, use a <select> or the popover dropdown, which collapse to one line. The AI-model picker and wallet grid are deliberately not segmented: each option carries a badge, a context-window figure or a glassy card, so they use a radio-card grid that wraps with grid-cols-[repeat(auto-fit,minmax(9.5rem,1fr))]. Rule of thumb: if an option needs more than a short label, it is a radio-card grid, not a segmented control. And if the thing is a command rather than a persistent choice — Save, Deploy — reach for a split button; toggle groups represent state, buttons represent actions, and mixing the two confuses users.
How does the View Transitions morphing highlight work, and what happens where it isn't supported?
The density switcher gives its moving highlight a view-transition-name. When the checked option changes, running that state change inside a view transition makes the browser snapshot the highlight's old and new positions and tween between them, so the pill appears to morph from one button to the next rather than teleport. Since state is driven by radios, the :has() rule that repositions the highlight is the real DOM change the transition captures. The design rule is progressive enhancement: in an engine without the View Transitions API the highlight still moves — it just uses the plain CSS transition instead of the cross-element morph, so nothing is gated behind the API and nothing breaks. Honour prefers-reduced-motion:reduce as well; the comet, liquid-metal and glitch demos cut their motion under that query, and the morph should drop to an instant swap too.
The glow, ripple and spotlight demos react to the cursor — how is that built, and does it need JavaScript?
They split by what they track. The cursor-following spotlight updates two custom properties — --x and --y — from a pointermove listener on the group, and a radial-gradient centred on those variables paints the glow; that needs a few lines of JS because CSS cannot read the pointer position. The ripple shockwave reads the exact click coordinates the same way, then plays a scale-and-fade keyframe from that point. By contrast the conic-border pills, the glitch RGB-split and the liquid-metal thumb are pure CSS — they animate on :hover, :checked and an @property-registered angle, with no script at all. All of them must respect prefers-reduced-motion:reduce, and the pointer-driven ones should no-op on touch, where there is no hover — fall back to the static gradient so a tap still reads as a normal, legible button.