Buttons

Follow Button

A follow button with three labels across two states: 'Following' honestly turns into 'Unfollow' under the cursor and on focus.

  • button
  • follow
  • subscribe
  • toggle

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/button-051?lang=en

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-051" (Follow Button) 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/button-051.json

Registry item: https://vibeui.ru/r/button-051.json
Installs to: components/vibeui/button-051.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 follow button with three labels across two states: 'Following' honestly turns into 'Unfollow' under the cursor and on focus.

A follow button that does not lie about what a click will do. Not following: an accent 'Follow'. Following: a light 'Following' that turns into 'Unfollow' with a warning palette the moment it is hovered or focused. Both labels share one grid cell, so the width never jumps. Zero dependencies, one file.

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

<Button051 defaultFollowing onChange={setFollowing} />

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 three labels across two states: 'Following' reports status, 'Unfollow' reports the consequence of a click
- the swap firing on focus-visible too: from the keyboard the consequence must be just as visible
- both labels inside one grid cell — otherwise the button's width jumps under the cursor
- the warning palette only on 'Unfollow', not on the whole following state
- aria-pressed beside the visible label: the state has to reach the accessibility tree
- the pill shape and the different weights: following is an accent, status is a quiet outline

## 6. You may change
- the three labels through the followLabel, followingLabel and unfollowLabel props
- the initial state through the defaultFollowing prop
- the onChange handler: it receives the new state
- the accent colour through the accent prop
- the warning colour through the --vibeui-button-051-danger variable

## 7. Rules
- Do not drop the 'Unfollow' state: a button that still reads as 'following' under the cursor misleads about the click.
- Do not pass onClick: the component owns the toggle, and changes arrive through onChange.
- Do not make the labels wildly different in length — the width is set by the longest one.
- 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/button-051.json
https://vibeui.ru/r/button-051.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-button-051-*. Клиентский компонент: состояние внутреннее, наружу уходит через onChange, дополнительно объявлено через aria-pressed. Подмена подписи сделана двумя слоями в одной grid-ячейке (grid-area:1/1): обе строки всегда в разметке, ширина берётся по длинной, поэтому кнопка не меняет размер под курсором. Подмена срабатывает и на :focus-visible, а не только на :hover.

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 Button051Props = Omit<
  ComponentPropsWithoutRef<"button">,
  "children" | "onChange"
> & {
  followLabel?: string
  followingLabel?: string
  /** Подпись, которая подменяет «Вы подписаны» при наведении. */
  unfollowLabel?: string
  defaultFollowing?: boolean
  onChange?: (following: boolean) => void
  accent?: string
}

// Идея компонента: три подписи на два состояния. Подписан — «Вы подписаны»,
// и только под курсором или фокусом подпись честно превращается в «Отписаться»
// с тревожной палитрой. Подмена сделана двумя слоями в grid-ячейке: обе
// подписи всегда в разметке, ширина берётся по длинной, кнопка не прыгает.
const STYLES = `
:where([data-vibeui-block="button-051"]){
--vibeui-button-051-accent:oklch(0.5 0.16 265);
--vibeui-button-051-fg:oklch(0.99 0.01 265);
--vibeui-button-051-surface:oklch(1 0 0);
--vibeui-button-051-border:oklch(0.87 0.006 265);
--vibeui-button-051-ink:oklch(0.26 0.02 265);
--vibeui-button-051-danger:oklch(0.55 0.19 22);
--vibeui-button-051-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="button-051"]{
appearance:none;cursor:pointer;box-sizing:border-box;
display:inline-flex;align-items:center;justify-content:center;gap:0.5rem;
height:2.5rem;padding:0 1.125rem;border-radius:9999px;
border:1px solid transparent;
background:var(--vibeui-button-051-accent);color:var(--vibeui-button-051-fg);
font-family:var(--vibeui-button-051-font);font-size:0.875rem;font-weight:650;line-height:1;
transition:background-color .16s ease,color .16s ease,border-color .16s ease;
}
[data-vibeui-block="button-051"][data-following="true"]{
background:var(--vibeui-button-051-surface);color:var(--vibeui-button-051-ink);
border-color:var(--vibeui-button-051-border);
}
[data-vibeui-block="button-051"][data-following="true"]:hover,
[data-vibeui-block="button-051"][data-following="true"]:focus-visible{
background:color-mix(in oklab,var(--vibeui-button-051-danger) 10%,var(--vibeui-button-051-surface));
border-color:var(--vibeui-button-051-danger);color:var(--vibeui-button-051-danger);
}
[data-vibeui-block="button-051"][data-following="false"]:hover{
background:color-mix(in oklab,var(--vibeui-button-051-accent) 88%,black);
}
[data-vibeui-block="button-051"]:focus-visible{outline:2px solid var(--vibeui-button-051-accent);outline-offset:3px}
[data-vibeui-block="button-051"][data-following="true"]:focus-visible{outline-color:var(--vibeui-button-051-danger)}
/* Обе подписи лежат в одной ячейке: ширина считается по длинной. */
[data-vibeui-block="button-051"] [data-part="slot"]{display:grid}
[data-vibeui-block="button-051"] [data-part="slot"] > span{grid-area:1/1;transition:opacity .14s ease}
[data-vibeui-block="button-051"] [data-part="off"]{opacity:0}
[data-vibeui-block="button-051"][data-following="true"]:hover [data-part="on"],
[data-vibeui-block="button-051"][data-following="true"]:focus-visible [data-part="on"]{opacity:0}
[data-vibeui-block="button-051"][data-following="true"]:hover [data-part="off"],
[data-vibeui-block="button-051"][data-following="true"]:focus-visible [data-part="off"]{opacity:1}
[data-vibeui-block="button-051"] [data-part="tick"]{position:relative;flex:none;width:0.875rem;height:0.875rem}
[data-vibeui-block="button-051"] [data-part="tick"]::after{
content:"";position:absolute;left:0.25rem;top:0.0625rem;width:0.3125rem;height:0.625rem;
box-sizing:border-box;border:1.75px solid currentColor;border-top:0;border-left:0;
transform:rotate(42deg);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="button-051"] *{animation:none!important;transition:none!important}}
`

/**
 * Кнопка подписки с состоянием «вы подписаны» и подменой на «Отписаться».
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Button051({
  followLabel = "Подписаться",
  followingLabel = "Вы подписаны",
  unfollowLabel = "Отписаться",
  defaultFollowing = false,
  onChange,
  accent,
  type = "button",
  className,
  style,
  ...props
}: Button051Props) {
  const [following, setFollowing] = useState(defaultFollowing)

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

  return (
    <>
      <style href="vibeui-button-051" precedence="medium">
        {STYLES}
      </style>
      <button
        {...props}
        type={type}
        data-vibeui-block="button-051"
        data-following={String(following)}
        className={className}
        style={palette}
        aria-pressed={following}
        onClick={() => {
          const next = !following

          setFollowing(next)
          onChange?.(next)
        }}
      >
        {following ? <span data-part="tick" aria-hidden="true" /> : null}
        {following ? (
          <span data-part="slot">
            <span data-part="on">{followingLabel}</span>
            <span data-part="off" aria-hidden="true">
              {unfollowLabel}
            </span>
          </span>
        ) : (
          followLabel
        )}
      </button>
    </>
  )
}