Menu
Action Sheet
A bottom action sheet: large targets near the bottom edge and a separate cancel — closing must be as easy as opening.
- menu
- sheet
- mobile
- actions
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/menu-007?lang=en
The Catalog project
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 "menu-007" (Action Sheet) 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/menu-007.json
Registry item: https://vibeui.ru/r/menu-007.json
Installs to: components/vibeui/menu-007.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 bottom action sheet: large targets near the bottom edge and a separate cancel — closing must be as easy as opening.
A phone action sheet: a header naming the object, large items with explanations and a separate cancel button. Zero dependencies, one file.
## 3. How to use it
import { Menu007 } from "@/components/vibeui/menu-007"
<Menu007 title="The Catalog project" actions={actions} />
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 local --vibeui-menu-007-* palette — do not swap it for your theme tokens
- the 2.75rem item height: only a thumb reaches the bottom edge
- the separate large cancel — closing must be as easy as opening
- env(safe-area-inset-bottom): otherwise the sheet slides under the home bar
- the object's name in the header: "delete" without a name causes mistakes
- the explanation on the destructive action: what exactly happens
## 6. You may change
- the actions array and their explanations
- title — what the object is
- cancelLabel
- the accent through the accent prop
## 7. Rules
- Positioning is not part of the component: pinning it to the bottom is the caller's job.
- More than five actions do not fit: the rest belong on their own screen.
- A destructive action here deserves its own confirmation: one thumb tap is too easy.
- 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/menu-007.jsonhttps://vibeui.ru/r/menu-007.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-menu-007-*. Клиентский: "use client". Нижний отступ учитывает env(safe-area-inset-bottom), высота пункта 2.75rem — цель под большой палец. У пунктов есть пояснения второй строкой, опасное действие красится текстом, отмена вынесена отдельной кнопкой во всю ширину.
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 Menu007Action = {
label: string
hint?: string
danger?: boolean
}
export type Menu007Props = Omit<ComponentPropsWithoutRef<"div">, "children"> & {
title?: string
actions?: Menu007Action[]
cancelLabel?: string
accent?: string
}
// Идея компонента: меню действий, которое на телефоне выезжает снизу. Список у
// края экрана — единственное место, куда дотягивается большой палец; выпадающее
// меню в верхнем углу для этого не годится. Кнопка «отмена» стоит отдельно и
// крупно: закрыть лист должно быть так же легко, как открыть.
const STYLES = `
:where([data-vibeui-block="menu-007"]){
--vibeui-menu-007-bg:oklch(1 0 0);
--vibeui-menu-007-fg:oklch(0.24 0.014 265);
--vibeui-menu-007-muted:oklch(0.56 0.014 265);
--vibeui-menu-007-border:oklch(0.9 0.006 265);
--vibeui-menu-007-hover:oklch(0.96 0.004 265);
--vibeui-menu-007-danger:oklch(0.56 0.19 25);
--vibeui-menu-007-accent:oklch(0.55 0.17 265);
--vibeui-menu-007-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="menu-007"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:17rem;box-sizing:border-box;
font-family:var(--vibeui-menu-007-font);color:var(--vibeui-menu-007-fg);
}
[data-vibeui-block="menu-007"] [data-part="trigger"]{
appearance:none;cursor:pointer;align-self:flex-start;
height:2.25rem;padding:0 0.875rem;
border:1px solid var(--vibeui-menu-007-border);border-radius:0.625rem;
background:var(--vibeui-menu-007-bg);color:inherit;
font:inherit;font-size:0.8125rem;font-weight:600;
}
[data-vibeui-block="menu-007"] [data-part="trigger"]:focus-visible{outline:2px solid var(--vibeui-menu-007-accent);outline-offset:2px}
/* Лист прижат к нижнему краю: туда дотягивается большой палец. */
[data-vibeui-block="menu-007"] [data-part="sheet"]{
display:flex;flex-direction:column;gap:0.5rem;
padding:0.5rem 0.5rem calc(0.5rem + env(safe-area-inset-bottom,0px));
border:1px solid var(--vibeui-menu-007-border);
border-radius:1.125rem 1.125rem 0.875rem 0.875rem;
background:var(--vibeui-menu-007-bg);
box-shadow:0 -12px 32px -24px oklch(0.2 0.02 265 / 55%);
}
[data-vibeui-block="menu-007"] [data-part="grabber"]{
align-self:center;width:2.25rem;height:0.25rem;margin:0.125rem 0 0.25rem;
border-radius:9999px;background:var(--vibeui-menu-007-border);
}
[data-vibeui-block="menu-007"] [data-part="title"]{
padding:0 0.5rem 0.25rem;font-size:0.75rem;color:var(--vibeui-menu-007-muted);
}
[data-vibeui-block="menu-007"] [data-part="item"]{
display:flex;flex-direction:column;gap:0.0625rem;
width:100%;min-height:2.75rem;padding:0.4375rem 0.75rem;
appearance:none;border:0;border-radius:0.75rem;background:transparent;color:inherit;
font:inherit;font-size:0.9375rem;font-weight:600;text-align:left;cursor:pointer;
}
[data-vibeui-block="menu-007"] [data-part="item"]:hover{background:var(--vibeui-menu-007-hover)}
[data-vibeui-block="menu-007"] [data-part="item"]:focus-visible{outline:2px solid var(--vibeui-menu-007-accent);outline-offset:-2px}
[data-vibeui-block="menu-007"] [data-part="item"][data-danger="true"]{color:var(--vibeui-menu-007-danger)}
[data-vibeui-block="menu-007"] [data-part="hint"]{font-size:0.75rem;font-weight:500;color:var(--vibeui-menu-007-muted)}
/* Отмена крупная и отдельная: закрыть должно быть так же легко, как открыть. */
[data-vibeui-block="menu-007"] [data-part="cancel"]{
width:100%;min-height:2.75rem;
appearance:none;border:1px solid var(--vibeui-menu-007-border);border-radius:0.75rem;
background:var(--vibeui-menu-007-bg);color:inherit;
font:inherit;font-size:0.9375rem;font-weight:650;cursor:pointer;
}
[data-vibeui-block="menu-007"] [data-part="cancel"]:focus-visible{outline:2px solid var(--vibeui-menu-007-accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="menu-007"] *{animation:none!important;transition:none!important}}
`
const DEFAULT_ACTIONS: Menu007Action[] = [
{ label: "Поделиться", hint: "Ссылка с правом на просмотр" },
{ label: "Дублировать", hint: "Копия появится рядом" },
{ label: "Переместить в архив" },
{ label: "Удалить", hint: "Отменить будет нельзя", danger: true },
]
/**
* Лист действий снизу: крупные цели и отдельная кнопка отмены.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Menu007({
title = "Проект «Каталог»",
actions = DEFAULT_ACTIONS,
cancelLabel = "Отмена",
accent,
className,
style,
...props
}: Menu007Props) {
const [open, setOpen] = useState(true)
const palette = {
...(accent ? { "--vibeui-menu-007-accent": accent } : null),
...style,
} as CSSProperties
return (
<>
<style href="vibeui-menu-007" precedence="medium">
{STYLES}
</style>
<div
{...props}
data-vibeui-block="menu-007"
className={className}
style={palette}
>
{open ? null : (
<button
type="button"
data-part="trigger"
onClick={() => setOpen(true)}
>
Действия
</button>
)}
{open ? (
<div data-part="sheet" role="menu" aria-label={title}>
<span data-part="grabber" aria-hidden="true" />
<p data-part="title">{title}</p>
{actions.map((action) => (
<button
key={action.label}
type="button"
role="menuitem"
data-part="item"
data-danger={action.danger}
onClick={() => setOpen(false)}
>
{action.label}
{action.hint ? (
<span data-part="hint">{action.hint}</span>
) : null}
</button>
))}
<button
type="button"
data-part="cancel"
onClick={() => setOpen(false)}
>
{cancelLabel}
</button>
</div>
) : null}
</div>
</>
)
}