Inputs

Segmented Radio

A horizontal segmented radio group: the pill under the selection does not repaint, it slides between buttons, and pure CSS computes the offset via :has().

  • radio
  • segmented
  • tabs
  • css-only

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/radio-005?lang=en

Report period
Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "radio-005" (Segmented Radio) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/radio-005.json

Registry item: https://vibeui.ru/r/radio-005.json
Installs to: components/vibeui/radio-005.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
A horizontal segmented radio group: the pill under the selection does not repaint, it slides between buttons, and pure CSS computes the offset via :has().

A segmented control with a sliding pill in pure CSS. One file, zero dependencies, a server component.

## 3. How to use it
import { Radio005 } from "@/components/vibeui/radio-005"

<Radio005
  legend="Report period"
  defaultValue="Month"
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-radio-005-* palette — do not swap it for your theme tokens
- the single shared pill instead of recolouring each button: only that produces movement between segments
- the :has(label:nth-of-type(n) input:checked) rules — they are the sliding mechanism, and the numbering caps at five segments
- the radio hidden by clip-path but not disabled: arrow keys must still switch the group
- the visible focus ring on the segment — the input is hidden and without this rule the keyboard path disappears
- the component's own light surface — without it the dark text disappears on a dark page

## 6. You may change
- the group heading through legend
- the segment captions through options
- the preselected segment through defaultValue
- the form field name through name

## 7. Rules
- No more than five segments: each index has its own offset rule in the CSS.
- Long captions break the rhythm — a segment is sized for a single word.
- The pill moves with transform, so the travel is switched off by prefers-reduced-motion and the choice stays instantly visible.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/radio-005.json
https://vibeui.ru/r/radio-005.json

Серверный компонент: состояния в JS нет. Группа — grid с равными колонками по числу вариантов, поверх лежит одна плашка-индикатор шириной в колонку. Сдвиг задан правилами :has(label:nth-of-type(n) input:checked) — под каждый номер сегмента своё правило, поэтому поддерживается до пяти вариантов. Радио спрятано clip-path'ом, но остаётся доступным с клавиатуры: стрелки по-прежнему переключают группу. Палитра в --vibeui-radio-005-*.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Radio005Props = Omit<
  ComponentPropsWithoutRef<"fieldset">,
  "children" | "defaultValue"
> & {
  legend?: string
  /** До пяти сегментов: под каждый номер в CSS заведено своё правило сдвига. */
  options?: string[]
  name?: string
  defaultValue?: string
  accent?: string
}

