Dialog

Two-Step Dialog

A two-step wizard with no state: hidden radios hold the step and a :has rule swaps the screens — Next and Back are labels, not handlers.

  • dialog
  • wizard
  • steps
  • no-js

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/dialog-009?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 "dialog-009" (Two-Step Dialog) 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-009.json

Registry item: https://vibeui.ru/r/dialog-009.json
Installs to: components/vibeui/dialog-009.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 two-step wizard with no state: hidden radios hold the step and a :has rule swaps the screens — Next and Back are labels, not handlers.

A modal two-step wizard: progress dots on top, a screen with a field, Next and Back. The step lives in hidden radios and the swap is a :has rule, so there is no client code at all. Zero dependencies, one file, its own palette.

## 3. How to use it
import { Dialog009 } from "@/components/vibeui/dialog-009"

<Dialog009
  id="create-project"
  steps={[{ title: "Name", description: "…", fieldLabel: "Name" }, { title: "Address", description: "…", fieldLabel: "Address" }]}
/>

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-009-* palette — do not swap it for your theme tokens
- the hidden radios as the step store: they are what replaces state
- swapping screens through :has(...:checked) on the window rather than sibling display rules
- the progress dots: a wizard without an indicator feels endless
- aria-hidden and tabIndex={-1} on the utility radios — they have no business in the Tab order
- styling the window by attribute and the palette through its own style — the window sits in the top layer

## 6. You may change
- steps — exactly two: heading, explanation and field label
- the nextLabel, backLabel and submitLabel copy
- the accent through the accent prop — the dots and buttons
- the fields inside the steps: replace the inputs in your own copy
- trigger — the label of the button that opens the wizard

## 7. Rules
- There are exactly two steps. Three or more makes the :has trick unwieldy — that needs a stateful wizard.
- Field values are not carried between steps by the component: they stay in the DOM, read them on submit.
- The navigation buttons are <label>s, so they take no part in form submission. That is deliberate.
- 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-009.json
https://vibeui.ru/r/dialog-009.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-dialog-009-*. Шаг хранят два скрытых radio, а видимость экранов переключает :has([data-part="state"][value=…]:checked) на окне. Кнопки «Далее» и «Назад» — это <label for>, поэтому переход между шагами не требует ни состояния, ни обработчиков. Точки прогресса подсвечиваются тем же правилом.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

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>
    </>
  )
}