Dialog
Two-Step Dialog
Мастер из двух шагов без состояния: текущий шаг держат скрытые radio, а экраны переключает правило :has — «Далее» и «Назад» это подписи, а не обработчики.
- dialog
- wizard
- steps
- no-js
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/dialog-009
Название проекта
Его увидят участники команды. Поменять можно в любой момент.
Адрес проекта
По нему сайт откроется до подключения своего домена.
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «dialog-009» (Two-Step Dialog) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/dialog-009.json
Registry item: https://vibeui.ru/r/dialog-009.json
Ставится в: components/vibeui/dialog-009.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Мастер из двух шагов без состояния: текущий шаг держат скрытые radio, а экраны переключает правило :has — «Далее» и «Назад» это подписи, а не обработчики.
Модальный мастер на два шага: точки прогресса сверху, экран с полем, кнопки «Далее» и «Назад». Шаг держат скрытые radio, переключение — правило :has, поэтому клиентского кода нет вовсе. Ноль зависимостей, один файл, своя палитра.
## 3. Как использовать
import { Dialog009 } from "@/components/vibeui/dialog-009"
<Dialog009
id="create-project"
steps={[{ title: "Название", description: "…", fieldLabel: "Название" }, { title: "Адрес", description: "…", fieldLabel: "Адрес" }]}
/>
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-dialog-009-* и не подменять её токенами вашей темы
- скрытые radio как хранилище шага: именно они заменяют состояние
- переключение экранов через :has(...:checked) на окне, а не display у соседей
- точки прогресса: мастер без индикатора ощущается бесконечным
- aria-hidden и tabIndex={-1} у служебных radio — в Tab-порядке им делать нечего
- стилизацию окна по атрибуту и палитру отдельным style — окно в верхнем слое
## 6. Можно менять
- steps — ровно два шага: заголовок, объяснение и подпись поля
- тексты nextLabel, backLabel и submitLabel
- акцент через проп accent — цвет точек и кнопок
- поля внутри шагов: замените input на свои в собственной копии
- trigger — подпись кнопки, открывающей мастер
## 7. Правила
- Шага ровно два. Для трёх и больше приём с :has разрастается — там уже нужен обычный мастер с состоянием.
- Значения полей между шагами не сохраняются компонентом: они остаются в DOM, читайте их при отправке.
- Кнопки перехода — это <label>, поэтому они не участвуют в отправке формы. Это намеренно.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/dialog-009.jsonhttps://vibeui.ru/r/dialog-009.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-dialog-009-*. Шаг хранят два скрытых radio, а видимость экранов переключает :has([data-part="state"][value=…]:checked) на окне. Кнопки «Далее» и «Назад» — это <label for>, поэтому переход между шагами не требует ни состояния, ни обработчиков. Точки прогресса подсвечиваются тем же правилом.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
import type { CSSProperties } from "react"
export type Dialog009Step = {
title: string
description: string
fieldLabel: string
placeholder?: string
}
export type Dialog009Props = {
id?: string
trigger?: string
steps?: [Dialog009Step, Dialog009Step]
nextLabel?: string
backLabel?: string
submitLabel?: string
accent?: string
className?: string
style?: CSSProperties
}
// Идея компонента: мастер из двух шагов без состояния. Текущий шаг хранят
// скрытые radio, а видимость экранов переключает :has(#…:checked) — поэтому
// «Далее» и «Назад» это обычные подписи к radio, а не обработчики. Точки
// прогресса сверху показывают, где человек находится: мастер без индикатора
// ощущается бесконечным.
const STYLES = `
:where([data-vibeui-block="dialog-009"]){
--vibeui-dialog-009-fg:oklch(0.22 0.016 265);
--vibeui-dialog-009-muted:oklch(0.5 0.014 265);
--vibeui-dialog-009-bg:oklch(1 0 0);
--vibeui-dialog-009-border:oklch(0.89 0.006 265);
--vibeui-dialog-009-accent:oklch(0.55 0.2 262);
--vibeui-dialog-009-radius:1rem;
--vibeui-dialog-009-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="dialog-009"]{display:inline-flex;font-family:var(--vibeui-dialog-009-font)}
[data-vibeui-block="dialog-009"] [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-009-border);border-radius:0.5rem;
background:var(--vibeui-dialog-009-bg);color:var(--vibeui-dialog-009-fg);
}
[data-vibeui-block="dialog-009"] [data-part="trigger"]:hover{background:color-mix(in oklab,var(--vibeui-dialog-009-border) 30%,transparent)}
[data-vibeui-block="dialog-009"] [data-part="trigger"]:focus-visible{outline:2px solid var(--vibeui-dialog-009-accent);outline-offset:2px}
[data-vibeui-dialog-009-window]{
position:fixed;inset:0;margin:auto;height:fit-content;
width:min(26rem,calc(100vw - 2rem));box-sizing:border-box;padding:1.375rem;
border:1px solid var(--vibeui-dialog-009-border,oklch(0.89 0.006 265));
border-radius:var(--vibeui-dialog-009-radius,1rem);
background:var(--vibeui-dialog-009-bg,oklch(1 0 0));
color:var(--vibeui-dialog-009-fg,oklch(0.22 0.016 265));
font-family:var(--vibeui-dialog-009-font,ui-sans-serif,system-ui,sans-serif);
box-shadow:0 24px 60px -24px oklch(0.2 0.03 265 / 45%);
opacity:0;transform:scale(0.97);
transition:opacity .18s ease,transform .18s ease,display .18s allow-discrete,overlay .18s allow-discrete;
}
[data-vibeui-dialog-009-window]:popover-open{opacity:1;transform:none}
@starting-style{[data-vibeui-dialog-009-window]:popover-open{opacity:0;transform:scale(0.97)}}
[data-vibeui-dialog-009-window]::backdrop{background:oklch(0.18 0.02 265 / 45%)}
/* Radio состояния шагов: их не видно, но именно они держат мастер. */
[data-vibeui-dialog-009-window] [data-part="state"]{position:absolute;opacity:0;pointer-events:none}
[data-vibeui-dialog-009-window] [data-part="dots"]{display:flex;gap:0.375rem;margin-bottom:0.875rem}
[data-vibeui-dialog-009-window] [data-part="dot"]{
width:1.5rem;height:0.25rem;border-radius:9999px;
background:var(--vibeui-dialog-009-border,oklch(0.89 0.006 265));
transition:background-color .18s ease;
}
[data-vibeui-dialog-009-window] [data-part="title"]{margin:0 0 0.25rem;font-size:1rem;font-weight:620;line-height:1.35}
[data-vibeui-dialog-009-window] [data-part="description"]{margin:0 0 0.875rem;font-size:0.8125rem;line-height:1.5;color:var(--vibeui-dialog-009-muted,oklch(0.5 0.014 265))}
[data-vibeui-dialog-009-window] label[data-part="field"]{display:flex;flex-direction:column;gap:0.375rem;font-size:0.8125rem;font-weight:500}
[data-vibeui-dialog-009-window] input[type="text"]{
width:100%;box-sizing:border-box;margin:0;height:2.375rem;padding:0 0.75rem;
border:1px solid var(--vibeui-dialog-009-border,oklch(0.89 0.006 265));
border-radius:0.5rem;background:oklch(1 0 0);color:inherit;font:inherit;font-size:0.9375rem;
}
[data-vibeui-dialog-009-window] input[type="text"]:focus{
outline:none;border-color:var(--vibeui-dialog-009-accent,oklch(0.55 0.2 262));
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-dialog-009-accent,oklch(0.55 0.2 262)) 20%,transparent);
}
[data-vibeui-dialog-009-window] [data-part="actions"]{display:flex;justify-content:flex-end;gap:0.5rem;margin-top:1.25rem}
[data-vibeui-dialog-009-window] [data-part="nav"],
[data-vibeui-dialog-009-window] [data-part="submit"]{
cursor:pointer;font:inherit;font-size:0.875rem;font-weight:600;
display:inline-flex;align-items:center;height:2.25rem;padding:0 1rem;
border-radius:0.5rem;border:1px solid transparent;
}
[data-vibeui-dialog-009-window] [data-part="nav"][data-kind="next"],
[data-vibeui-dialog-009-window] [data-part="submit"]{background:var(--vibeui-dialog-009-accent,oklch(0.55 0.2 262));color:oklch(1 0 0)}
[data-vibeui-dialog-009-window] [data-part="nav"][data-kind="back"]{
background:transparent;color:inherit;border-color:var(--vibeui-dialog-009-border,oklch(0.89 0.006 265));
}
/* Экран второго шага и его кнопки скрыты, пока выбран первый шаг. */
[data-vibeui-dialog-009-window]:has([data-part="state"][value="1"]:checked) [data-screen="2"],
[data-vibeui-dialog-009-window]:has([data-part="state"][value="2"]:checked) [data-screen="1"]{display:none}
[data-vibeui-dialog-009-window]:has([data-part="state"][value="1"]:checked) [data-part="dot"]:first-child,
[data-vibeui-dialog-009-window]:has([data-part="state"][value="2"]:checked) [data-part="dot"]{
background:var(--vibeui-dialog-009-accent,oklch(0.55 0.2 262));
}
[data-vibeui-dialog-009-window] :focus-visible{outline:2px solid var(--vibeui-dialog-009-accent,oklch(0.55 0.2 262));outline-offset:2px}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="dialog-009"] *{animation:none!important;transition:none!important}
[data-vibeui-dialog-009-window]{transition:none!important;opacity:1;transform:none}
}
`
const DEFAULT_STEPS: [Dialog009Step, Dialog009Step] = [
{
title: "Название проекта",
description: "Его увидят участники команды. Поменять можно в любой момент.",
fieldLabel: "Название",
placeholder: "Сайт студии",
},
{
title: "Адрес проекта",
description: "По нему сайт откроется до подключения своего домена.",
fieldLabel: "Адрес",
placeholder: "studio-polet",
},
]
/**
* Мастер из двух шагов без состояния: шаг держат скрытые radio.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Dialog009({
id = "vibeui-dialog-009",
trigger = "Создать проект",
steps = DEFAULT_STEPS,
nextLabel = "Далее",
backLabel = "Назад",
submitLabel = "Создать",
accent,
className,
style,
}: Dialog009Props) {
const palette = {
...(accent ? { "--vibeui-dialog-009-accent": accent } : null),
...style,
} as CSSProperties
return (
<>
<style href="vibeui-dialog-009" precedence="medium">
{STYLES}
</style>
<div data-vibeui-block="dialog-009" className={className} style={palette}>
<button data-part="trigger" type="button" popoverTarget={id}>
{trigger}
</button>
<div
id={id}
popover="auto"
data-vibeui-dialog-009-window=""
role="dialog"
aria-labelledby={`${id}-title-1`}
style={palette}
>
<input
data-part="state"
type="radio"
name={`${id}-step`}
id={`${id}-step-1`}
value="1"
defaultChecked
aria-hidden="true"
tabIndex={-1}
/>
<input
data-part="state"
type="radio"
name={`${id}-step`}
id={`${id}-step-2`}
value="2"
aria-hidden="true"
tabIndex={-1}
/>
<div data-part="dots" aria-hidden="true">
<span data-part="dot" />
<span data-part="dot" />
</div>
{steps.map((step, index) => (
<div key={step.title} data-screen={index + 1}>
<h2 data-part="title" id={`${id}-title-${index + 1}`}>
{step.title}
</h2>
<p data-part="description">{step.description}</p>
<label data-part="field">
{step.fieldLabel}
<input type="text" placeholder={step.placeholder} />
</label>
<div data-part="actions">
{index === 0 ? (
<label
data-part="nav"
data-kind="next"
htmlFor={`${id}-step-2`}
>
{nextLabel}
</label>
) : (
<>
<label
data-part="nav"
data-kind="back"
htmlFor={`${id}-step-1`}
>
{backLabel}
</label>
<button data-part="submit" type="button" popoverTarget={id}>
{submitLabel}
</button>
</>
)}
</div>
</div>
))}
</div>
</div>
</>
)
}