Buttons
Native Share Button
A share button with two paths: the system sheet where it exists, and a quiet link copy with a receipt where it does not.
- button
- share
- clipboard
- progressive
Preview
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 3. The agent opens the link and installs the component from the registry.
put this in the header: https://vibeui.ru/c/button-054?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-054" (Native Share 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-054.json
Registry item: https://vibeui.ru/r/button-054.json
Installs to: components/vibeui/button-054.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 share button with two paths: the system sheet where it exists, and a quiet link copy with a receipt where it does not.
One handler covering two scenarios. Where navigator.share exists, the system share sheet opens. Where it does not, the link quietly goes to the clipboard, the mark turns into a checkmark, the label into 'Link copied', and a couple of seconds later everything returns. The receipt is mirrored in aria-live. Zero dependencies, one file.
## 3. How to use it
import { Button054 } from "@/components/vibeui/button-054"
<Button054 url="https://vibeui.dev/blocks" title="VibeUI">
Share
</Button054>
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 navigator.share check before calling it: on desktop it is missing, and without a fallback the button would do nothing
- catching the share-sheet dismissal: the user closing the sheet is not an error and needs no receipt
- the copy receipt inside the button itself — otherwise it is unclear whether anything happened
- clearing the timer in the useEffect cleanup: without it the state flips back after unmount
- aria-live on the hidden line: the receipt has to arrive without a screen too
- the fixed minimum width — labels of different length must not resize the button
## 6. You may change
- the children and copiedLabel labels
- the link through the url prop, where an empty string means the current address
- the share sheet's heading through the title prop
- the accent colour through the accent prop
- the length of the copied state in the useEffect handler
## 7. Rules
- navigator.share works only over HTTPS and only from a user gesture — never call it from a timer.
- Do not show the 'copied' receipt after the system sheet: the user may have chosen nothing.
- The clipboard can be blocked by page policy — the handler already survives that, do not remove the try/catch.
- 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-054.jsonhttps://vibeui.ru/r/button-054.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-button-054-*. Клиентский компонент. Обработчик проверяет navigator.share и при его наличии открывает системный лист; отказ пользователя (AbortError) перехватывается и не считается ошибкой. Иначе ссылка уходит в navigator.clipboard, и кнопка на 2,2 секунды переходит в состояние «скопировано» — таймер снимается в useEffect. Пустой url означает текущий адрес страницы, поэтому компонент безопасен при серверном рендере.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
"use client"
import { useEffect, useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Button054Props = Omit<
ComponentPropsWithoutRef<"button">,
"children"
> & {
children?: string
/** Ссылка, которой делятся. Пустая строка — текущий адрес страницы. */
url?: string
title?: string
/** Подпись после запасного пути: ссылка ушла в буфер обмена. */
copiedLabel?: string
accent?: string
}
// Идея компонента: два пути в одной кнопке. Если браузер умеет navigator.share
// — открывается системный лист «Поделиться»; если нет, ссылка молча уходит
// в буфер обмена, и кнопка сама об этом отчитывается. Отказ пользователя
// от системного листа (AbortError) не считается ошибкой и не даёт отчёта.
const STYLES = `
:where([data-vibeui-block="button-054"]){
--vibeui-button-054-accent:oklch(0.52 0.16 235);
--vibeui-button-054-done:oklch(0.5 0.13 155);
--vibeui-button-054-fg:oklch(0.99 0.01 235);
--vibeui-button-054-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="button-054"]{
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:9.5rem;height:2.625rem;padding:0 1.125rem;border-radius:0.75rem;
background:var(--vibeui-button-054-accent);color:var(--vibeui-button-054-fg);
font-family:var(--vibeui-button-054-font);font-size:0.875rem;font-weight:650;line-height:1;
transition:background-color .2s ease,filter .16s ease;
}
[data-vibeui-block="button-054"][data-copied="true"]{background:var(--vibeui-button-054-done)}
[data-vibeui-block="button-054"]:hover:not(:disabled){filter:brightness(1.07)}
[data-vibeui-block="button-054"]:focus-visible{outline:2px solid var(--vibeui-button-054-accent);outline-offset:3px}
[data-vibeui-block="button-054"]:disabled{cursor:not-allowed;opacity:.55}
/* Знак «поделиться»: три узла и две связи между ними. */
[data-vibeui-block="button-054"] [data-part="share"]{position:relative;flex:none;width:1rem;height:1rem}
[data-vibeui-block="button-054"] [data-part="share"] i{
position:absolute;width:0.375rem;height:0.375rem;border-radius:50%;
background:currentColor;
}
[data-vibeui-block="button-054"] [data-part="share"] i:nth-child(1){right:0;top:0}
[data-vibeui-block="button-054"] [data-part="share"] i:nth-child(2){left:0;top:50%;margin-top:-0.1875rem}
[data-vibeui-block="button-054"] [data-part="share"] i:nth-child(3){right:0;bottom:0}
[data-vibeui-block="button-054"] [data-part="share"]::before,
[data-vibeui-block="button-054"] [data-part="share"]::after{
content:"";position:absolute;left:0.25rem;top:50%;width:0.6875rem;height:1.25px;
background:currentColor;transform-origin:left center;
}
[data-vibeui-block="button-054"] [data-part="share"]::before{transform:rotate(-32deg)}
[data-vibeui-block="button-054"] [data-part="share"]::after{transform:rotate(32deg)}
[data-vibeui-block="button-054"] [data-part="check"]{position:relative;flex:none;width:1rem;height:1rem}
[data-vibeui-block="button-054"] [data-part="check"]::after{
content:"";position:absolute;left:0.1875rem;top:0.0625rem;width:0.375rem;height:0.6875rem;
box-sizing:border-box;border:2px solid currentColor;border-top:0;border-left:0;
transform:rotate(42deg);
}
[data-vibeui-block="button-054"] [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-054"]{transition:none!important}}
`
/**
* Кнопка «поделиться» с нативным листом и копированием ссылки как запасным путём.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Button054({
children = "Поделиться",
url = "",
title = "VibeUI",
copiedLabel = "Ссылка скопирована",
accent,
type = "button",
className,
style,
...props
}: Button054Props) {
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!copied) return
const timer = window.setTimeout(() => setCopied(false), 2200)
return () => window.clearTimeout(timer)
}, [copied])
const palette = {
...(accent ? { "--vibeui-button-054-accent": accent } : null),
...style,
} as CSSProperties
const share = async () => {
const link = url || window.location.href
if (navigator.share) {
try {
await navigator.share({ title, url: link })
} catch {
// Отказ от системного листа — не ошибка: молча выходим.
}
return
}
try {
await navigator.clipboard.writeText(link)
setCopied(true)
} catch {
setCopied(false)
}
}
return (
<>
<style href="vibeui-button-054" precedence="medium">
{STYLES}
</style>
<button
{...props}
type={type}
data-vibeui-block="button-054"
data-copied={String(copied)}
className={className}
style={palette}
onClick={share}
>
{copied ? (
<span data-part="check" aria-hidden="true" />
) : (
<span data-part="share" aria-hidden="true">
<i />
<i />
<i />
</span>
)}
{copied ? copiedLabel : children}
<span data-part="live" role="status" aria-live="polite">
{copied ? copiedLabel : ""}
</span>
</button>
</>
)
}