Buttons

Add To Cart Button

An add-to-cart button where adding changes not only the look but the meaning of the click: now it opens the cart.

  • button
  • cart
  • commerce
  • 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.

put this in the header: https://vibeui.ru/c/button-049?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-049" (Add To Cart 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-049.json

Registry item: https://vibeui.ru/r/button-049.json
Installs to: components/vibeui/button-049.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
An add-to-cart button where adding changes not only the look but the meaning of the click: now it opens the cart.

A product button with two states. Before adding it is an accent 'Add to cart' with a bag mark; after, a green 'In cart — open' with a checkmark, and the click now opens the cart instead of adding a second time. The change goes into aria-live, so it is heard without a screen too. Zero dependencies, one file.

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

<Button049 onAdd={addToCart} onOpenCart={openCart}>
  Add to cart
</Button049>

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 click changing meaning with the state: adding the same item twice is almost always a user error
- the hidden aria-live line: without it the fact of adding reaches the eyes only
- the fixed minimum width — otherwise the button jumps when the label changes
- the bag mark switching to a checkmark alongside the colour: colour alone is not read by everyone
- the two separate onAdd and onOpenCart handlers — your app needs to know which action happened
- the green palette of the in-cart state, distinct from the resting accent

## 6. You may change
- the children and addedLabel labels
- the onAdd and onOpenCart handlers
- the initial state through the defaultAdded prop
- the accent colour through the accent prop
- outer spacing through className

## 7. Rules
- The component knows nothing about the cart: it only flips state and calls handlers. Syncing with the real cart is your app's job.
- Do not leave the in-cart state when the server request failed: reset it through defaultAdded or by remounting.
- Do not turn the button into a quantity counter — that needs a dedicated stepper.
- 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-049.json
https://vibeui.ru/r/button-049.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-button-049-*. Клиентский компонент: состояние «в корзине» внутреннее, наружу уходят два разных обработчика — onAdd и onOpenCart, компонент сам решает, какой вызвать. Минимальная ширина зафиксирована, поэтому при смене подписи кнопка не прыгает. Смена состояния объявляется скрытой строкой role="status" с aria-live="polite".

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 Button049Props = Omit<
  ComponentPropsWithoutRef<"button">,
  "children" | "onClick"
> & {
  children?: string
  /** Подпись после добавления: она же приглашение перейти в корзину. */
  addedLabel?: string
  defaultAdded?: boolean
  onAdd?: () => void
  onOpenCart?: () => void
  accent?: string
}

// Идея компонента: одна кнопка на два разных действия. Пока товара нет
// в корзине — «В корзину»; после добавления она меняет и цвет, и смысл:
// теперь ведёт в корзину. Смена состояния объявляется в aria-live, иначе
// человек со скринридером не узнает, что добавление прошло.
const STYLES = `
:where([data-vibeui-block="button-049"]){
--vibeui-button-049-accent:oklch(0.53 0.16 275);
--vibeui-button-049-done:oklch(0.5 0.13 155);
--vibeui-button-049-fg:oklch(0.99 0.01 275);
--vibeui-button-049-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="button-049"]{
position:relative;appearance:none;border:0;cursor:pointer;box-sizing:border-box;
display:inline-flex;align-items:center;justify-content:center;gap:0.5rem;
min-width:11.5rem;height:2.75rem;padding:0 1.125rem;border-radius:0.75rem;
background:var(--vibeui-button-049-accent);color:var(--vibeui-button-049-fg);
font-family:var(--vibeui-button-049-font);font-size:0.875rem;font-weight:650;line-height:1;
transition:background-color .2s ease,filter .16s ease;
}
[data-vibeui-block="button-049"][data-added="true"]{background:var(--vibeui-button-049-done)}
[data-vibeui-block="button-049"]:hover:not(:disabled){filter:brightness(1.07)}
[data-vibeui-block="button-049"]:focus-visible{outline:2px solid var(--vibeui-button-049-accent);outline-offset:3px}
[data-vibeui-block="button-049"][data-added="true"]:focus-visible{outline-color:var(--vibeui-button-049-done)}
[data-vibeui-block="button-049"]:disabled{cursor:not-allowed;opacity:.55}
/* Корзина: короб с ручкой, обе части — грани псевдоэлементов. */
[data-vibeui-block="button-049"] [data-part="bag"]{position:relative;flex:none;width:1rem;height:1.0625rem}
[data-vibeui-block="button-049"] [data-part="bag"]::before{
content:"";position:absolute;left:0;bottom:0;width:1rem;height:0.75rem;
box-sizing:border-box;border:1.75px solid currentColor;border-radius:0.1875rem;
}
[data-vibeui-block="button-049"] [data-part="bag"]::after{
content:"";position:absolute;left:0.25rem;top:0;width:0.5rem;height:0.4375rem;
box-sizing:border-box;border:1.75px solid currentColor;border-bottom:0;
border-radius:0.25rem 0.25rem 0 0;
}
[data-vibeui-block="button-049"] [data-part="check"]{position:relative;flex:none;width:1rem;height:1.0625rem}
[data-vibeui-block="button-049"] [data-part="check"]::after{
content:"";position:absolute;left:0.1875rem;top:50%;width:0.375rem;height:0.6875rem;
margin-top:-0.4375rem;box-sizing:border-box;
border:2px solid currentColor;border-top:0;border-left:0;transform:rotate(42deg);
}
[data-vibeui-block="button-049"] [data-part="live"]{
position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="button-049"]{transition:none!important}}
`

/**
 * Кнопка «в корзину» с состоянием «в корзине», которое меняет и смысл клика.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Button049({
  children = "В корзину",
  addedLabel = "В корзине — открыть",
  defaultAdded = false,
  onAdd,
  onOpenCart,
  accent,
  type = "button",
  className,
  style,
  ...props
}: Button049Props) {
  const [added, setAdded] = useState(defaultAdded)

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

  return (
    <>
      <style href="vibeui-button-049" precedence="medium">
        {STYLES}
      </style>
      <button
        {...props}
        type={type}
        data-vibeui-block="button-049"
        data-added={String(added)}
        className={className}
        style={palette}
        onClick={() => {
          if (added) {
            onOpenCart?.()
            return
          }

          setAdded(true)
          onAdd?.()
        }}
      >
        <span
          data-part={added ? "check" : "bag"}
          aria-hidden="true"
          key={added ? "check" : "bag"}
        />
        {added ? addedLabel : children}
        <span data-part="live" role="status" aria-live="polite">
          {added ? "Товар добавлен в корзину" : ""}
        </span>
      </button>
    </>
  )
}