Button Group

Currency Picker

A currency picker whose dividers are drawn as grid gaps: gap:1px over a border-coloured fill instead of negative margins.

  • buttongroup
  • currency
  • grid
  • dividers

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/buttongroup-020?lang=en

Report currency
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 "buttongroup-020" (Currency Picker) 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/buttongroup-020.json

Registry item: https://vibeui.ru/r/buttongroup-020.json
Installs to: components/vibeui/buttongroup-020.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 currency picker whose dividers are drawn as grid gaps: gap:1px over a border-coloured fill instead of negative margins.

A currency picker on a grid where the dividers are the gaps between cells. Zero dependencies, one file, a server component.

## 3. How to use it
import { Buttongroup020 } from "@/components/vibeui/buttongroup-020"

<Buttongroup020
  currencies={[{ code: "USD", symbol: "$", name: "US dollar" }]}
  defaultValue="USD"
  label="Report currency"
  name="currency"
/>

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-buttongroup-020-* palette — do not swap it for your theme tokens
- the dividers made of gap:1px over the track fill: the trick survives wrapping, which margin:-1px cannot
- overflow:hidden on the track — without it the cell corners stick out of the rounded border
- the inset box-shadow instead of a border on the selected cell: a border inside a grid shifts the content by a pixel
- the currency name as text under clip-path: a three-letter code means nothing read aloud
- the aria-hidden on the symbol so "₽" is not announced twice alongside the code

## 6. You may change
- the currencies, their symbols and names through currencies
- the initial currency through defaultValue
- the accessible group name through label
- the form field name through name and the accent through accent

## 7. Rules
- The 6rem column minimum is hard-coded in auto-fit: retune it for longer codes.
- The component converts nothing: your code changes the amounts after the form is submitted.
- Currency symbols differ in width: tabular figures do not align "₽" with "£".
- 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/buttongroup-020.json
https://vibeui.ru/r/buttongroup-020.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в переменных --vibeui-buttongroup-020-*. Серверный: выбор держат radio внутри fieldset. Приём — трек с background цвета границы и gap:1px: сквозь зазоры видно подложку, и линия между сегментами получается ровно одна, без margin:-1px и без вычитания радиусов. Приём переживает перенос: repeat(auto-fit,minmax(6rem,1fr)) раскладывает валюты в несколько рядов, и сетка сама рисует крест. overflow:hidden на треке обрезает углы сегментов по внешнему радиусу. Выбранный сегмент помечен inset box-shadow, а не border: рамка внутри сетки сместила бы содержимое на пиксель. Название валюты лежит текстом под clip-path — код «RUB» вслух звучит как три буквы.

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 Buttongroup020Currency = {
  code: string
  symbol: string
  name: string
}

export type Buttongroup020Props = Omit<
  ComponentPropsWithoutRef<"fieldset">,
  "children"
> & {
  currencies?: Buttongroup020Currency[]
  defaultValue?: string
  label?: string
  name?: string
  accent?: string
}

