Buttons

Pill Toggle Button

A button that toggles: label and track share one shape, and the state is announced through aria-pressed. Self-contained — one file, no dependencies.

  • button
  • toggle
  • switch
  • pill
  • stateful

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.
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 "button-009" (Pill Toggle Button) from VibeUI

## 1. Install first — do not skip, do not recreate
Install command is unavailable: the VibeUI registry URL is not configured.

Installs to: components/vibeui/button-009.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 button that toggles: label and track share one shape, and the state is announced through aria-pressed. Self-contained — one file, no dependencies.

A button and a switch in one shape. The label sits on the left and changes with the state; a track with a round knob sits on the right and the knob slides across when pressed. Turning it on changes both the pill background and the track colour. The state is announced through aria-pressed, so this stays a button rather than becoming a checkbox.

## 3. How to use it
import { Button009 } from "@/components/vibeui/button-009"

<Button009
  defaultPressed={false}
  onPressedChange={setNotifications}
  onLabel="Notifications on"
  offLabel="Notifications off"
/>

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
- aria-pressed as the carrier of state — do not swap it for a checkbox or role=switch without reworking the markup
- the label changing with the state: the state has to be readable without colour
- the knob sliding and the track changing colour — together, since separately the state reads worse
- the pill shape and the track sitting to the right of the label
- the different background and text colour between the on and off states

## 6. You may change
- the onLabel and offLabel labels
- the starting state through defaultPressed
- the onPressedChange handler
- the on-state colour through the accent prop
- outer spacing through className

## 7. Rules
- Do not pass onClick: the component owns the toggle, and changes arrive through onPressedChange.
- Do not leave onLabel and offLabel identical — the state would then be visible only by colour.
- 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

The install command is unavailable: the environment variableREGISTRY_BASE_URL

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-button-009-*. Клиентский компонент: состояние переключателя внутреннее, наружу отдаётся через onPressedChange. Начальное состояние — defaultPressed.

Component source

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

"use client"

import { useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Button009Props = Omit<
  ComponentPropsWithoutRef<"button">,
  "onClick"
> & {
  defaultPressed?: boolean
  onPressedChange?: (pressed: boolean) => void
  onLabel?: string
  offLabel?: string
  accent?: string
}

// Идея компонента: кнопка и переключатель в одном пятне. Справа — дорожка
// с бегунком, который переезжает при нажатии, подпись меняется вместе
// с состоянием. Состояние объявлено через aria-pressed, поэтому кнопка
// остаётся кнопкой, а не чекбоксом.
const STYLES = `
:where([data-vibeui-block="button-009"]){
--vibeui-button-009-bg:oklch(0.96 0.004 265);
--vibeui-button-009-fg:oklch(0.42 0.014 265);
--vibeui-button-009-border:oklch(0.55 0.02 265 / 24%);
--vibeui-button-009-on-bg:oklch(0.94 0.045 160);
--vibeui-button-009-on-fg:oklch(0.36 0.08 160);
--vibeui-button-009-accent:oklch(0.62 0.15 160);
--vibeui-button-009-track:oklch(0.55 0.02 265 / 26%);
--vibeui-button-009-ring:oklch(0.55 0.02 265 / 60%);
--vibeui-button-009-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="button-009"]{
appearance:none;cursor:pointer;
display:inline-flex;align-items:center;gap:0.625rem;
height:2.5rem;padding:0 0.5rem 0 1rem;
border:1px solid var(--vibeui-button-009-border);border-radius:9999px;
font-family:var(--vibeui-button-009-font);font-size:0.875rem;font-weight:500;line-height:1;
background:var(--vibeui-button-009-bg);color:var(--vibeui-button-009-fg);
transition:background-color .2s ease,color .2s ease,border-color .2s ease;
}
[data-vibeui-block="button-009"][aria-pressed="true"]{
background:var(--vibeui-button-009-on-bg);color:var(--vibeui-button-009-on-fg);
border-color:color-mix(in oklab, var(--vibeui-button-009-accent) 40%, transparent);
}
[data-vibeui-block="button-009"] [data-part="track"]{
position:relative;width:2rem;height:1.125rem;flex:none;border-radius:9999px;
background:var(--vibeui-button-009-track);transition:background-color .2s ease;
}
[data-vibeui-block="button-009"][aria-pressed="true"] [data-part="track"]{background:var(--vibeui-button-009-accent)}
[data-vibeui-block="button-009"] [data-part="thumb"]{
position:absolute;top:0.1875rem;left:0.1875rem;width:0.75rem;height:0.75rem;border-radius:9999px;
background:oklch(1 0 0);box-shadow:0 1px 2px oklch(0 0 0 / 25%);
transition:transform .22s cubic-bezier(0.16,1,0.3,1);
}
[data-vibeui-block="button-009"][aria-pressed="true"] [data-part="thumb"]{transform:translateX(0.875rem)}
[data-vibeui-block="button-009"]:hover:not(:disabled){border-color:color-mix(in oklab, var(--vibeui-button-009-fg) 35%, transparent)}
[data-vibeui-block="button-009"]:focus-visible{outline:2px solid var(--vibeui-button-009-ring);outline-offset:2px}
[data-vibeui-block="button-009"]:disabled{cursor:not-allowed;opacity:.55}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="button-009"] *{transition:none!important}}
`

/**
 * Кнопка-переключатель: подпись и бегунок в одном пятне. Один файл, ноль
 * зависимостей. Клиентский компонент: состояние переключателя.
 */
export function Button009({
  defaultPressed = false,
  onPressedChange,
  onLabel = "Уведомления включены",
  offLabel = "Уведомления выключены",
  accent,
  type = "button",
  className,
  style,
  ...props
}: Button009Props) {
  const [pressed, setPressed] = useState(defaultPressed)

  const palette = {
    ...(accent ? { "--vibeui-button-009-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-button-009" precedence="medium">
        {STYLES}
      </style>
      <button
        {...props}
        type={type}
        data-vibeui-block="button-009"
        aria-pressed={pressed}
        className={className}
        style={palette}
        onClick={() => {
          const next = !pressed
          setPressed(next)
          onPressedChange?.(next)
        }}
      >
        {pressed ? onLabel : offLabel}
        <span data-part="track" aria-hidden="true">
          <span data-part="thumb" />
        </span>
      </button>
    </>
  )
}