Dialog
Action Sheet
An action sheet that slides up from the bottom on a phone and becomes an ordinary centred dialog on a wide screen: one markup, two layouts.
- dialog
- 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/dialog-007?lang=en
Studio site
Published 12 March
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 "dialog-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/dialog-007.json
Registry item: https://vibeui.ru/r/dialog-007.json
Installs to: components/vibeui/dialog-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
An action sheet that slides up from the bottom on a phone and becomes an ordinary centred dialog on a wide screen: one markup, two layouts.
An action sheet: on a phone it slides up from the bottom with a grip, on a wide screen it turns into a centred dialog. The items are links, the destructive one is coloured, and cancel sits at the bottom as its own button. Zero dependencies, one file, its own palette, no client JS.
## 3. How to use it
import { Dialog007 } from "@/components/vibeui/dialog-007"
<Dialog007
id="project-actions"
title="Studio site"
actions={[{ label: "Duplicate", href: "#" }, { label: "Delete", href: "#", danger: true }]}
/>
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-dialog-007-* palette — do not swap it for your theme tokens (bg-popover, bg-muted and the like)
- the bottom placement on phones: a thumb does not reach the top of the screen
- the grip in the mobile layout — it says where the sheet came from
- the media query on the viewport rather than a container query: the sheet is positioned against the screen
- the tall action rows (no less than 2.75rem) — they are hit with a thumb
- the separate cancel button: dismissing by tapping outside is harder on a phone than it looks
## 6. You may change
- the actions array: labels, hrefs and the danger flag
- children — your own content instead of the list
- the title, description and cancelLabel copy
- the 40rem breakpoint if your mobile boundary differs
## 7. Rules
- More than six actions do not fit: move the rest to a page.
- Put the destructive action last and colour it — but do not make it the only thing that stands out.
- The items are links. If one runs an action, swap <a> for <button type="button"> in your copy.
- 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/dialog-007.jsonhttps://vibeui.ru/r/dialog-007.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-dialog-007-*. По умолчанию лист прижат к нижнему краю экрана и выезжает через translate; от 40rem ширины окна медиазапрос превращает его в обычное окно по центру. Медиазапрос здесь по ширине окна, а не блока: лист позиционируется относительно экрана, а не родителя, поэтому container-запрос был бы неверен.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
import type { CSSProperties, ReactNode } from "react"
export type Dialog007Action = {
label: string
href?: string
danger?: boolean
}
export type Dialog007Props = {
id?: string
trigger?: string
title?: string
description?: string
actions?: Dialog007Action[]
cancelLabel?: string
children?: ReactNode
className?: string
style?: CSSProperties
}
// Идея компонента: лист действий, выезжающий снизу, — привычная форма выбора
// на телефоне, где до верха экрана большой палец не достаёт. На широком экране
// он превращается в обычное окно по центру: одна разметка, две раскладки,
// переключение обычным медиазапросом по ширине окна, а не блока — лист
// позиционируется относительно экрана, а не родителя.
const STYLES = `
:where([data-vibeui-block="dialog-007"]){
--vibeui-dialog-007-fg:oklch(0.22 0.016 265);
--vibeui-dialog-007-muted:oklch(0.5 0.014 265);
--vibeui-dialog-007-bg:oklch(1 0 0);
--vibeui-dialog-007-border:oklch(0.9 0.006 265);
--vibeui-dialog-007-danger:oklch(0.56 0.19 25);
--vibeui-dialog-007-accent:oklch(0.55 0.2 262);
--vibeui-dialog-007-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="dialog-007"]{display:inline-flex;font-family:var(--vibeui-dialog-007-font)}
[data-vibeui-block="dialog-007"] [data-part="trigger"]{
appearance:none;cursor:pointer;font:inherit;font-size:0.875rem;font-weight:500;
display:inline-flex;align-items:center;height:2.25rem;padding:0 0.9375rem;
border:1px solid var(--vibeui-dialog-007-border);border-radius:0.5rem;
background:var(--vibeui-dialog-007-bg);color:var(--vibeui-dialog-007-fg);
}
[data-vibeui-block="dialog-007"] [data-part="trigger"]:hover{background:color-mix(in oklab,var(--vibeui-dialog-007-border) 30%,transparent)}
[data-vibeui-block="dialog-007"] [data-part="trigger"]:focus-visible{outline:2px solid var(--vibeui-dialog-007-accent);outline-offset:2px}
/* Мобильная раскладка по умолчанию: лист прижат к нижнему краю экрана. */
[data-vibeui-dialog-007-sheet]{
position:fixed;inset:auto 0 0;margin:0;
width:100%;box-sizing:border-box;padding:0.75rem 0.75rem 1rem;
border:0;border-radius:1.25rem 1.25rem 0 0;
background:var(--vibeui-dialog-007-bg,oklch(1 0 0));
color:var(--vibeui-dialog-007-fg,oklch(0.22 0.016 265));
font-family:var(--vibeui-dialog-007-font,ui-sans-serif,system-ui,sans-serif);
box-shadow:0 -12px 40px -20px oklch(0.2 0.03 265 / 45%);
translate:0 100%;
transition:translate .24s cubic-bezier(.32,.72,0,1),display .24s allow-discrete,overlay .24s allow-discrete;
}
[data-vibeui-dialog-007-sheet]:popover-open{translate:0 0}
@starting-style{[data-vibeui-dialog-007-sheet]:popover-open{translate:0 100%}}
[data-vibeui-dialog-007-sheet]::backdrop{background:oklch(0.18 0.02 265 / 45%)}
/* Полоска-ручка: сообщает, что лист пришёл снизу и туда же уйдёт. */
[data-vibeui-dialog-007-sheet] [data-part="grip"]{
display:block;width:2.25rem;height:0.25rem;margin:0 auto 0.75rem;
border-radius:9999px;background:var(--vibeui-dialog-007-border,oklch(0.9 0.006 265));
}
[data-vibeui-dialog-007-sheet] [data-part="head"]{padding:0 0.5rem 0.625rem}
[data-vibeui-dialog-007-sheet] [data-part="title"]{margin:0;font-size:0.9375rem;font-weight:620;line-height:1.35}
[data-vibeui-dialog-007-sheet] [data-part="description"]{margin:0.125rem 0 0;font-size:0.8125rem;line-height:1.45;color:var(--vibeui-dialog-007-muted,oklch(0.5 0.014 265))}
[data-vibeui-dialog-007-sheet] [data-part="list"]{display:flex;flex-direction:column;gap:0.125rem}
[data-vibeui-dialog-007-sheet] [data-part="action"]{
display:flex;align-items:center;width:100%;box-sizing:border-box;
padding:0.8125rem 0.75rem;border:0;border-radius:0.75rem;
background:transparent;color:inherit;text-decoration:none;
font:inherit;font-size:0.9375rem;cursor:pointer;text-align:left;
transition:background-color .16s ease;
}
[data-vibeui-dialog-007-sheet] [data-part="action"]:hover{background:color-mix(in oklab,var(--vibeui-dialog-007-border,oklch(0.9 0.006 265)) 35%,transparent)}
[data-vibeui-dialog-007-sheet] [data-part="action"][data-danger="true"]{color:var(--vibeui-dialog-007-danger,oklch(0.56 0.19 25))}
[data-vibeui-dialog-007-sheet] [data-cancel="true"]{
margin-top:0.5rem;justify-content:center;font-weight:600;
background:color-mix(in oklab,var(--vibeui-dialog-007-border,oklch(0.9 0.006 265)) 35%,transparent);
}
[data-vibeui-dialog-007-sheet] :focus-visible{outline:2px solid var(--vibeui-dialog-007-accent,oklch(0.55 0.2 262));outline-offset:-2px}
/* От 40rem ширины окна лист становится обычным окном по центру. */
@media (min-width: 40rem){
[data-vibeui-dialog-007-sheet]{
inset:0;margin:auto;height:fit-content;
width:min(22rem,calc(100vw - 2rem));
border:1px solid var(--vibeui-dialog-007-border,oklch(0.9 0.006 265));
border-radius:1rem;padding:1rem;
translate:0 0;opacity:0;transform:scale(0.97);
transition:opacity .18s ease,transform .18s ease,display .18s allow-discrete,overlay .18s allow-discrete;
}
[data-vibeui-dialog-007-sheet]:popover-open{opacity:1;transform:none}
@starting-style{[data-vibeui-dialog-007-sheet]:popover-open{opacity:0;transform:scale(0.97)}}
[data-vibeui-dialog-007-sheet] [data-part="grip"]{display:none}
}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="dialog-007"] *{animation:none!important;transition:none!important}
[data-vibeui-dialog-007-sheet]{transition:none!important;translate:0 0;opacity:1;transform:none}
}
`
const DEFAULT_ACTIONS: Dialog007Action[] = [
{ label: "Дублировать проект", href: "#" },
{ label: "Экспортировать в архив", href: "#" },
{ label: "Перенести в другую папку", href: "#" },
{ label: "Удалить проект", href: "#", danger: true },
]
/**
* Лист действий снизу на телефоне и обычное окно на широком экране.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Dialog007({
id = "vibeui-dialog-007",
trigger = "Действия",
title = "Сайт студии",
description = "Опубликован 12 марта",
actions = DEFAULT_ACTIONS,
cancelLabel = "Отмена",
children,
className,
style,
}: Dialog007Props) {
return (
<>
<style href="vibeui-dialog-007" precedence="medium">
{STYLES}
</style>
<div data-vibeui-block="dialog-007" className={className} style={style}>
<button data-part="trigger" type="button" popoverTarget={id}>
{trigger}
</button>
<div
id={id}
popover="auto"
data-vibeui-dialog-007-sheet=""
role="dialog"
aria-labelledby={`${id}-title`}
style={style}
>
<span data-part="grip" aria-hidden="true" />
<div data-part="head">
<p data-part="title" id={`${id}-title`}>
{title}
</p>
{description ? <p data-part="description">{description}</p> : null}
</div>
<div data-part="list">
{children ??
actions.map((action) => (
<a
key={action.label}
data-part="action"
data-danger={action.danger || undefined}
href={action.href}
>
{action.label}
</a>
))}
<button
data-part="action"
data-cancel="true"
type="button"
popoverTarget={id}
>
{cancelLabel}
</button>
</div>
</div>
</div>
</>
)
}