// Идея компонента: разделители нарисованы не рамками, а просветами. Трек —
// grid с gap:1px и заливкой цвета границы: сквозь зазоры видно подложку, и
// линия между сегментами получается ровно одна, без отрицательных отступов
// и без вычитания радиусов. Приём переживает перенос строки: auto-fit
// раскладывает валюты в несколько рядов, и сетка сама рисует крест.
// overflow:hidden на треке обрезает углы сегментов по внешнему радиусу.
const STYLES = `
:where([data-vibeui-block="buttongroup-020"]){
--vibeui-buttongroup-020-surface:oklch(1 0 0);
--vibeui-buttongroup-020-fg:oklch(0.25 0.016 265);
--vibeui-buttongroup-020-muted:oklch(0.57 0.014 265);
--vibeui-buttongroup-020-border:oklch(0.88 0.008 265);
--vibeui-buttongroup-020-on:oklch(0.965 0.03 250);
--vibeui-buttongroup-020-accent:oklch(0.5 0.14 250);
--vibeui-buttongroup-020-radius:0.75rem;
--vibeui-buttongroup-020-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="buttongroup-020"]{
box-sizing:border-box;display:block;width:100%;max-width:24rem;
margin:0;padding:0;border:0;
font-family:var(--vibeui-buttongroup-020-font);
}
[data-vibeui-block="buttongroup-020"] *{box-sizing:border-box}
[data-vibeui-block="buttongroup-020"] legend{
position:absolute;width:1px;height:1px;padding:0;margin:-1px;
overflow:hidden;clip-path:inset(50%);white-space:nowrap;
}
/* Заливка трека видна в зазорах — это и есть разделители. */
[data-vibeui-block="buttongroup-020"] [data-part="track"]{
display:grid;grid-template-columns:repeat(auto-fit,minmax(6rem,1fr));
gap:1px;isolation:isolate;
border:1px solid var(--vibeui-buttongroup-020-border);
border-radius:var(--vibeui-buttongroup-020-radius);
background:var(--vibeui-buttongroup-020-border);
overflow:hidden;
}
[data-vibeui-block="buttongroup-020"] [data-part="segment"]{
position:relative;z-index:0;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.125rem;
padding:0.625rem 0.5rem;
background:var(--vibeui-buttongroup-020-surface);
color:var(--vibeui-buttongroup-020-muted);
cursor:pointer;
transition:background-color .16s ease,color .16s ease;
}
[data-vibeui-block="buttongroup-020"] input{
position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer;
}
[data-vibeui-block="buttongroup-020"] [data-part="symbol"]{
font-size:1.125rem;font-weight:700;line-height:1.1;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="buttongroup-020"] [data-part="code"]{
font-size:0.6875rem;font-weight:650;letter-spacing:0.06em;line-height:1.2;
}
[data-vibeui-block="buttongroup-020"] [data-part="name"]{
position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);
}
[data-vibeui-block="buttongroup-020"] [data-part="segment"]:hover{color:var(--vibeui-buttongroup-020-fg)}
[data-vibeui-block="buttongroup-020"] [data-part="segment"]:has(input:checked){
z-index:1;
background:var(--vibeui-buttongroup-020-on);
color:var(--vibeui-buttongroup-020-accent);
box-shadow:inset 0 0 0 1px var(--vibeui-buttongroup-020-accent);
}
[data-vibeui-block="buttongroup-020"] [data-part="segment"]:has(input:focus-visible){
z-index:2;
outline:2px solid var(--vibeui-buttongroup-020-accent);outline-offset:-2px;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="buttongroup-020"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_CURRENCIES: Buttongroup020Currency[] = [
  { code: "RUB", symbol: "₽", name: "российский рубль" },
  { code: "USD", symbol: "$", name: "доллар США" },
  { code: "EUR", symbol: "€", name: "евро" },
  { code: "GBP", symbol: "£", name: "фунт стерлингов" },
]

/**
 * Выбор валюты, где разделители — просветы сетки, а не рамки сегментов.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Buttongroup020({
  currencies = DEFAULT_CURRENCIES,
  defaultValue = "RUB",
  label = "Валюта отчёта",
  name = "buttongroup-020",
  accent,
  className,
  style,
  ...props
}: Buttongroup020Props) {
  const palette = {
    ...(accent ? { "--vibeui-buttongroup-020-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-buttongroup-020" precedence="medium">
        {STYLES}
      </style>
      <fieldset
        {...props}
        data-vibeui-block="buttongroup-020"
        className={className}
        style={palette}
      >
        <legend>{label}</legend>
        <div data-part="track">
          {currencies.map((currency) => (
            <label key={currency.code} data-part="segment">
              <input
                type="radio"
                name={name}
                value={currency.code}
                defaultChecked={currency.code === defaultValue}
              />
              <span data-part="symbol" aria-hidden="true">
                {currency.symbol}
              </span>
              <span data-part="code">{currency.code}</span>
              <span data-part="name">, {currency.name}</span>
            </label>
          ))}
        </div>
      </fieldset>
    </>
  )
}