// Идея компонента: горизонтальный переключатель режимов — форма, в которой
// варианты сравниваются глазом за один взгляд. Подложка выбранного сегмента
// не перерисовывается, а переезжает: одна плашка под всеми кнопками,
// сдвиг считает CSS через :has(), поэтому анимация есть, а состояния в JS нет.
const STYLES = `
:where([data-vibeui-block="radio-005"]){
--vibeui-radio-005-bg:oklch(1 0 0);
--vibeui-radio-005-fg:oklch(0.22 0.014 265);
--vibeui-radio-005-muted:oklch(0.5 0.014 265);
--vibeui-radio-005-border:oklch(0.91 0.006 265);
--vibeui-radio-005-rail:oklch(0.96 0.003 265);
--vibeui-radio-005-accent:oklch(0.55 0.19 262);
--vibeui-radio-005-on-accent:oklch(1 0 0);
--vibeui-radio-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--vibeui-radio-005-count:3;
}
[data-vibeui-block="radio-005"]{
display:flex;flex-direction:column;
width:100%;max-width:22rem;box-sizing:border-box;
margin:0;padding:0.5rem 0.875rem 0.875rem;
background:var(--vibeui-radio-005-bg);
border:1px solid var(--vibeui-radio-005-border);border-radius:0.875rem;
font-family:var(--vibeui-radio-005-font);color:var(--vibeui-radio-005-fg);
}
[data-vibeui-block="radio-005"] legend{
float:left;width:100%;padding:0;margin:0.375rem 0 0.5rem;
font-size:0.6875rem;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
color:var(--vibeui-radio-005-muted);
}
[data-vibeui-block="radio-005"] [data-part="group"]{
clear:both;position:relative;display:grid;
grid-template-columns:repeat(var(--vibeui-radio-005-count),1fr);
padding:0.1875rem;border-radius:0.625rem;
background:var(--vibeui-radio-005-rail);
}
/* Плашка одна на всю группу и переезжает по сегментам: перекрашивание
   каждой кнопки по отдельности не даёт этого движения. */
[data-vibeui-block="radio-005"] [data-part="indicator"]{
position:absolute;left:0.1875rem;top:0.1875rem;bottom:0.1875rem;
width:calc((100% - 0.375rem) / var(--vibeui-radio-005-count));
border-radius:0.5rem;background:var(--vibeui-radio-005-accent);
box-shadow:0 1px 3px oklch(0.2 0.02 265 / 22%);
transition:transform .2s cubic-bezier(.32,.72,0,1);
}
[data-vibeui-block="radio-005"] [data-part="group"]:has(label:nth-of-type(1) input:checked) [data-part="indicator"]{transform:translateX(0)}
[data-vibeui-block="radio-005"] [data-part="group"]:has(label:nth-of-type(2) input:checked) [data-part="indicator"]{transform:translateX(100%)}
[data-vibeui-block="radio-005"] [data-part="group"]:has(label:nth-of-type(3) input:checked) [data-part="indicator"]{transform:translateX(200%)}
[data-vibeui-block="radio-005"] [data-part="group"]:has(label:nth-of-type(4) input:checked) [data-part="indicator"]{transform:translateX(300%)}
[data-vibeui-block="radio-005"] [data-part="group"]:has(label:nth-of-type(5) input:checked) [data-part="indicator"]{transform:translateX(400%)}
[data-vibeui-block="radio-005"] [data-part="seg"]{
position:relative;z-index:1;display:grid;place-items:center;
min-height:2rem;padding:0 0.5rem;border-radius:0.5rem;cursor:pointer;
font-size:0.8125rem;font-weight:600;color:var(--vibeui-radio-005-muted);
transition:color .2s ease;
}
[data-vibeui-block="radio-005"] [data-part="seg"]:has(input:checked){color:var(--vibeui-radio-005-on-accent)}
[data-vibeui-block="radio-005"] [data-part="seg"]:has(input:focus-visible){outline:2px solid var(--vibeui-radio-005-accent);outline-offset:2px}
/* Радио спрятано визуально, но не от клавиатуры: стрелки по-прежнему
   переключают группу, а Tab заводит в неё фокус. */
[data-vibeui-block="radio-005"] input{
position:absolute;width:1px;height:1px;margin:0;
clip-path:inset(50%);overflow:hidden;white-space:nowrap;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="radio-005"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_OPTIONS = ["День", "Неделя", "Месяц"]

/**
 * Сегментированная радиогруппа: подложка переезжает на CSS через :has().
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Radio005({
  legend = "Период отчёта",
  options = DEFAULT_OPTIONS,
  name = "vibeui-radio-005",
  defaultValue = "Неделя",
  accent,
  className,
  style,
  ...props
}: Radio005Props) {
  const palette = {
    "--vibeui-radio-005-count": String(options.length),
    ...(accent ? { "--vibeui-radio-005-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-radio-005" precedence="medium">
        {STYLES}
      </style>
      <fieldset
        {...props}
        data-vibeui-block="radio-005"
        className={className}
        style={palette}
      >
        <legend>{legend}</legend>
        <div data-part="group">
          <span data-part="indicator" aria-hidden="true" />
          {options.map((option) => (
            <label key={option} data-part="seg">
              <input
                type="radio"
                name={name}
                value={option}
                defaultChecked={option === defaultValue}
              />
              <span>{option}</span>
            </label>
          ))}
        </div>
      </fieldset>
    </>
  )
}