Buttons

Dual Icon Button

A button with icons on both sides: the left one names the action, the right one promises what follows, and on hover they behave differently.

  • button
  • icon
  • leading
  • trailing

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-036?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-036" (Dual Icon 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-036.json

Registry item: https://vibeui.ru/r/button-036.json
Installs to: components/vibeui/button-036.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 with icons on both sides: the left one names the action, the right one promises what follows, and on hover they behave differently.

A filled button with two icons in different roles. The left one (a plus or a magnifier) names the action and animates in place: the plus turns, the magnifier grows. The right one (a chevron or an arrow) promises a transition and slides forward. Both are drawn in CSS. Zero dependencies, one file.

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

<Button036 leading="plus" trailing="chevron" onClick={createProject}>
  Create project
</Button036>

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 distinct roles of the icons: what we do on the left, what happens next on the right; swapping them makes no sense
- their different hover behaviour: the left one animates in place, the right one slides away — that is how direction reads
- the asymmetric padding: smaller on the left, because an icon is optically heavier than empty space
- the icons built from pseudo-elements: they scale with currentColor and pull in no icon library
- aria-hidden on both icons — they decorate the label rather than replace it
- the none option on each side: the button has to work with a single icon too

## 6. You may change
- the label
- the left icon through the leading prop (plus, search or none)
- the right icon through the trailing prop (chevron, arrow or none)
- the accent colour through the accent prop
- the onClick handler and the rest of the native button props

## 7. Rules
- Do not make both icons directional: two arrows inside one button argue with each other.
- Do not replace the CSS icons with images: the component has to stay a single file with no network requests.
- Do not remove aria-hidden — otherwise a screen reader announces the decorative nodes as empty elements.
- 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-036.json
https://vibeui.ru/r/button-036.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-button-036-*. Серверный компонент. Оба значка — пустые span с data-part="lead"/"trail" и data-icon, форма рисуется гранями и фоном псевдоэлементов, поэтому ни SVG, ни иконочного шрифта не нужно. Левый паддинг меньше правого: значок оптически тяжелее пустоты. Значения "none" убирают узел из разметки.

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 Button036Props = ComponentPropsWithoutRef<"button"> & {
  /** Значок слева: он говорит, что за действие. */
  leading?: "plus" | "search" | "none"
  /** Значок справа: он говорит, что будет после клика. */
  trailing?: "chevron" | "arrow" | "none"
  accent?: string
}

// Идея компонента: два значка с разными ролями. Левый называет действие,
// правый обещает продолжение, поэтому у них разные отступы и разное
// поведение на наведении: левый оживает на месте, правый уезжает вперёд.
// Оба нарисованы гранями псевдоэлементов — ни SVG, ни шрифта иконок.
const STYLES = `
:where([data-vibeui-block="button-036"]){
--vibeui-button-036-accent:oklch(0.55 0.19 295);
--vibeui-button-036-fg:oklch(0.99 0.01 295);
--vibeui-button-036-radius:0.625rem;
--vibeui-button-036-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="button-036"]{
appearance:none;border:0;cursor:pointer;box-sizing:border-box;
display:inline-flex;align-items:center;gap:0.5rem;
height:2.5rem;padding:0 0.75rem 0 0.6875rem;border-radius:var(--vibeui-button-036-radius);
background:var(--vibeui-button-036-accent);color:var(--vibeui-button-036-fg);
font-family:var(--vibeui-button-036-font);font-size:0.875rem;font-weight:600;line-height:1;
transition:filter .16s ease;
}
[data-vibeui-block="button-036"]:hover:not(:disabled){filter:brightness(1.08)}
[data-vibeui-block="button-036"]:focus-visible{outline:2px solid var(--vibeui-button-036-accent);outline-offset:2px}
[data-vibeui-block="button-036"]:disabled{cursor:not-allowed;opacity:.5}
[data-vibeui-block="button-036"] [data-part="lead"],
[data-vibeui-block="button-036"] [data-part="trail"]{
position:relative;flex:none;width:0.875rem;height:0.875rem;
}
[data-vibeui-block="button-036"] [data-part="lead"][data-icon="plus"]::before,
[data-vibeui-block="button-036"] [data-part="lead"][data-icon="plus"]::after{
content:"";position:absolute;left:50%;top:50%;background:currentColor;border-radius:1px;
}
[data-vibeui-block="button-036"] [data-part="lead"][data-icon="plus"]::before{width:0.75rem;height:1.75px;margin:-0.875px 0 0 -0.375rem}
[data-vibeui-block="button-036"] [data-part="lead"][data-icon="plus"]::after{width:1.75px;height:0.75rem;margin:-0.375rem 0 0 -0.875px}
[data-vibeui-block="button-036"] [data-part="lead"][data-icon="search"]::before{
content:"";position:absolute;left:0;top:0;width:0.625rem;height:0.625rem;
box-sizing:border-box;border:1.75px solid currentColor;border-radius:50%;
}
[data-vibeui-block="button-036"] [data-part="lead"][data-icon="search"]::after{
content:"";position:absolute;right:0.0625rem;bottom:0.0625rem;width:0.3125rem;height:1.75px;
background:currentColor;border-radius:1px;transform:rotate(45deg);transform-origin:right center;
}
[data-vibeui-block="button-036"] [data-part="lead"]{transition:transform .16s ease}
[data-vibeui-block="button-036"]:hover:not(:disabled) [data-part="lead"]{transform:rotate(90deg)}
[data-vibeui-block="button-036"] [data-part="lead"][data-icon="search"]{transition:transform .16s ease}
[data-vibeui-block="button-036"]:hover:not(:disabled) [data-part="lead"][data-icon="search"]{transform:scale(1.12) rotate(0deg)}
[data-vibeui-block="button-036"] [data-part="trail"]::before{
content:"";position:absolute;right:0.1875rem;top:50%;
width:0.4375rem;height:0.4375rem;box-sizing:border-box;
border:1.75px solid currentColor;border-left:0;border-bottom:0;
transform:translateY(-50%) rotate(45deg);
}
[data-vibeui-block="button-036"] [data-part="trail"][data-icon="arrow"]::after{
content:"";position:absolute;left:0;top:50%;width:0.75rem;height:1.75px;
margin-top:-0.875px;background:currentColor;border-radius:1px;
}
[data-vibeui-block="button-036"] [data-part="trail"]{transition:transform .18s cubic-bezier(0.16,1,0.3,1)}
[data-vibeui-block="button-036"]:hover:not(:disabled) [data-part="trail"]{transform:translateX(3px)}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="button-036"] *{animation:none!important;transition:none!important}}
`

/**
 * Кнопка с двумя значками: левый называет действие, правый обещает переход.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Button036({
  leading = "plus",
  trailing = "chevron",
  accent,
  type = "button",
  className,
  style,
  children = "Создать проект",
  ...props
}: Button036Props) {
  const palette = {
    ...(accent ? { "--vibeui-button-036-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-button-036" precedence="medium">
        {STYLES}
      </style>
      <button
        {...props}
        type={type}
        data-vibeui-block="button-036"
        className={className}
        style={palette}
      >
        {leading === "none" ? null : (
          <span data-part="lead" data-icon={leading} aria-hidden="true" />
        )}
        {children}
        {trailing === "none" ? null : (
          <span data-part="trail" data-icon={trailing} aria-hidden="true" />
        )}
      </button>
    </>
  )
}