Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="st-i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
  <symbol id="st-i-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
  <symbol id="st-i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></symbol>
  <symbol id="st-i-logo" viewBox="0 0 24 24"><circle cx="9" cy="12" r="6.5" fill="#2563eb"/><circle cx="15" cy="12" r="6.5" fill="#60a5fa" fill-opacity=".85"/></symbol>
</svg>

<div><span class="lbl">Onboarding tour</span>
<div class="btns"><button class="st-btn is-primary" type="button" data-open="st-tour" aria-haspopup="dialog">Take the tour</button></div></div>

<div><span class="lbl">Multi-step form</span>
<div class="btns"><button class="st-btn" type="button" data-open="st-create" aria-haspopup="dialog">Create workspace</button></div></div>

<!-- Onboarding tour: three slides with Back / Next; Skip and "Get started" close it -->
<dialog class="st is-tour" id="st-tour" aria-labelledby="st-tour-title" data-steps>
  <form class="st-form" method="dialog">
    <div class="st-slides">
      <h2 class="st-chip" id="st-tour-title"><svg aria-hidden="true"><use href="#st-i-logo"/></svg>Welcome to Tandem</h2>
      <div class="st-slide" data-step>
        <div class="st-art is-sky" aria-hidden="true">
          <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="288" cy="24" r="44" fill="#fff" fill-opacity=".45"/><circle cx="20" cy="150" r="34" fill="#fff" fill-opacity=".4"/>
            <rect x="34" y="28" width="260" height="132" rx="14" fill="#1e3a8a" fill-opacity=".08"/>
            <rect x="30" y="20" width="260" height="132" rx="14" fill="#fff"/>
            <rect x="144" y="30" width="32" height="14" rx="7" fill="#2563eb"/>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="9" font-weight="600" fill="#64748b" text-anchor="middle"><text x="64" y="40">Mon</text><text x="112" y="40">Tue</text><text x="160" y="40" fill="#fff">Wed</text><text x="208" y="40">Thu</text><text x="256" y="40">Fri</text></g>
            <path d="M40 50h240" stroke="#e2e8f0"/><path d="M88 56v88M136 56v88M184 56v88M232 56v88" stroke="#f1f5f9"/>
            <rect x="44" y="58" width="40" height="30" rx="6" fill="#dbeafe"/><rect x="50" y="66" width="24" height="4" rx="2" fill="#2563eb"/><rect x="50" y="75" width="16" height="4" rx="2" fill="#93c5fd"/>
            <rect x="92" y="78" width="40" height="42" rx="6" fill="#e0e7ff"/><rect x="98" y="86" width="26" height="4" rx="2" fill="#6366f1"/><rect x="98" y="95" width="18" height="4" rx="2" fill="#a5b4fc"/>
            <rect x="140" y="58" width="40" height="24" rx="6" fill="#d1fae5"/><rect x="146" y="66" width="22" height="4" rx="2" fill="#059669"/>
            <rect x="140" y="98" width="40" height="34" rx="6" fill="#dbeafe"/><rect x="146" y="106" width="24" height="4" rx="2" fill="#2563eb"/><rect x="146" y="115" width="14" height="4" rx="2" fill="#93c5fd"/>
            <rect x="188" y="66" width="40" height="36" rx="6" fill="#fef3c7"/><rect x="194" y="74" width="24" height="4" rx="2" fill="#d97706"/><rect x="194" y="83" width="16" height="4" rx="2" fill="#fcd34d"/>
            <rect x="236" y="58" width="40" height="28" rx="6" fill="#fce7f3"/><rect x="242" y="66" width="22" height="4" rx="2" fill="#db2777"/>
            <rect x="236" y="96" width="40" height="30" rx="6" fill="#e0e7ff"/><rect x="242" y="104" width="24" height="4" rx="2" fill="#6366f1"/>
            <g stroke="#fff" stroke-width="3"><circle cx="232" cy="152" r="11" fill="#2563eb"/><circle cx="251" cy="152" r="11" fill="#059669"/><circle cx="270" cy="152" r="11" fill="#d97706"/></g>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle"><text x="232" y="155">MK</text><text x="251" y="155">JO</text><text x="270" y="155">AL</text></g>
          </svg>
        </div>
        <div class="st-copy"><h3 tabindex="-1">Plan the week together</h3><p>Drop tasks onto one shared weekly board, so everyone can see who is doing what and when.</p></div>
      </div>
      <div class="st-slide" data-step hidden>
        <div class="st-art is-indigo" aria-hidden="true">
          <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="294" cy="142" r="40" fill="#fff" fill-opacity=".4"/><circle cx="26" cy="20" r="30" fill="#fff" fill-opacity=".45"/>
            <rect x="24" y="18" width="124" height="138" rx="14" fill="#fff" fill-opacity=".72"/>
            <rect x="36" y="30" width="42" height="6" rx="3" fill="#a5b4fc"/>
            <rect x="36" y="46" width="100" height="34" rx="8" fill="#fff" stroke="#e0e7ff"/><rect x="46" y="56" width="52" height="4" rx="2" fill="#94a3b8"/><rect x="46" y="66" width="34" height="4" rx="2" fill="#cbd5e1"/>
            <rect x="36.5" y="88.5" width="99" height="33" rx="8" stroke="#a5b4fc" stroke-dasharray="4 3"/>
            <rect x="36" y="130" width="100" height="18" rx="8" fill="#fff" fill-opacity=".8"/>
            <rect x="172" y="18" width="124" height="138" rx="14" fill="#fff" fill-opacity=".72"/>
            <circle cx="191" cy="33" r="9" fill="#6366f1"/><text x="191" y="36" font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle">SR</text><rect x="206" y="30" width="46" height="6" rx="3" fill="#a5b4fc"/>
            <rect x="184" y="50" width="100" height="34" rx="8" fill="#fff" stroke="#e0e7ff"/><rect x="194" y="60" width="48" height="4" rx="2" fill="#94a3b8"/><rect x="194" y="70" width="30" height="4" rx="2" fill="#cbd5e1"/>
            <rect x="184.5" y="92.5" width="99" height="33" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-dasharray="4 3"/>
            <path d="M104 104c18-22 40-24 58-6" stroke="#6366f1" stroke-opacity=".55" stroke-width="2" stroke-dasharray="1 5" stroke-linecap="round"/>
            <g transform="rotate(-6 210 118)">
              <rect x="163" y="106" width="100" height="36" rx="8" fill="#4338ca" fill-opacity=".14"/>
              <rect x="160" y="100" width="100" height="36" rx="8" fill="#fff" stroke="#6366f1" stroke-width="1.5"/>
              <rect x="170" y="110" width="24" height="6" rx="3" fill="#6366f1"/><rect x="170" y="122" width="56" height="4" rx="2" fill="#94a3b8"/>
              <circle cx="244" cy="118" r="8" fill="#d97706"/>
            </g>
            <path d="M236 122v21l5.2-5 3.8 8.6 4-1.8-3.8-8.4h7.2z" fill="#0f172a" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
          </svg>
        </div>
        <div class="st-copy"><h3 tabindex="-1">Assign work in one drag</h3><p>Drag a card onto a teammate and Tandem assigns it, sets the due date and lets them know.</p></div>
      </div>
      <div class="st-slide" data-step hidden>
        <div class="st-art is-mint" aria-hidden="true">
          <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="30" cy="144" r="38" fill="#fff" fill-opacity=".4"/><circle cx="300" cy="150" r="30" fill="#fff" fill-opacity=".45"/>
            <rect x="24" y="26" width="192" height="132" rx="14" fill="#065f46" fill-opacity=".08"/>
            <rect x="20" y="18" width="192" height="132" rx="14" fill="#fff"/>
            <path d="M34 62h164M34 106h164" stroke="#f1f5f9"/>
            <circle cx="46" cy="40" r="11" fill="#2563eb"/><circle cx="46" cy="84" r="11" fill="#d97706"/><circle cx="46" cy="128" r="11" fill="#6366f1"/>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle"><text x="46" y="43">MK</text><text x="46" y="87">AL</text><text x="46" y="131">SR</text></g>
            <rect x="66" y="33" width="90" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="44" width="60" height="5" rx="2.5" fill="#cbd5e1"/>
            <rect x="66" y="77" width="76" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="88" width="96" height="5" rx="2.5" fill="#cbd5e1"/>
            <rect x="66" y="121" width="84" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="132" width="50" height="5" rx="2.5" fill="#cbd5e1"/>
            <circle cx="190" cy="40" r="9" fill="#10b981"/><path d="M186 40.5l2.8 2.8 5-5.6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            <rect x="170" y="78" width="30" height="12" rx="6" fill="#dbeafe"/><rect x="177" y="82.5" width="16" height="3" rx="1.5" fill="#2563eb"/>
            <circle cx="190" cy="128" r="9" fill="#10b981"/><path d="M186 128.5l2.8 2.8 5-5.6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            <path d="M222 50l-9 6 12 1z" fill="#2563eb"/>
            <rect x="220" y="30" width="88" height="32" rx="12" fill="#2563eb"/>
            <text x="264" y="50" font-family="ui-sans-serif,system-ui,sans-serif" font-size="10" font-weight="700" fill="#fff" text-anchor="middle">All caught up</text>
            <circle cx="262" cy="112" r="22" fill="#fff"/>
            <path d="M255 117h14l-1.6-2.4v-5.6a5.4 5.4 0 0 0-10.8 0v5.6z" stroke="#2563eb" stroke-width="2" stroke-linejoin="round"/><path d="M259.6 120.5a2.5 2.5 0 0 0 4.8 0" stroke="#2563eb" stroke-width="2" stroke-linecap="round"/>
            <circle cx="271" cy="100" r="4.5" fill="#ef4444" stroke="#fff" stroke-width="2"/>
          </svg>
        </div>
        <div class="st-copy"><h3 tabindex="-1">Stay in sync without meetings</h3><p>Short written updates and a daily digest replace the status call, so calendars stay free for real work.</p></div>
      </div>
    </div>
    <div class="st-foot">
      <div class="st-progress">
        <span class="st-dots" aria-hidden="true"><span data-marker data-state="current"></span><span data-marker data-state="todo"></span><span data-marker data-state="todo"></span></span>
        <p class="st-status" data-step-status aria-live="polite">Step 1 of 3</p>
      </div>
      <button class="st-btn is-text" value="skip" formnovalidate>Skip</button>
      <button class="st-btn" type="button" data-back hidden>Back</button>
      <button class="st-btn is-primary" type="button" data-next autofocus>Next<svg aria-hidden="true"><use href="#st-i-arrow"/></svg></button>
      <button class="st-btn is-primary" value="done" data-finish hidden>Get started</button>
    </div>
  </form>
</dialog>

<!-- Multi-step form: each step is checked with reportValidity() before moving on -->
<dialog class="st is-form" id="st-create" aria-labelledby="st-create-title" data-steps>
  <form class="st-head" method="dialog">
    <div class="st-head-row">
      <div class="st-titles"><h2 class="st-title" id="st-create-title">Create a workspace</h2><p class="st-sub">Three quick steps and your team is ready to plan.</p></div>
      <button class="st-x" value="close" aria-label="Close"><svg aria-hidden="true"><use href="#st-i-x"/></svg></button>
    </div>
    <ol class="st-stepper" aria-label="Setup steps">
      <li data-marker data-state="current" aria-current="step"><span class="st-num" aria-hidden="true"><b>1</b><svg><use href="#st-i-check"/></svg></span><span>Details</span><span class="st-sr" data-done hidden> (completed)</span></li>
      <li data-marker data-state="todo"><span class="st-num" aria-hidden="true"><b>2</b><svg><use href="#st-i-check"/></svg></span><span>Invite</span><span class="st-sr" data-done hidden> (completed)</span></li>
      <li data-marker data-state="todo"><span class="st-num" aria-hidden="true"><b>3</b><svg><use href="#st-i-check"/></svg></span><span>Plan</span><span class="st-sr" data-done hidden> (completed)</span></li>
    </ol>
  </form>
  <form class="st-form" method="dialog">
    <div class="st-body">
      <div class="st-pane" data-step>
        <h3 tabindex="-1">Name your workspace</h3>
        <p class="st-pane-sub">This is what your team sees when they sign in.</p>
        <div class="st-field">
          <label class="st-label" for="st-name">Workspace name</label>
          <input class="st-input" id="st-name" name="workspace" required autocomplete="organization" placeholder="e.g. Northwind Design" autofocus>
        </div>
        <div class="st-field">
          <label class="st-label" for="st-url">Workspace URL</label>
          <div class="st-affix"><span id="st-url-prefix">tandem.app/</span><input class="st-input" id="st-url" name="url" required pattern="[a-z0-9\-]{3,30}" autocomplete="off" spellcheck="false" placeholder="northwind" aria-describedby="st-url-prefix st-url-hint"></div>
          <p class="st-hint" id="st-url-hint">Lowercase letters, numbers and hyphens, 3 to 30 characters.</p>
        </div>
      </div>
      <div class="st-pane" data-step hidden>
        <h3 tabindex="-1">Invite your team</h3>
        <p class="st-pane-sub">Each person gets an email with a link to join.</p>
        <fieldset class="st-fieldset">
          <legend class="st-label">Email addresses</legend>
          <div class="st-emails">
            <label class="st-sr" for="st-email-1">Teammate 1 email</label><input class="st-input" type="email" id="st-email-1" name="invite" placeholder="[email protected]" autocomplete="off">
            <label class="st-sr" for="st-email-2">Teammate 2 email</label><input class="st-input" type="email" id="st-email-2" name="invite" placeholder="[email protected]" autocomplete="off">
            <label class="st-sr" for="st-email-3">Teammate 3 email</label><input class="st-input" type="email" id="st-email-3" name="invite" placeholder="[email protected]" autocomplete="off">
          </div>
        </fieldset>
        <div class="st-field">
          <label class="st-label" for="st-role">Invite as</label>
          <select class="st-input st-select" id="st-role" name="role"><option>Member</option><option>Admin</option></select>
        </div>
        <p class="st-skip"><button class="st-link" type="button" data-skip>Skip for now</button><span>You can invite people later from Settings.</span></p>
      </div>
      <div class="st-pane" data-step hidden>
        <h3 tabindex="-1">Choose a plan</h3>
        <p class="st-pane-sub">Change or cancel at any time. Prices exclude VAT.</p>
        <fieldset class="st-plans">
          <legend class="st-sr">Plan</legend>
          <label class="st-plan"><input type="radio" name="plan" value="free"><span class="st-plan-name">Free</span><span class="st-price"><b>£0</b>for up to 3 people</span><span class="st-plan-note">2 boards, 7-day history</span></label>
          <label class="st-plan"><input type="radio" name="plan" value="team" checked><span class="st-plan-name">Team <span class="st-badge">Popular</span></span><span class="st-price"><b>£8</b>per user / month</span><span class="st-plan-note">Unlimited boards and guests</span></label>
          <label class="st-plan"><input type="radio" name="plan" value="business"><span class="st-plan-name">Business</span><span class="st-price"><b>£16</b>per user / month</span><span class="st-plan-note">SSO, audit log, priority support</span></label>
        </fieldset>
      </div>
    </div>
    <div class="st-foot">
      <p class="st-status" data-step-status aria-live="polite">Step 1 of 3</p>
      <button class="st-btn" type="button" data-back hidden>Back</button>
      <button class="st-btn is-primary" type="button" data-next>Continue<svg aria-hidden="true"><use href="#st-i-arrow"/></svg></button>
      <button class="st-btn is-primary" value="done" data-finish hidden>Create workspace</button>
    </div>
  </form>
</dialog>
CSS
/* Multi-step modals on the native <dialog> element. Each step is a panel hidden with the hidden attribute;
   the small script below moves between panels, validates each step and keeps the progress markers in sync. */
.st-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.st-btn{--st-accent:#2563eb;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:42px;padding:9px 16px;border:1px solid #cbd5e1;border-radius:10px;
  background:#fff;color:#0f172a;font-family:inherit;font-size:.9rem;font-weight:600;line-height:1.2;text-decoration:none;cursor:pointer;transition:background-color .15s,border-color .15s,box-shadow .15s}
.st-btn:hover{background:#f1f5f9;border-color:#94a3b8}
.st-btn:focus-visible{outline:2px solid var(--st-accent);outline-offset:2px}
.st-btn.is-primary{background:var(--st-accent);border-color:var(--st-accent);color:#fff;box-shadow:0 6px 16px -8px rgba(37,99,235,.7)}
.st-btn.is-primary:hover{background:#1d4ed8;border-color:#1d4ed8}
.st-btn.is-text{border-color:transparent;background:transparent;color:#475569}
.st-btn.is-text:hover{background:#f1f5f9;color:#0f172a}
.st-btn svg{width:18px;height:18px;flex:none}

.st{--st-accent:#2563eb;--st-ink:#0f172a;--st-muted:#475569;--st-line:#e2e8f0;--st-soft:#f8fafc;--st-field:#7c8aa0;--st-radius:18px;--st-w:560px;
  width:min(var(--st-w),calc(100% - 32px));max-width:none;max-height:calc(100dvh - 56px);margin:auto;padding:0;border:0;border-radius:var(--st-radius);
  background:#fff;color:var(--st-ink);font-size:.95rem;line-height:1.55;overflow:hidden;overscroll-behavior:contain;
  box-shadow:0 0 0 1px rgba(15,23,42,.06),0 28px 64px -18px rgba(15,23,42,.45)}
.st[open]{display:flex;flex-direction:column;animation:st-in .22s cubic-bezier(.2,.8,.2,1)}
.st::backdrop{background:rgba(15,23,42,.55);backdrop-filter:blur(2px)}
.st[open]::backdrop{animation:st-fade .22s ease-out}
@keyframes st-in{from{opacity:0;transform:translateY(10px) scale(.97)}}
@keyframes st-fade{from{opacity:0}}
/* --st-dir is 1 going forward and -1 going back (set by the script) */
@keyframes st-slide{from{opacity:0;transform:translateX(calc(var(--st-dir,1) * 18px))}}
html:has(.st:modal){overflow:hidden} /* stop the page scrolling behind an open modal */
.st [hidden]{display:none!important}

.st-form{display:flex;flex-direction:column;flex:1;min-height:0;margin:0}
.st-foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:10px;padding:16px 24px;border-top:1px solid var(--st-line);background:var(--st-soft)}
.st-status{margin:0 auto 0 0;color:var(--st-muted);font-size:.85rem;font-weight:600}

/* Onboarding tour */
.st-slides{position:relative;flex:1 1 auto;min-height:0;overflow-y:auto}
.st-chip{position:absolute;top:16px;left:16px;z-index:1;display:inline-flex;align-items:center;gap:7px;margin:0;padding:5px 12px 5px 6px;border-radius:999px;
  background:rgba(255,255,255,.9);color:#1e3a8a;font-size:.78rem;font-weight:700;letter-spacing:.01em;line-height:1.4;box-shadow:0 1px 2px rgba(15,23,42,.08)}
.st-chip svg{width:20px;height:20px}
.st-slide,.st-pane{animation:st-slide .32s cubic-bezier(.2,.8,.2,1)}
.st-art{display:grid;place-items:center;height:210px;padding:34px 20px 10px}
.st-art svg{width:100%;height:100%;max-width:340px}
.st-art.is-sky{background:linear-gradient(135deg,#e0f2fe,#dbeafe 55%,#e0e7ff)}
.st-art.is-indigo{background:linear-gradient(135deg,#e0e7ff,#ede9fe 60%,#fae8ff)}
.st-art.is-mint{background:linear-gradient(135deg,#d1fae5,#ccfbf1 55%,#e0f2fe)}
.st-copy{padding:22px 28px 24px}
.st-copy h3{margin:0;border-radius:4px;color:var(--st-ink);font-size:1.3rem;font-weight:700;letter-spacing:-.015em;line-height:1.3}
.st-copy p{margin:6px 0 0;color:var(--st-muted)}
.st-copy h3:focus-visible,.st-pane h3:focus-visible{outline:2px solid var(--st-accent);outline-offset:4px}
.st-progress{display:flex;align-items:center;gap:12px;margin-right:auto}
.st-progress .st-status{margin:0}
.st-dots{display:flex;gap:6px}
.st-dots span{width:8px;height:8px;border-radius:999px;background:#cbd5e1;transition:width .25s,background-color .25s}
.st-dots [data-state=done]{background:#93c5fd}
.st-dots [data-state=current]{width:22px;background:var(--st-accent)}

/* Multi-step form: header with a stepper */
.st.is-form{--st-w:600px}
.st-head{flex:none;margin:0;padding:22px 24px 0;border-bottom:1px solid var(--st-line)}
.st-head-row{display:flex;align-items:flex-start;gap:14px}
.st-titles{flex:1;min-width:0}
.st-title{margin:0;font-size:1.15rem;font-weight:700;letter-spacing:-.01em;line-height:1.35}
.st-sub{margin:3px 0 0;color:var(--st-muted);font-size:.85rem}
.st-x{flex:none;display:grid;place-items:center;width:36px;height:36px;margin:-4px -8px 0 0;padding:0;border:0;border-radius:10px;background:transparent;color:#475569;cursor:pointer;
  transition:background-color .15s,color .15s}
.st-x:hover{background:#f1f5f9;color:var(--st-ink)}
.st-x:focus-visible{outline:2px solid var(--st-accent);outline-offset:1px}
.st-x svg{width:20px;height:20px}
.st-stepper{display:flex;align-items:center;margin:18px 0 0;padding:0 0 18px;list-style:none}
.st-stepper li{display:flex;flex:1;align-items:center;gap:10px;min-width:0;color:var(--st-muted);font-size:.875rem;font-weight:600}
.st-stepper li:last-child{flex:none}
.st-stepper li:not(:last-child)::after{content:"";flex:1;min-width:12px;height:2px;margin-right:10px;border-radius:2px;background:var(--st-line)}
.st-num{display:grid;place-items:center;flex:none;width:28px;height:28px;border:2px solid #cbd5e1;border-radius:50%;background:#fff;color:var(--st-muted);font-size:.8rem;font-weight:700}
.st-num svg{display:none;width:16px;height:16px}
.st-stepper [data-state=current],.st-stepper [data-state=done]{color:var(--st-ink)}
.st-stepper [data-state=current] .st-num{border-color:var(--st-accent);background:var(--st-accent);color:#fff;box-shadow:0 0 0 4px #dbeafe}
.st-stepper [data-state=done] .st-num{border-color:#dbeafe;background:#dbeafe;color:#1d4ed8}
.st-stepper [data-state=done] .st-num b{display:none}
.st-stepper [data-state=done] .st-num svg{display:block}
.st-stepper li[data-state=done]::after{background:var(--st-accent)}

/* Step panels and fields */
.st-body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:22px 24px 24px}
.st-pane h3{margin:0;border-radius:4px;color:var(--st-ink);font-size:1.05rem;font-weight:700;line-height:1.4}
.st-pane-sub{margin:3px 0 18px;color:var(--st-muted);font-size:.9rem}
.st-field{margin:0 0 16px}
.st-field:last-child{margin-bottom:0}
.st-label{display:block;margin:0 0 6px;padding:0;color:var(--st-ink);font-size:.875rem;font-weight:600;line-height:1.4}
.st-input{display:block;width:100%;min-height:44px;margin:0;padding:10px 12px;border:1px solid var(--st-field);border-radius:10px;background:#fff;color:var(--st-ink);
  font:inherit;font-size:.95rem;line-height:1.4;transition:border-color .15s}
.st-input::placeholder{color:#64748b;opacity:1}
.st-input:hover{border-color:#64748b}
.st-input:focus-visible{border-color:var(--st-accent);outline:2px solid var(--st-accent);outline-offset:1px}
.st-affix{display:flex}
.st-affix span{display:flex;align-items:center;padding:0 12px;border:1px solid var(--st-field);border-right:0;border-radius:10px 0 0 10px;background:#f1f5f9;color:var(--st-muted);font-size:.9rem}
.st-affix .st-input{flex:1;min-width:0;border-radius:0 10px 10px 0}
.st-hint{margin:6px 0 0;color:var(--st-muted);font-size:.8rem}
.st-fieldset{min-width:0;margin:0 0 16px;padding:0;border:0}
.st-emails{display:grid;gap:8px}
.st-select{appearance:none;padding-right:38px;cursor:pointer;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='m2 5 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 12px center/16px}
.st-skip{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;margin:18px 0 0;padding-top:16px;border-top:1px dashed var(--st-line);color:var(--st-muted);font-size:.85rem}
.st-link{margin-left:-6px;padding:4px 6px;border:0;border-radius:6px;background:none;color:#1d4ed8;font:inherit;font-weight:600;text-decoration:underline;text-underline-offset:3px;cursor:pointer}
.st-link:hover{color:#1e40af}
.st-link:focus-visible{outline:2px solid var(--st-accent);outline-offset:1px}

/* Plan choice: radio cards */
.st-plans{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;min-width:0;margin:0;padding:0;border:0}
.st-plan{position:relative;display:flex;flex-direction:column;gap:2px;padding:14px;border:1px solid #cbd5e1;border-radius:14px;background:#fff;cursor:pointer;
  transition:border-color .15s,background-color .15s,box-shadow .15s}
.st-plan:hover{border-color:#94a3b8}
.st-plan:has(:checked){border-color:var(--st-accent);background:#eff6ff;box-shadow:inset 0 0 0 1px var(--st-accent)}
.st-plan input{position:absolute;top:14px;right:14px;width:18px;height:18px;margin:0;border:1.5px solid var(--st-field);border-radius:50%;background:#fff;appearance:none;cursor:pointer}
.st-plan input:checked{border:5px solid var(--st-accent)}
.st-plan input:focus-visible{outline:2px solid var(--st-accent);outline-offset:2px}
.st-plan-name{display:flex;align-items:center;gap:6px;color:var(--st-ink);font-size:.95rem;font-weight:700}
.st-badge{padding:1px 7px;border-radius:999px;background:#dbeafe;color:#1e40af;font-size:.68rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase}
.st-price{margin-top:6px;color:var(--st-muted);font-size:.8rem}
.st-price b{display:block;color:var(--st-ink);font-size:1.5rem;font-weight:700;letter-spacing:-.02em;line-height:1.2}
.st-plan-note{margin-top:8px;padding-top:8px;border-top:1px solid var(--st-line);color:var(--st-muted);font-size:.8rem}

@media (max-width:600px){
  .st{max-height:calc(100dvh - 32px)}
  .st-head,.st-body,.st-foot{padding-left:18px;padding-right:18px}
  .st-copy{padding-left:18px;padding-right:18px}
  .st-foot .st-btn{flex:1 1 auto}
  .st-foot>.st-status,.st-progress{flex-basis:100%}
  .st-plans{grid-template-columns:1fr}
  .st-plan{display:grid;grid-template-columns:1fr auto;gap:2px 12px;padding-right:44px}
  .st-plan input{top:50%;translate:0 -50%}
  .st-price{grid-column:2;grid-row:1 / 3;align-self:center;margin:0;text-align:right}
  .st-plan-note{margin:0;padding:0;border:0}
}
@media (prefers-reduced-motion:reduce){
  .st[open],.st[open]::backdrop,.st-slide,.st-pane{animation:none}
  .st-btn,.st-x,.st-input,.st-plan,.st-dots span{transition:none}
}
JavaScript
// Open a dialog from any button with data-open="dialog-id". showModal() traps focus, closes on Esc
// and makes the page behind inert. Buttons inside <form method="dialog"> close it with no script.
document.querySelectorAll('[data-open]').forEach(function (btn) {
  var dialog = document.getElementById(btn.getAttribute('data-open'));
  if (!dialog) return;
  btn.addEventListener('click', function () {
    dialog.showModal();
    dialog.addEventListener('close', function () { btn.focus(); }, { once: true }); // focus back to the trigger
  });
  // A click on the backdrop lands on the <dialog> itself (its form fills the box), so close it
  dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
});

// Step-by-step dialogs. Inside any element with data-steps:
//   [data-step]          one panel per step, hidden with the hidden attribute
//   [data-marker]        progress dots / stepper items: get data-state="done|current|todo" and aria-current="step"
//   [data-done]          visually hidden "completed" text inside a marker, shown once that step is done
//   [data-step-status]   polite live region that reads "Step 2 of 3"
//   [data-back] [data-next] [data-skip]   buttons that move between steps; [data-finish] shows on the last step only
document.querySelectorAll('[data-steps]').forEach(function (root) {
  var panels = root.querySelectorAll('[data-step]'), last = panels.length - 1, step = 0;
  var form = panels[0].closest('form');
  function show(sel, on) { root.querySelectorAll(sel).forEach(function (el) { el.hidden = !on; }); }

  function go(n, moveFocus) {
    root.style.setProperty('--st-dir', n < step ? -1 : 1); // slide direction for the CSS animation
    step = n;
    panels.forEach(function (p, i) { p.hidden = i !== n; });
    root.querySelectorAll('[data-marker]').forEach(function (m, i) {
      m.setAttribute('data-state', i < n ? 'done' : i === n ? 'current' : 'todo');
      if (i === n) m.setAttribute('aria-current', 'step'); else m.removeAttribute('aria-current');
      var done = m.querySelector('[data-done]'); if (done) done.hidden = i >= n;
    });
    show('[data-back]', n > 0); show('[data-next]', n < last); show('[data-finish]', n === last);
    var status = root.querySelector('[data-step-status]');
    if (status) status.textContent = 'Step ' + (n + 1) + ' of ' + panels.length;
    if (moveFocus) panels[n].querySelector('[tabindex="-1"]').focus(); // land on the new step's heading
  }

  function next(skip) {
    var fields = panels[step].querySelectorAll('input, select, textarea');
    if (skip) panels[step].querySelectorAll('input').forEach(function (f) { f.value = f.defaultValue; }); // "Skip for now" empties the step
    else if (![].every.call(fields, function (f) { return f.reportValidity(); })) return; // browser message on the first invalid field
    go(step + 1, true);
  }

  root.addEventListener('click', function (e) {
    var b = e.target.closest('[data-next], [data-back], [data-skip]');
    if (b) b.hasAttribute('data-back') ? go(step - 1, true) : next(b.hasAttribute('data-skip'));
  });
  if (form) form.addEventListener('submit', function (e) {
    if (e.submitter && e.submitter.formNoValidate) return; // Skip: <form method="dialog"> closes the dialog
    if (step < last) { e.preventDefault(); next(); }       // Enter in a field goes to the next step, not out
  });
  root.addEventListener('close', function () {
    if (root.returnValue === 'done' && form) form.reset(); // finished: clear the fields for next time
    root.returnValue = ''; step = 0; go(0);                // always reopen on step 1
  });
  go(0);
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="st-i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
  <symbol id="st-i-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
  <symbol id="st-i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></symbol>
  <symbol id="st-i-logo" viewBox="0 0 24 24"><circle cx="9" cy="12" r="6.5" fill="#2563eb"/><circle cx="15" cy="12" r="6.5" fill="#60a5fa" fill-opacity=".85"/></symbol>
</svg>

<div><span class="lbl">Onboarding tour</span>
<div class="d-flex flex-wrap gap-2"><button class="btn st-btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#st-tour">Take the tour</button></div></div>

<div><span class="lbl">Multi-step form</span>
<div class="d-flex flex-wrap gap-2"><button class="btn st-btn" type="button" data-bs-toggle="modal" data-bs-target="#st-create">Create workspace</button></div></div>

<!-- Onboarding tour: three slides with Back / Next; Skip and "Get started" close it -->
<div class="modal fade st st-tour" id="st-tour" tabindex="-1" aria-labelledby="st-tour-title" aria-hidden="true" data-steps>
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-body p-0">
        <h2 class="st-chip" id="st-tour-title"><svg aria-hidden="true"><use href="#st-i-logo"/></svg>Welcome to Tandem</h2>
        <div class="st-slide" data-step>
          <div class="st-art is-sky" aria-hidden="true">
            <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="288" cy="24" r="44" fill="#fff" fill-opacity=".45"/><circle cx="20" cy="150" r="34" fill="#fff" fill-opacity=".4"/>
            <rect x="34" y="28" width="260" height="132" rx="14" fill="#1e3a8a" fill-opacity=".08"/>
            <rect x="30" y="20" width="260" height="132" rx="14" fill="#fff"/>
            <rect x="144" y="30" width="32" height="14" rx="7" fill="#2563eb"/>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="9" font-weight="600" fill="#64748b" text-anchor="middle"><text x="64" y="40">Mon</text><text x="112" y="40">Tue</text><text x="160" y="40" fill="#fff">Wed</text><text x="208" y="40">Thu</text><text x="256" y="40">Fri</text></g>
            <path d="M40 50h240" stroke="#e2e8f0"/><path d="M88 56v88M136 56v88M184 56v88M232 56v88" stroke="#f1f5f9"/>
            <rect x="44" y="58" width="40" height="30" rx="6" fill="#dbeafe"/><rect x="50" y="66" width="24" height="4" rx="2" fill="#2563eb"/><rect x="50" y="75" width="16" height="4" rx="2" fill="#93c5fd"/>
            <rect x="92" y="78" width="40" height="42" rx="6" fill="#e0e7ff"/><rect x="98" y="86" width="26" height="4" rx="2" fill="#6366f1"/><rect x="98" y="95" width="18" height="4" rx="2" fill="#a5b4fc"/>
            <rect x="140" y="58" width="40" height="24" rx="6" fill="#d1fae5"/><rect x="146" y="66" width="22" height="4" rx="2" fill="#059669"/>
            <rect x="140" y="98" width="40" height="34" rx="6" fill="#dbeafe"/><rect x="146" y="106" width="24" height="4" rx="2" fill="#2563eb"/><rect x="146" y="115" width="14" height="4" rx="2" fill="#93c5fd"/>
            <rect x="188" y="66" width="40" height="36" rx="6" fill="#fef3c7"/><rect x="194" y="74" width="24" height="4" rx="2" fill="#d97706"/><rect x="194" y="83" width="16" height="4" rx="2" fill="#fcd34d"/>
            <rect x="236" y="58" width="40" height="28" rx="6" fill="#fce7f3"/><rect x="242" y="66" width="22" height="4" rx="2" fill="#db2777"/>
            <rect x="236" y="96" width="40" height="30" rx="6" fill="#e0e7ff"/><rect x="242" y="104" width="24" height="4" rx="2" fill="#6366f1"/>
            <g stroke="#fff" stroke-width="3"><circle cx="232" cy="152" r="11" fill="#2563eb"/><circle cx="251" cy="152" r="11" fill="#059669"/><circle cx="270" cy="152" r="11" fill="#d97706"/></g>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle"><text x="232" y="155">MK</text><text x="251" y="155">JO</text><text x="270" y="155">AL</text></g>
          </svg>
          </div>
          <div class="st-copy"><h3 tabindex="-1">Plan the week together</h3><p>Drop tasks onto one shared weekly board, so everyone can see who is doing what and when.</p></div>
        </div>
        <div class="st-slide" data-step hidden>
          <div class="st-art is-indigo" aria-hidden="true">
            <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="294" cy="142" r="40" fill="#fff" fill-opacity=".4"/><circle cx="26" cy="20" r="30" fill="#fff" fill-opacity=".45"/>
            <rect x="24" y="18" width="124" height="138" rx="14" fill="#fff" fill-opacity=".72"/>
            <rect x="36" y="30" width="42" height="6" rx="3" fill="#a5b4fc"/>
            <rect x="36" y="46" width="100" height="34" rx="8" fill="#fff" stroke="#e0e7ff"/><rect x="46" y="56" width="52" height="4" rx="2" fill="#94a3b8"/><rect x="46" y="66" width="34" height="4" rx="2" fill="#cbd5e1"/>
            <rect x="36.5" y="88.5" width="99" height="33" rx="8" stroke="#a5b4fc" stroke-dasharray="4 3"/>
            <rect x="36" y="130" width="100" height="18" rx="8" fill="#fff" fill-opacity=".8"/>
            <rect x="172" y="18" width="124" height="138" rx="14" fill="#fff" fill-opacity=".72"/>
            <circle cx="191" cy="33" r="9" fill="#6366f1"/><text x="191" y="36" font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle">SR</text><rect x="206" y="30" width="46" height="6" rx="3" fill="#a5b4fc"/>
            <rect x="184" y="50" width="100" height="34" rx="8" fill="#fff" stroke="#e0e7ff"/><rect x="194" y="60" width="48" height="4" rx="2" fill="#94a3b8"/><rect x="194" y="70" width="30" height="4" rx="2" fill="#cbd5e1"/>
            <rect x="184.5" y="92.5" width="99" height="33" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-dasharray="4 3"/>
            <path d="M104 104c18-22 40-24 58-6" stroke="#6366f1" stroke-opacity=".55" stroke-width="2" stroke-dasharray="1 5" stroke-linecap="round"/>
            <g transform="rotate(-6 210 118)">
              <rect x="163" y="106" width="100" height="36" rx="8" fill="#4338ca" fill-opacity=".14"/>
              <rect x="160" y="100" width="100" height="36" rx="8" fill="#fff" stroke="#6366f1" stroke-width="1.5"/>
              <rect x="170" y="110" width="24" height="6" rx="3" fill="#6366f1"/><rect x="170" y="122" width="56" height="4" rx="2" fill="#94a3b8"/>
              <circle cx="244" cy="118" r="8" fill="#d97706"/>
            </g>
            <path d="M236 122v21l5.2-5 3.8 8.6 4-1.8-3.8-8.4h7.2z" fill="#0f172a" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
          </svg>
          </div>
          <div class="st-copy"><h3 tabindex="-1">Assign work in one drag</h3><p>Drag a card onto a teammate and Tandem assigns it, sets the due date and lets them know.</p></div>
        </div>
        <div class="st-slide" data-step hidden>
          <div class="st-art is-mint" aria-hidden="true">
            <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="30" cy="144" r="38" fill="#fff" fill-opacity=".4"/><circle cx="300" cy="150" r="30" fill="#fff" fill-opacity=".45"/>
            <rect x="24" y="26" width="192" height="132" rx="14" fill="#065f46" fill-opacity=".08"/>
            <rect x="20" y="18" width="192" height="132" rx="14" fill="#fff"/>
            <path d="M34 62h164M34 106h164" stroke="#f1f5f9"/>
            <circle cx="46" cy="40" r="11" fill="#2563eb"/><circle cx="46" cy="84" r="11" fill="#d97706"/><circle cx="46" cy="128" r="11" fill="#6366f1"/>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle"><text x="46" y="43">MK</text><text x="46" y="87">AL</text><text x="46" y="131">SR</text></g>
            <rect x="66" y="33" width="90" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="44" width="60" height="5" rx="2.5" fill="#cbd5e1"/>
            <rect x="66" y="77" width="76" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="88" width="96" height="5" rx="2.5" fill="#cbd5e1"/>
            <rect x="66" y="121" width="84" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="132" width="50" height="5" rx="2.5" fill="#cbd5e1"/>
            <circle cx="190" cy="40" r="9" fill="#10b981"/><path d="M186 40.5l2.8 2.8 5-5.6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            <rect x="170" y="78" width="30" height="12" rx="6" fill="#dbeafe"/><rect x="177" y="82.5" width="16" height="3" rx="1.5" fill="#2563eb"/>
            <circle cx="190" cy="128" r="9" fill="#10b981"/><path d="M186 128.5l2.8 2.8 5-5.6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            <path d="M222 50l-9 6 12 1z" fill="#2563eb"/>
            <rect x="220" y="30" width="88" height="32" rx="12" fill="#2563eb"/>
            <text x="264" y="50" font-family="ui-sans-serif,system-ui,sans-serif" font-size="10" font-weight="700" fill="#fff" text-anchor="middle">All caught up</text>
            <circle cx="262" cy="112" r="22" fill="#fff"/>
            <path d="M255 117h14l-1.6-2.4v-5.6a5.4 5.4 0 0 0-10.8 0v5.6z" stroke="#2563eb" stroke-width="2" stroke-linejoin="round"/><path d="M259.6 120.5a2.5 2.5 0 0 0 4.8 0" stroke="#2563eb" stroke-width="2" stroke-linecap="round"/>
            <circle cx="271" cy="100" r="4.5" fill="#ef4444" stroke="#fff" stroke-width="2"/>
          </svg>
          </div>
          <div class="st-copy"><h3 tabindex="-1">Stay in sync without meetings</h3><p>Short written updates and a daily digest replace the status call, so calendars stay free for real work.</p></div>
        </div>
      </div>
      <div class="modal-footer">
        <div class="st-progress">
          <span class="st-dots" aria-hidden="true"><span data-marker data-state="current"></span><span data-marker data-state="todo"></span><span data-marker data-state="todo"></span></span>
          <p class="st-status" data-step-status aria-live="polite">Step 1 of 3</p>
        </div>
        <button type="button" class="btn st-btn st-btn-text" data-bs-dismiss="modal">Skip</button>
        <button type="button" class="btn st-btn" data-back hidden>Back</button>
        <button type="button" class="btn st-btn btn-primary" data-next autofocus>Next<svg aria-hidden="true"><use href="#st-i-arrow"/></svg></button>
        <button type="button" class="btn st-btn btn-primary" data-finish data-bs-dismiss="modal" hidden>Get started</button>
      </div>
    </div>
  </div>
</div>

<!-- Multi-step form: each step is checked with reportValidity() before moving on -->
<div class="modal fade st st-create" id="st-create" tabindex="-1" aria-labelledby="st-create-title" aria-hidden="true" data-steps>
  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
    <div class="modal-content">
      <div class="modal-header">
        <div class="flex-grow-1"><h2 class="modal-title" id="st-create-title">Create a workspace</h2><p class="st-sub">Three quick steps and your team is ready to plan.</p></div>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        <ol class="st-stepper" aria-label="Setup steps">
          <li data-marker data-state="current" aria-current="step"><span class="st-num" aria-hidden="true"><b>1</b><svg><use href="#st-i-check"/></svg></span><span>Details</span><span class="visually-hidden" data-done hidden> (completed)</span></li>
          <li data-marker data-state="todo"><span class="st-num" aria-hidden="true"><b>2</b><svg><use href="#st-i-check"/></svg></span><span>Invite</span><span class="visually-hidden" data-done hidden> (completed)</span></li>
          <li data-marker data-state="todo"><span class="st-num" aria-hidden="true"><b>3</b><svg><use href="#st-i-check"/></svg></span><span>Plan</span><span class="visually-hidden" data-done hidden> (completed)</span></li>
        </ol>
      </div>
      <form class="st-main">
        <div class="modal-body">
          <div class="st-pane" data-step>
            <h3 tabindex="-1">Name your workspace</h3>
            <p class="st-pane-sub">This is what your team sees when they sign in.</p>
            <div class="mb-3">
              <label class="form-label" for="st-name">Workspace name</label>
              <input class="form-control" id="st-name" name="workspace" required autocomplete="organization" placeholder="e.g. Northwind Design" autofocus>
            </div>
            <div>
              <label class="form-label" for="st-url">Workspace URL</label>
              <div class="input-group"><span class="input-group-text" id="st-url-prefix">tandem.app/</span><input class="form-control" id="st-url" name="url" required pattern="[a-z0-9\-]{3,30}" autocomplete="off" spellcheck="false" placeholder="northwind" aria-describedby="st-url-prefix st-url-hint"></div>
              <p class="form-text mb-0" id="st-url-hint">Lowercase letters, numbers and hyphens, 3 to 30 characters.</p>
            </div>
          </div>
          <div class="st-pane" data-step hidden>
            <h3 tabindex="-1">Invite your team</h3>
            <p class="st-pane-sub">Each person gets an email with a link to join.</p>
            <fieldset class="mb-3">
              <legend class="form-label">Email addresses</legend>
              <div class="d-grid gap-2">
                <label class="visually-hidden" for="st-email-1">Teammate 1 email</label><input class="form-control" type="email" id="st-email-1" name="invite" placeholder="[email protected]" autocomplete="off">
                <label class="visually-hidden" for="st-email-2">Teammate 2 email</label><input class="form-control" type="email" id="st-email-2" name="invite" placeholder="[email protected]" autocomplete="off">
                <label class="visually-hidden" for="st-email-3">Teammate 3 email</label><input class="form-control" type="email" id="st-email-3" name="invite" placeholder="[email protected]" autocomplete="off">
              </div>
            </fieldset>
            <div>
              <label class="form-label" for="st-role">Invite as</label>
              <select class="form-select" id="st-role" name="role"><option>Member</option><option>Admin</option></select>
            </div>
            <p class="st-skip"><button class="btn btn-link" type="button" data-skip>Skip for now</button><span>You can invite people later from Settings.</span></p>
          </div>
          <div class="st-pane" data-step hidden>
            <h3 tabindex="-1">Choose a plan</h3>
            <p class="st-pane-sub">Change or cancel at any time. Prices exclude VAT.</p>
            <fieldset class="st-plans">
              <legend class="visually-hidden">Plan</legend>
              <label class="st-plan"><input class="form-check-input" type="radio" name="plan" value="free"><span class="st-plan-name">Free</span><span class="st-price"><b>£0</b>for up to 3 people</span><span class="st-plan-note">2 boards, 7-day history</span></label>
              <label class="st-plan"><input class="form-check-input" type="radio" name="plan" value="team" checked><span class="st-plan-name">Team <span class="badge rounded-pill st-badge">Popular</span></span><span class="st-price"><b>£8</b>per user / month</span><span class="st-plan-note">Unlimited boards and guests</span></label>
              <label class="st-plan"><input class="form-check-input" type="radio" name="plan" value="business"><span class="st-plan-name">Business</span><span class="st-price"><b>£16</b>per user / month</span><span class="st-plan-note">SSO, audit log, priority support</span></label>
            </fieldset>
          </div>
        </div>
        <div class="modal-footer">
          <p class="st-status" data-step-status aria-live="polite">Step 1 of 3</p>
          <button type="button" class="btn st-btn" data-back hidden>Back</button>
          <button type="button" class="btn st-btn btn-primary" data-next>Continue<svg aria-hidden="true"><use href="#st-i-arrow"/></svg></button>
          <button type="submit" class="btn st-btn btn-primary" data-finish hidden>Create workspace</button>
        </div>
      </form>
    </div>
  </div>
</div>
CSS
/* Bootstrap's Modal component, restyled through its --bs-modal-* and --bs-btn-* variables.
   Each step is a panel hidden with the hidden attribute; the script after the bundle moves between them. */
.st-btn{--bs-btn-padding-x:16px;--bs-btn-padding-y:9px;--bs-btn-font-size:.9rem;--bs-btn-font-weight:600;--bs-btn-line-height:1.2;--bs-btn-border-radius:10px;
  --bs-btn-bg:#fff;--bs-btn-color:#0f172a;--bs-btn-border-color:#cbd5e1;--bs-btn-hover-bg:#f1f5f9;--bs-btn-hover-color:#0f172a;--bs-btn-hover-border-color:#94a3b8;
  --bs-btn-active-bg:#e2e8f0;--bs-btn-active-color:#0f172a;--bs-btn-active-border-color:#94a3b8;
  display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:42px}
.st-btn.btn-primary{--bs-btn-bg:#2563eb;--bs-btn-color:#fff;--bs-btn-border-color:#2563eb;--bs-btn-hover-bg:#1d4ed8;--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:#1d4ed8;
  --bs-btn-active-bg:#1e40af;--bs-btn-active-color:#fff;--bs-btn-active-border-color:#1e40af;box-shadow:0 6px 16px -8px rgba(37,99,235,.7)}
.st-btn.st-btn-text{--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-color:#475569;--bs-btn-hover-bg:#f1f5f9;--bs-btn-hover-border-color:transparent;--bs-btn-hover-color:#0f172a;
  --bs-btn-active-bg:#e2e8f0;--bs-btn-active-border-color:transparent;--bs-btn-active-color:#0f172a}
.st-btn:focus-visible{outline:2px solid #2563eb;outline-offset:2px;box-shadow:none}
.st-btn svg{width:18px;height:18px;flex:none}

.st{--bs-modal-width:560px;--bs-modal-margin:1rem;--bs-modal-color:#0f172a;--bs-modal-bg:#fff;--bs-modal-border-width:0;--bs-modal-border-radius:18px;--bs-modal-inner-border-radius:18px;
  --bs-modal-header-padding:22px 24px 0;--bs-modal-header-border-color:#e2e8f0;--bs-modal-title-line-height:1.35;--bs-modal-footer-bg:#f8fafc;--bs-modal-footer-border-color:#e2e8f0;
  font-size:.95rem;line-height:1.55}
@media (min-width:576px){.st{--bs-modal-margin:1.75rem}}
.st.st-create{--bs-modal-width:600px}
.st .modal-content{overflow:hidden;box-shadow:0 0 0 1px rgba(15,23,42,.06),0 28px 64px -18px rgba(15,23,42,.45)}
.st .modal-footer{flex-wrap:wrap;align-items:center;gap:10px;padding:16px 24px}
.st .modal-footer>*{margin:0}
.st-status{margin:0 auto 0 0!important;color:#475569;font-size:.85rem;font-weight:600}
/* Same entrance as the plain version: a short rise and scale instead of Bootstrap's 50px drop */
.st.fade .modal-dialog{transform:translateY(10px) scale(.97);transition:transform .22s cubic-bezier(.2,.8,.2,1)}
.st.show .modal-dialog{transform:none}
.modal-backdrop{--bs-backdrop-bg:#0f172a;--bs-backdrop-opacity:.55;backdrop-filter:blur(2px)}
/* --st-dir is 1 going forward and -1 going back (set by the script) */
@keyframes st-slide{from{opacity:0;transform:translateX(calc(var(--st-dir,1) * 18px))}}

/* Onboarding tour */
.st-chip{position:absolute;top:16px;left:16px;z-index:1;display:inline-flex;align-items:center;gap:7px;margin:0;padding:5px 12px 5px 6px;border-radius:999px;
  background:rgba(255,255,255,.9);color:#1e3a8a;font-size:.78rem;font-weight:700;letter-spacing:.01em;line-height:1.4;box-shadow:0 1px 2px rgba(15,23,42,.08)}
.st-chip svg{width:20px;height:20px}
.st-slide,.st-pane{animation:st-slide .32s cubic-bezier(.2,.8,.2,1)}
.st-art{display:grid;place-items:center;height:210px;padding:34px 20px 10px}
.st-art svg{width:100%;height:100%;max-width:340px}
.st-art.is-sky{background:linear-gradient(135deg,#e0f2fe,#dbeafe 55%,#e0e7ff)}
.st-art.is-indigo{background:linear-gradient(135deg,#e0e7ff,#ede9fe 60%,#fae8ff)}
.st-art.is-mint{background:linear-gradient(135deg,#d1fae5,#ccfbf1 55%,#e0f2fe)}
.st-copy{padding:22px 28px 24px}
.st-copy h3{margin:0;border-radius:4px;color:#0f172a;font-size:1.3rem;font-weight:700;letter-spacing:-.015em;line-height:1.3}
.st-copy p{margin:6px 0 0;color:#475569}
.st-copy h3:focus-visible,.st-pane h3:focus-visible{outline:2px solid #2563eb;outline-offset:4px}
.st-progress{display:flex;align-items:center;gap:12px;margin-right:auto!important}
.st-progress .st-status{margin:0!important}
.st-dots{display:flex;gap:6px}
.st-dots span{width:8px;height:8px;border-radius:999px;background:#cbd5e1;transition:width .25s,background-color .25s}
.st-dots [data-state=done]{background:#93c5fd}
.st-dots [data-state=current]{width:22px;background:#2563eb}

/* Multi-step form: header with a stepper */
.st-create .modal-header{flex-wrap:wrap;align-items:flex-start;gap:0 14px}
.st-create .modal-header>.flex-grow-1{flex-basis:0;min-width:0}
.st .modal-title{font-size:1.15rem;font-weight:700;letter-spacing:-.01em}
.st-sub{margin:3px 0 0;color:#475569;font-size:.85rem}
.st .modal-header .btn-close{flex:none;width:36px;height:36px;margin:-4px -8px 0 auto;padding:0;border-radius:10px;background-size:13px;opacity:.75}
.st .btn-close:hover{background-color:#f1f5f9;opacity:1}
.st .btn-close:focus-visible{outline:2px solid #2563eb;outline-offset:1px;box-shadow:none;opacity:1}
.st-stepper{display:flex;flex:0 0 100%;align-items:center;margin:18px 0 0;padding:0 0 18px;list-style:none}
.st-stepper li{display:flex;flex:1;align-items:center;gap:10px;min-width:0;color:#475569;font-size:.875rem;font-weight:600}
.st-stepper li:last-child{flex:none}
.st-stepper li:not(:last-child)::after{content:"";flex:1;min-width:12px;height:2px;margin-right:10px;border-radius:2px;background:#e2e8f0}
.st-num{display:grid;place-items:center;flex:none;width:28px;height:28px;border:2px solid #cbd5e1;border-radius:50%;background:#fff;color:#475569;font-size:.8rem;font-weight:700}
.st-num svg{display:none;width:16px;height:16px}
.st-stepper [data-state=current],.st-stepper [data-state=done]{color:#0f172a}
.st-stepper [data-state=current] .st-num{border-color:#2563eb;background:#2563eb;color:#fff;box-shadow:0 0 0 4px #dbeafe}
.st-stepper [data-state=done] .st-num{border-color:#dbeafe;background:#dbeafe;color:#1d4ed8}
.st-stepper [data-state=done] .st-num b{display:none}
.st-stepper [data-state=done] .st-num svg{display:block}
.st-stepper li[data-state=done]::after{background:#2563eb}

/* Step panels and fields: Bootstrap's form controls with the set's border, radius and focus ring */
.st-main{display:flex;flex:1 1 auto;flex-direction:column;min-height:0}
.st-create .modal-body{padding:22px 24px 24px;overscroll-behavior:contain}
.st-pane h3{margin:0;border-radius:4px;color:#0f172a;font-size:1.05rem;font-weight:700;line-height:1.4}
.st-pane-sub{margin:3px 0 18px;color:#475569;font-size:.9rem}
.st .form-label{margin-bottom:6px;padding:0;color:#0f172a;font-size:.875rem;font-weight:600;line-height:1.4}
.st legend.form-label{float:none;width:auto}
.st .form-control,.st .form-select,.st .input-group-text{min-height:44px;border-color:#7c8aa0;border-radius:10px;font-size:.95rem;line-height:1.4}
.st .form-control,.st .form-select{padding:10px 12px;color:#0f172a;transition:border-color .15s}
.st .form-select{padding-right:38px;background-position:right 12px center;background-size:16px;cursor:pointer}
.st .form-control::placeholder{color:#64748b}
.st .form-control:hover,.st .form-select:hover{border-color:#64748b}
.st .form-control:focus,.st .form-select:focus{border-color:#2563eb;box-shadow:none;outline:2px solid #2563eb;outline-offset:1px}
.st .input-group-text{padding:0 12px;background:#f1f5f9;color:#475569;font-size:.9rem}
.st .form-text{margin-top:6px;color:#475569;font-size:.8rem}
.st-skip{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;margin:18px 0 0;padding-top:16px;border-top:1px dashed #e2e8f0;color:#475569;font-size:.85rem}
.st-skip .btn-link{--bs-btn-padding-x:6px;--bs-btn-padding-y:4px;--bs-btn-font-size:.85rem;--bs-btn-font-weight:600;--bs-btn-border-radius:6px;--bs-btn-color:#1d4ed8;--bs-btn-hover-color:#1e40af;--bs-btn-active-color:#1e40af;
  margin-left:-6px;border:0;text-underline-offset:3px}
.st-skip .btn-link:focus-visible{outline:2px solid #2563eb;outline-offset:1px;box-shadow:none}

/* Plan choice: radio cards built on .form-check-input */
.st-plans{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;min-width:0;margin:0;padding:0;border:0}
.st-plan{position:relative;display:flex;flex-direction:column;gap:2px;margin:0;padding:14px;border:1px solid #cbd5e1;border-radius:14px;background:#fff;cursor:pointer;
  transition:border-color .15s,background-color .15s,box-shadow .15s}
.st-plan:hover{border-color:#94a3b8}
.st-plan:has(:checked){border-color:#2563eb;background:#eff6ff;box-shadow:inset 0 0 0 1px #2563eb}
.st-plan .form-check-input{position:absolute;top:14px;right:14px;width:18px;height:18px;margin:0;border:1.5px solid #7c8aa0;cursor:pointer}
.st-plan .form-check-input:checked{border-color:#2563eb;background-color:#2563eb}
.st-plan .form-check-input:focus{box-shadow:none}
.st-plan .form-check-input:focus-visible{outline:2px solid #2563eb;outline-offset:2px}
.st-plan-name{display:flex;align-items:center;gap:6px;color:#0f172a;font-size:.95rem;font-weight:700}
.st-badge{padding:2px 7px;background:#dbeafe;color:#1e40af;font-size:.68rem;letter-spacing:.04em;text-transform:uppercase}
.st-price{margin-top:6px;color:#475569;font-size:.8rem}
.st-price b{display:block;color:#0f172a;font-size:1.5rem;font-weight:700;letter-spacing:-.02em;line-height:1.2}
.st-plan-note{margin-top:8px;padding-top:8px;border-top:1px solid #e2e8f0;color:#475569;font-size:.8rem}

@media (max-width:600px){
  .st{--bs-modal-header-padding:22px 18px 0}
  .st-copy{padding-left:18px;padding-right:18px}
  .st .modal-footer,.st-create .modal-body{padding-left:18px;padding-right:18px}
  .st .modal-footer .st-btn{flex:1 1 auto}
  .st .modal-footer>.st-status,.st-progress{flex-basis:100%}
  .st-plans{grid-template-columns:1fr}
  .st-plan{display:grid;grid-template-columns:1fr auto;gap:2px 12px;padding-right:44px}
  .st-plan .form-check-input{top:50%;translate:0 -50%}
  .st-price{grid-column:2;grid-row:1 / 3;align-self:center;margin:0;text-align:right}
  .st-plan-note{margin:0;padding:0;border:0}
}
@media (prefers-reduced-motion:reduce){
  .st.fade .modal-dialog,.st-btn,.st .form-control,.st .form-select,.st-plan,.st-dots span{transition:none}
  .st-slide,.st-pane{animation:none}
}
JavaScript
// Step-by-step dialogs. Inside any element with data-steps:
//   [data-step]          one panel per step, hidden with the hidden attribute
//   [data-marker]        progress dots / stepper items: get data-state="done|current|todo" and aria-current="step"
//   [data-done]          visually hidden "completed" text inside a marker, shown once that step is done
//   [data-step-status]   polite live region that reads "Step 2 of 3"
//   [data-back] [data-next] [data-skip]   buttons that move between steps; [data-finish] shows on the last step only
document.querySelectorAll('[data-steps]').forEach(function (root) {
  var panels = root.querySelectorAll('[data-step]'), last = panels.length - 1, step = 0;
  var form = panels[0].closest('form');
  function show(sel, on) { root.querySelectorAll(sel).forEach(function (el) { el.hidden = !on; }); }

  function go(n, moveFocus) {
    root.style.setProperty('--st-dir', n < step ? -1 : 1); // slide direction for the CSS animation
    step = n;
    panels.forEach(function (p, i) { p.hidden = i !== n; });
    root.querySelectorAll('[data-marker]').forEach(function (m, i) {
      m.setAttribute('data-state', i < n ? 'done' : i === n ? 'current' : 'todo');
      if (i === n) m.setAttribute('aria-current', 'step'); else m.removeAttribute('aria-current');
      var done = m.querySelector('[data-done]'); if (done) done.hidden = i >= n;
    });
    show('[data-back]', n > 0); show('[data-next]', n < last); show('[data-finish]', n === last);
    var status = root.querySelector('[data-step-status]');
    if (status) status.textContent = 'Step ' + (n + 1) + ' of ' + panels.length;
    if (moveFocus) panels[n].querySelector('[tabindex="-1"]').focus(); // land on the new step's heading
  }

  function next(skip) {
    var fields = panels[step].querySelectorAll('input, select, textarea');
    if (skip) panels[step].querySelectorAll('input').forEach(function (f) { f.value = f.defaultValue; }); // "Skip for now" empties the step
    else if (![].every.call(fields, function (f) { return f.reportValidity(); })) return; // browser message on the first invalid field
    go(step + 1, true);
  }

  root.addEventListener('click', function (e) {
    var b = e.target.closest('[data-next], [data-back], [data-skip]');
    if (b) b.hasAttribute('data-back') ? go(step - 1, true) : next(b.hasAttribute('data-skip'));
  });
  if (form) form.addEventListener('submit', function (e) {
    e.preventDefault();
    if (step < last) return next();                        // Enter in a field goes to the next step
    root.addEventListener('hidden.bs.modal', function () { form.reset(); }, { once: true }); // clear once it has faded out
    bootstrap.Modal.getOrCreateInstance(root).hide();      // the last step's submit button closes the modal
  });
  root.addEventListener('show.bs.modal', function () { step = 0; go(0); }); // always open on step 1
  root.addEventListener('shown.bs.modal', function () { var f = root.querySelector('[autofocus]'); if (f) f.focus(); }); // Bootstrap ignores autofocus
  go(0);
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="st-i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
  <symbol id="st-i-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
  <symbol id="st-i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></symbol>
  <symbol id="st-i-logo" viewBox="0 0 24 24"><circle cx="9" cy="12" r="6.5" fill="#2563eb"/><circle cx="15" cy="12" r="6.5" fill="#60a5fa" fill-opacity=".85"/></symbol>
</svg>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Onboarding tour</span>
<div class="flex flex-wrap gap-2.5"><button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#2563eb] bg-[#2563eb] text-white shadow-[0_6px_16px_-8px_rgba(37,99,235,.7)] hover:border-[#1d4ed8] hover:bg-[#1d4ed8]" type="button" data-open="st-tour" aria-haspopup="dialog">Take the tour</button></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Multi-step form</span>
<div class="flex flex-wrap gap-2.5"><button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#cbd5e1] bg-white text-[#0f172a] hover:border-[#94a3b8] hover:bg-[#f1f5f9]" type="button" data-open="st-create" aria-haspopup="dialog">Create workspace</button></div></div>

<!-- Onboarding tour: three slides with Back / Next; Skip and "Get started" close it -->
<dialog class="m-auto max-h-[calc(100dvh-56px)] w-[min(560px,calc(100%-32px))] max-w-none overflow-hidden overscroll-contain rounded-[18px] border-0 bg-white p-0 text-[.95rem] leading-[1.55] text-[#0f172a] shadow-[0_0_0_1px_rgba(15,23,42,.06),0_28px_64px_-18px_rgba(15,23,42,.45)] backdrop:bg-[rgba(15,23,42,.55)] backdrop:backdrop-blur-[2px] open:flex open:flex-col motion-safe:open:animate-[st-in_.22s_cubic-bezier(.2,.8,.2,1)] motion-safe:open:backdrop:animate-[st-fade_.22s_ease-out] max-[600px]:max-h-[calc(100dvh-32px)]" id="st-tour" aria-labelledby="st-tour-title" data-steps>
  <form class="m-0 flex min-h-0 flex-1 flex-col" method="dialog">
    <div class="relative min-h-0 flex-auto overflow-y-auto">
      <h2 class="absolute top-4 left-4 z-[1] m-0 inline-flex items-center gap-[7px] rounded-full bg-white/90 py-[5px] pr-3 pl-1.5 text-[.78rem] leading-[1.4] font-bold tracking-[.01em] text-[#1e3a8a] shadow-[0_1px_2px_rgba(15,23,42,.08)]" id="st-tour-title"><svg class="size-5" aria-hidden="true"><use href="#st-i-logo"/></svg>Welcome to Tandem</h2>
      <div class="motion-safe:animate-[st-slide_.32s_cubic-bezier(.2,.8,.2,1)]" data-step>
        <div class="grid h-[210px] place-items-center px-5 pt-[34px] pb-2.5 bg-[linear-gradient(135deg,#e0f2fe,#dbeafe_55%,#e0e7ff)] [&_svg]:size-full [&_svg]:max-w-[340px]" aria-hidden="true">
          <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="288" cy="24" r="44" fill="#fff" fill-opacity=".45"/><circle cx="20" cy="150" r="34" fill="#fff" fill-opacity=".4"/>
            <rect x="34" y="28" width="260" height="132" rx="14" fill="#1e3a8a" fill-opacity=".08"/>
            <rect x="30" y="20" width="260" height="132" rx="14" fill="#fff"/>
            <rect x="144" y="30" width="32" height="14" rx="7" fill="#2563eb"/>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="9" font-weight="600" fill="#64748b" text-anchor="middle"><text x="64" y="40">Mon</text><text x="112" y="40">Tue</text><text x="160" y="40" fill="#fff">Wed</text><text x="208" y="40">Thu</text><text x="256" y="40">Fri</text></g>
            <path d="M40 50h240" stroke="#e2e8f0"/><path d="M88 56v88M136 56v88M184 56v88M232 56v88" stroke="#f1f5f9"/>
            <rect x="44" y="58" width="40" height="30" rx="6" fill="#dbeafe"/><rect x="50" y="66" width="24" height="4" rx="2" fill="#2563eb"/><rect x="50" y="75" width="16" height="4" rx="2" fill="#93c5fd"/>
            <rect x="92" y="78" width="40" height="42" rx="6" fill="#e0e7ff"/><rect x="98" y="86" width="26" height="4" rx="2" fill="#6366f1"/><rect x="98" y="95" width="18" height="4" rx="2" fill="#a5b4fc"/>
            <rect x="140" y="58" width="40" height="24" rx="6" fill="#d1fae5"/><rect x="146" y="66" width="22" height="4" rx="2" fill="#059669"/>
            <rect x="140" y="98" width="40" height="34" rx="6" fill="#dbeafe"/><rect x="146" y="106" width="24" height="4" rx="2" fill="#2563eb"/><rect x="146" y="115" width="14" height="4" rx="2" fill="#93c5fd"/>
            <rect x="188" y="66" width="40" height="36" rx="6" fill="#fef3c7"/><rect x="194" y="74" width="24" height="4" rx="2" fill="#d97706"/><rect x="194" y="83" width="16" height="4" rx="2" fill="#fcd34d"/>
            <rect x="236" y="58" width="40" height="28" rx="6" fill="#fce7f3"/><rect x="242" y="66" width="22" height="4" rx="2" fill="#db2777"/>
            <rect x="236" y="96" width="40" height="30" rx="6" fill="#e0e7ff"/><rect x="242" y="104" width="24" height="4" rx="2" fill="#6366f1"/>
            <g stroke="#fff" stroke-width="3"><circle cx="232" cy="152" r="11" fill="#2563eb"/><circle cx="251" cy="152" r="11" fill="#059669"/><circle cx="270" cy="152" r="11" fill="#d97706"/></g>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle"><text x="232" y="155">MK</text><text x="251" y="155">JO</text><text x="270" y="155">AL</text></g>
          </svg>
        </div>
        <div class="px-7 pt-[22px] pb-6 max-[600px]:px-[18px]"><h3 class="m-0 rounded text-[1.3rem] leading-[1.3] font-bold tracking-[-.015em] text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#2563eb]" tabindex="-1">Plan the week together</h3><p class="mt-1.5 mb-0 text-[#475569]">Drop tasks onto one shared weekly board, so everyone can see who is doing what and when.</p></div>
      </div>
      <div class="motion-safe:animate-[st-slide_.32s_cubic-bezier(.2,.8,.2,1)]" data-step hidden>
        <div class="grid h-[210px] place-items-center px-5 pt-[34px] pb-2.5 bg-[linear-gradient(135deg,#e0e7ff,#ede9fe_60%,#fae8ff)] [&_svg]:size-full [&_svg]:max-w-[340px]" aria-hidden="true">
          <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="294" cy="142" r="40" fill="#fff" fill-opacity=".4"/><circle cx="26" cy="20" r="30" fill="#fff" fill-opacity=".45"/>
            <rect x="24" y="18" width="124" height="138" rx="14" fill="#fff" fill-opacity=".72"/>
            <rect x="36" y="30" width="42" height="6" rx="3" fill="#a5b4fc"/>
            <rect x="36" y="46" width="100" height="34" rx="8" fill="#fff" stroke="#e0e7ff"/><rect x="46" y="56" width="52" height="4" rx="2" fill="#94a3b8"/><rect x="46" y="66" width="34" height="4" rx="2" fill="#cbd5e1"/>
            <rect x="36.5" y="88.5" width="99" height="33" rx="8" stroke="#a5b4fc" stroke-dasharray="4 3"/>
            <rect x="36" y="130" width="100" height="18" rx="8" fill="#fff" fill-opacity=".8"/>
            <rect x="172" y="18" width="124" height="138" rx="14" fill="#fff" fill-opacity=".72"/>
            <circle cx="191" cy="33" r="9" fill="#6366f1"/><text x="191" y="36" font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle">SR</text><rect x="206" y="30" width="46" height="6" rx="3" fill="#a5b4fc"/>
            <rect x="184" y="50" width="100" height="34" rx="8" fill="#fff" stroke="#e0e7ff"/><rect x="194" y="60" width="48" height="4" rx="2" fill="#94a3b8"/><rect x="194" y="70" width="30" height="4" rx="2" fill="#cbd5e1"/>
            <rect x="184.5" y="92.5" width="99" height="33" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-dasharray="4 3"/>
            <path d="M104 104c18-22 40-24 58-6" stroke="#6366f1" stroke-opacity=".55" stroke-width="2" stroke-dasharray="1 5" stroke-linecap="round"/>
            <g transform="rotate(-6 210 118)">
              <rect x="163" y="106" width="100" height="36" rx="8" fill="#4338ca" fill-opacity=".14"/>
              <rect x="160" y="100" width="100" height="36" rx="8" fill="#fff" stroke="#6366f1" stroke-width="1.5"/>
              <rect x="170" y="110" width="24" height="6" rx="3" fill="#6366f1"/><rect x="170" y="122" width="56" height="4" rx="2" fill="#94a3b8"/>
              <circle cx="244" cy="118" r="8" fill="#d97706"/>
            </g>
            <path d="M236 122v21l5.2-5 3.8 8.6 4-1.8-3.8-8.4h7.2z" fill="#0f172a" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
          </svg>
        </div>
        <div class="px-7 pt-[22px] pb-6 max-[600px]:px-[18px]"><h3 class="m-0 rounded text-[1.3rem] leading-[1.3] font-bold tracking-[-.015em] text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#2563eb]" tabindex="-1">Assign work in one drag</h3><p class="mt-1.5 mb-0 text-[#475569]">Drag a card onto a teammate and Tandem assigns it, sets the due date and lets them know.</p></div>
      </div>
      <div class="motion-safe:animate-[st-slide_.32s_cubic-bezier(.2,.8,.2,1)]" data-step hidden>
        <div class="grid h-[210px] place-items-center px-5 pt-[34px] pb-2.5 bg-[linear-gradient(135deg,#d1fae5,#ccfbf1_55%,#e0f2fe)] [&_svg]:size-full [&_svg]:max-w-[340px]" aria-hidden="true">
          <svg viewBox="0 0 320 170" fill="none" focusable="false">
            <circle cx="30" cy="144" r="38" fill="#fff" fill-opacity=".4"/><circle cx="300" cy="150" r="30" fill="#fff" fill-opacity=".45"/>
            <rect x="24" y="26" width="192" height="132" rx="14" fill="#065f46" fill-opacity=".08"/>
            <rect x="20" y="18" width="192" height="132" rx="14" fill="#fff"/>
            <path d="M34 62h164M34 106h164" stroke="#f1f5f9"/>
            <circle cx="46" cy="40" r="11" fill="#2563eb"/><circle cx="46" cy="84" r="11" fill="#d97706"/><circle cx="46" cy="128" r="11" fill="#6366f1"/>
            <g font-family="ui-sans-serif,system-ui,sans-serif" font-size="7.5" font-weight="700" fill="#fff" text-anchor="middle"><text x="46" y="43">MK</text><text x="46" y="87">AL</text><text x="46" y="131">SR</text></g>
            <rect x="66" y="33" width="90" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="44" width="60" height="5" rx="2.5" fill="#cbd5e1"/>
            <rect x="66" y="77" width="76" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="88" width="96" height="5" rx="2.5" fill="#cbd5e1"/>
            <rect x="66" y="121" width="84" height="5" rx="2.5" fill="#94a3b8"/><rect x="66" y="132" width="50" height="5" rx="2.5" fill="#cbd5e1"/>
            <circle cx="190" cy="40" r="9" fill="#10b981"/><path d="M186 40.5l2.8 2.8 5-5.6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            <rect x="170" y="78" width="30" height="12" rx="6" fill="#dbeafe"/><rect x="177" y="82.5" width="16" height="3" rx="1.5" fill="#2563eb"/>
            <circle cx="190" cy="128" r="9" fill="#10b981"/><path d="M186 128.5l2.8 2.8 5-5.6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            <path d="M222 50l-9 6 12 1z" fill="#2563eb"/>
            <rect x="220" y="30" width="88" height="32" rx="12" fill="#2563eb"/>
            <text x="264" y="50" font-family="ui-sans-serif,system-ui,sans-serif" font-size="10" font-weight="700" fill="#fff" text-anchor="middle">All caught up</text>
            <circle cx="262" cy="112" r="22" fill="#fff"/>
            <path d="M255 117h14l-1.6-2.4v-5.6a5.4 5.4 0 0 0-10.8 0v5.6z" stroke="#2563eb" stroke-width="2" stroke-linejoin="round"/><path d="M259.6 120.5a2.5 2.5 0 0 0 4.8 0" stroke="#2563eb" stroke-width="2" stroke-linecap="round"/>
            <circle cx="271" cy="100" r="4.5" fill="#ef4444" stroke="#fff" stroke-width="2"/>
          </svg>
        </div>
        <div class="px-7 pt-[22px] pb-6 max-[600px]:px-[18px]"><h3 class="m-0 rounded text-[1.3rem] leading-[1.3] font-bold tracking-[-.015em] text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#2563eb]" tabindex="-1">Stay in sync without meetings</h3><p class="mt-1.5 mb-0 text-[#475569]">Short written updates and a daily digest replace the status call, so calendars stay free for real work.</p></div>
      </div>
    </div>
    <div class="flex flex-wrap items-center justify-end gap-2.5 border-t border-[#e2e8f0] bg-[#f8fafc] px-6 py-4 max-[600px]:px-[18px]">
      <div class="mr-auto flex items-center gap-3 max-[600px]:basis-full">
        <span class="flex gap-1.5" aria-hidden="true"><span class="h-2 w-2 rounded-full bg-[#cbd5e1] transition-[width,background-color] duration-[250ms] motion-reduce:transition-none data-[state=done]:bg-[#93c5fd] data-[state=current]:w-[22px] data-[state=current]:bg-[#2563eb]" data-marker data-state="current"></span><span class="h-2 w-2 rounded-full bg-[#cbd5e1] transition-[width,background-color] duration-[250ms] motion-reduce:transition-none data-[state=done]:bg-[#93c5fd] data-[state=current]:w-[22px] data-[state=current]:bg-[#2563eb]" data-marker data-state="todo"></span><span class="h-2 w-2 rounded-full bg-[#cbd5e1] transition-[width,background-color] duration-[250ms] motion-reduce:transition-none data-[state=done]:bg-[#93c5fd] data-[state=current]:w-[22px] data-[state=current]:bg-[#2563eb]" data-marker data-state="todo"></span></span>
        <p class="m-0 text-[.85rem] font-semibold text-[#475569]" data-step-status aria-live="polite">Step 1 of 3</p>
      </div>
      <button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-transparent bg-transparent text-[#475569] hover:bg-[#f1f5f9] hover:text-[#0f172a] max-[600px]:flex-auto" value="skip" formnovalidate>Skip</button>
      <button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#cbd5e1] bg-white text-[#0f172a] hover:border-[#94a3b8] hover:bg-[#f1f5f9] max-[600px]:flex-auto" type="button" data-back hidden>Back</button>
      <button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#2563eb] bg-[#2563eb] text-white shadow-[0_6px_16px_-8px_rgba(37,99,235,.7)] hover:border-[#1d4ed8] hover:bg-[#1d4ed8] max-[600px]:flex-auto" type="button" data-next autofocus>Next<svg class="size-[18px] flex-none" aria-hidden="true"><use href="#st-i-arrow"/></svg></button>
      <button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#2563eb] bg-[#2563eb] text-white shadow-[0_6px_16px_-8px_rgba(37,99,235,.7)] hover:border-[#1d4ed8] hover:bg-[#1d4ed8] max-[600px]:flex-auto" value="done" data-finish hidden>Get started</button>
    </div>
  </form>
</dialog>

<!-- Multi-step form: each step is checked with reportValidity() before moving on -->
<dialog class="m-auto max-h-[calc(100dvh-56px)] w-[min(600px,calc(100%-32px))] max-w-none overflow-hidden overscroll-contain rounded-[18px] border-0 bg-white p-0 text-[.95rem] leading-[1.55] text-[#0f172a] shadow-[0_0_0_1px_rgba(15,23,42,.06),0_28px_64px_-18px_rgba(15,23,42,.45)] backdrop:bg-[rgba(15,23,42,.55)] backdrop:backdrop-blur-[2px] open:flex open:flex-col motion-safe:open:animate-[st-in_.22s_cubic-bezier(.2,.8,.2,1)] motion-safe:open:backdrop:animate-[st-fade_.22s_ease-out] max-[600px]:max-h-[calc(100dvh-32px)]" id="st-create" aria-labelledby="st-create-title" data-steps>
  <form class="m-0 flex-none border-b border-[#e2e8f0] px-6 pt-[22px] max-[600px]:px-[18px]" method="dialog">
    <div class="flex items-start gap-3.5">
      <div class="min-w-0 flex-1"><h2 class="m-0 text-[1.15rem] leading-[1.35] font-bold tracking-[-.01em]" id="st-create-title">Create a workspace</h2><p class="mt-[3px] mb-0 text-[.85rem] text-[#475569]">Three quick steps and your team is ready to plan.</p></div>
      <button class="-mt-1 -mr-2 grid size-9 flex-none cursor-pointer place-items-center rounded-[10px] border-0 bg-transparent p-0 text-[#475569] transition-colors duration-150 hover:bg-[#f1f5f9] hover:text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb] motion-reduce:transition-none" value="close" aria-label="Close"><svg class="size-5" aria-hidden="true"><use href="#st-i-x"/></svg></button>
    </div>
    <ol class="m-0 mt-[18px] flex list-none items-center p-0 pb-[18px]" aria-label="Setup steps">
      <li class="group flex min-w-0 flex-1 items-center gap-2.5 text-[.875rem] font-semibold text-[#475569] last:flex-none data-[state=current]:text-[#0f172a] data-[state=done]:text-[#0f172a] after:mr-2.5 after:h-0.5 after:min-w-3 after:flex-1 after:rounded-sm after:bg-[#e2e8f0] last:after:hidden data-[state=done]:after:bg-[#2563eb]" data-marker data-state="current" aria-current="step"><span class="grid size-7 flex-none place-items-center rounded-full border-2 border-[#cbd5e1] bg-white text-[.8rem] font-bold text-[#475569] group-data-[state=current]:border-[#2563eb] group-data-[state=current]:bg-[#2563eb] group-data-[state=current]:text-white group-data-[state=current]:shadow-[0_0_0_4px_#dbeafe] group-data-[state=done]:border-[#dbeafe] group-data-[state=done]:bg-[#dbeafe] group-data-[state=done]:text-[#1d4ed8]" aria-hidden="true"><b class="font-bold group-data-[state=done]:hidden">1</b><svg class="hidden size-4 group-data-[state=done]:block"><use href="#st-i-check"/></svg></span><span>Details</span><span class="sr-only" data-done hidden> (completed)</span></li>
      <li class="group flex min-w-0 flex-1 items-center gap-2.5 text-[.875rem] font-semibold text-[#475569] last:flex-none data-[state=current]:text-[#0f172a] data-[state=done]:text-[#0f172a] after:mr-2.5 after:h-0.5 after:min-w-3 after:flex-1 after:rounded-sm after:bg-[#e2e8f0] last:after:hidden data-[state=done]:after:bg-[#2563eb]" data-marker data-state="todo"><span class="grid size-7 flex-none place-items-center rounded-full border-2 border-[#cbd5e1] bg-white text-[.8rem] font-bold text-[#475569] group-data-[state=current]:border-[#2563eb] group-data-[state=current]:bg-[#2563eb] group-data-[state=current]:text-white group-data-[state=current]:shadow-[0_0_0_4px_#dbeafe] group-data-[state=done]:border-[#dbeafe] group-data-[state=done]:bg-[#dbeafe] group-data-[state=done]:text-[#1d4ed8]" aria-hidden="true"><b class="font-bold group-data-[state=done]:hidden">2</b><svg class="hidden size-4 group-data-[state=done]:block"><use href="#st-i-check"/></svg></span><span>Invite</span><span class="sr-only" data-done hidden> (completed)</span></li>
      <li class="group flex min-w-0 flex-1 items-center gap-2.5 text-[.875rem] font-semibold text-[#475569] last:flex-none data-[state=current]:text-[#0f172a] data-[state=done]:text-[#0f172a] after:mr-2.5 after:h-0.5 after:min-w-3 after:flex-1 after:rounded-sm after:bg-[#e2e8f0] last:after:hidden data-[state=done]:after:bg-[#2563eb]" data-marker data-state="todo"><span class="grid size-7 flex-none place-items-center rounded-full border-2 border-[#cbd5e1] bg-white text-[.8rem] font-bold text-[#475569] group-data-[state=current]:border-[#2563eb] group-data-[state=current]:bg-[#2563eb] group-data-[state=current]:text-white group-data-[state=current]:shadow-[0_0_0_4px_#dbeafe] group-data-[state=done]:border-[#dbeafe] group-data-[state=done]:bg-[#dbeafe] group-data-[state=done]:text-[#1d4ed8]" aria-hidden="true"><b class="font-bold group-data-[state=done]:hidden">3</b><svg class="hidden size-4 group-data-[state=done]:block"><use href="#st-i-check"/></svg></span><span>Plan</span><span class="sr-only" data-done hidden> (completed)</span></li>
    </ol>
  </form>
  <form class="m-0 flex min-h-0 flex-1 flex-col" method="dialog">
    <div class="min-h-0 flex-auto overflow-y-auto overscroll-contain px-6 pt-[22px] pb-6 max-[600px]:px-[18px]">
      <div class="motion-safe:animate-[st-slide_.32s_cubic-bezier(.2,.8,.2,1)]" data-step>
        <h3 class="m-0 rounded text-[1.05rem] leading-[1.4] font-bold text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#2563eb]" tabindex="-1">Name your workspace</h3>
        <p class="mt-[3px] mb-[18px] text-[.9rem] text-[#475569]">This is what your team sees when they sign in.</p>
        <div class="mb-4">
          <label class="mb-1.5 block p-0 text-[.875rem] leading-[1.4] font-semibold text-[#0f172a]" for="st-name">Workspace name</label>
          <input class="m-0 block min-h-[44px] w-full rounded-[10px] border border-[#7c8aa0] bg-white px-3 py-2.5 text-[.95rem] leading-[1.4] text-[#0f172a] transition-[border-color] duration-150 placeholder:text-[#64748b] hover:border-[#64748b] focus-visible:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb] motion-reduce:transition-none" id="st-name" name="workspace" required autocomplete="organization" placeholder="e.g. Northwind Design" autofocus>
        </div>
        <div>
          <label class="mb-1.5 block p-0 text-[.875rem] leading-[1.4] font-semibold text-[#0f172a]" for="st-url">Workspace URL</label>
          <div class="flex"><span class="flex items-center rounded-l-[10px] border border-r-0 border-[#7c8aa0] bg-[#f1f5f9] px-3 text-[.9rem] text-[#475569]" id="st-url-prefix">tandem.app/</span><input class="m-0 block min-h-[44px] w-full rounded-[10px] border border-[#7c8aa0] bg-white px-3 py-2.5 text-[.95rem] leading-[1.4] text-[#0f172a] transition-[border-color] duration-150 placeholder:text-[#64748b] hover:border-[#64748b] focus-visible:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb] motion-reduce:transition-none min-w-0 flex-1 rounded-l-none" id="st-url" name="url" required pattern="[a-z0-9\-]{3,30}" autocomplete="off" spellcheck="false" placeholder="northwind" aria-describedby="st-url-prefix st-url-hint"></div>
          <p class="mt-1.5 mb-0 text-[.8rem] text-[#475569]" id="st-url-hint">Lowercase letters, numbers and hyphens, 3 to 30 characters.</p>
        </div>
      </div>
      <div class="motion-safe:animate-[st-slide_.32s_cubic-bezier(.2,.8,.2,1)]" data-step hidden>
        <h3 class="m-0 rounded text-[1.05rem] leading-[1.4] font-bold text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#2563eb]" tabindex="-1">Invite your team</h3>
        <p class="mt-[3px] mb-[18px] text-[.9rem] text-[#475569]">Each person gets an email with a link to join.</p>
        <fieldset class="m-0 mb-4 min-w-0 border-0 p-0">
          <legend class="mb-1.5 block p-0 text-[.875rem] leading-[1.4] font-semibold text-[#0f172a]">Email addresses</legend>
          <div class="grid gap-2">
            <label class="sr-only" for="st-email-1">Teammate 1 email</label><input class="m-0 block min-h-[44px] w-full rounded-[10px] border border-[#7c8aa0] bg-white px-3 py-2.5 text-[.95rem] leading-[1.4] text-[#0f172a] transition-[border-color] duration-150 placeholder:text-[#64748b] hover:border-[#64748b] focus-visible:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb] motion-reduce:transition-none" type="email" id="st-email-1" name="invite" placeholder="[email protected]" autocomplete="off">
            <label class="sr-only" for="st-email-2">Teammate 2 email</label><input class="m-0 block min-h-[44px] w-full rounded-[10px] border border-[#7c8aa0] bg-white px-3 py-2.5 text-[.95rem] leading-[1.4] text-[#0f172a] transition-[border-color] duration-150 placeholder:text-[#64748b] hover:border-[#64748b] focus-visible:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb] motion-reduce:transition-none" type="email" id="st-email-2" name="invite" placeholder="[email protected]" autocomplete="off">
            <label class="sr-only" for="st-email-3">Teammate 3 email</label><input class="m-0 block min-h-[44px] w-full rounded-[10px] border border-[#7c8aa0] bg-white px-3 py-2.5 text-[.95rem] leading-[1.4] text-[#0f172a] transition-[border-color] duration-150 placeholder:text-[#64748b] hover:border-[#64748b] focus-visible:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb] motion-reduce:transition-none" type="email" id="st-email-3" name="invite" placeholder="[email protected]" autocomplete="off">
          </div>
        </fieldset>
        <div>
          <label class="mb-1.5 block p-0 text-[.875rem] leading-[1.4] font-semibold text-[#0f172a]" for="st-role">Invite as</label>
          <select class="m-0 block min-h-[44px] w-full rounded-[10px] border border-[#7c8aa0] bg-white px-3 py-2.5 text-[.95rem] leading-[1.4] text-[#0f172a] transition-[border-color] duration-150 placeholder:text-[#64748b] hover:border-[#64748b] focus-visible:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb] motion-reduce:transition-none cursor-pointer appearance-none bg-[url(data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20viewBox=%270%200%2016%2016%27%3E%3Cpath%20fill=%27none%27%20stroke=%27%23475569%27%20stroke-width=%272%27%20stroke-linecap=%27round%27%20stroke-linejoin=%27round%27%20d=%27m2%205%206%206%206-6%27/%3E%3C/svg%3E)] bg-[length:16px] bg-[position:right_12px_center] bg-no-repeat pr-[38px]" id="st-role" name="role"><option>Member</option><option>Admin</option></select>
        </div>
        <p class="mt-[18px] mb-0 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-dashed border-[#e2e8f0] pt-4 text-[.85rem] text-[#475569]"><button class="-ml-1.5 cursor-pointer rounded-md border-0 bg-transparent px-1.5 py-1 font-semibold text-[#1d4ed8] underline underline-offset-[3px] hover:text-[#1e40af] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb]" type="button" data-skip>Skip for now</button><span>You can invite people later from Settings.</span></p>
      </div>
      <div class="motion-safe:animate-[st-slide_.32s_cubic-bezier(.2,.8,.2,1)]" data-step hidden>
        <h3 class="m-0 rounded text-[1.05rem] leading-[1.4] font-bold text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#2563eb]" tabindex="-1">Choose a plan</h3>
        <p class="mt-[3px] mb-[18px] text-[.9rem] text-[#475569]">Change or cancel at any time. Prices exclude VAT.</p>
        <fieldset class="m-0 grid min-w-0 grid-cols-3 gap-2.5 border-0 p-0 max-[600px]:grid-cols-1">
          <legend class="sr-only">Plan</legend>
          <label class="relative flex cursor-pointer flex-col gap-0.5 rounded-[14px] border border-[#cbd5e1] bg-white p-3.5 transition-[border-color,background-color,box-shadow] duration-150 motion-reduce:transition-none hover:not-has-checked:border-[#94a3b8] has-checked:border-[#2563eb] has-checked:bg-[#eff6ff] has-checked:shadow-[inset_0_0_0_1px_#2563eb] max-[600px]:grid max-[600px]:grid-cols-[1fr_auto] max-[600px]:gap-x-3 max-[600px]:pr-11"><input class="absolute top-3.5 right-3.5 m-0 size-[18px] cursor-pointer appearance-none rounded-full border-[1.5px] border-[#7c8aa0] bg-white checked:border-[5px] checked:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] max-[600px]:top-1/2 max-[600px]:-translate-y-1/2" type="radio" name="plan" value="free"><span class="flex items-center gap-1.5 text-[.95rem] font-bold text-[#0f172a]">Free</span><span class="mt-1.5 text-[.8rem] text-[#475569] max-[600px]:col-start-2 max-[600px]:row-span-2 max-[600px]:row-start-1 max-[600px]:m-0 max-[600px]:self-center max-[600px]:text-right"><b class="block text-[1.5rem] leading-[1.2] font-bold tracking-[-.02em] text-[#0f172a]">£0</b>for up to 3 people</span><span class="mt-2 border-t border-[#e2e8f0] pt-2 text-[.8rem] text-[#475569] max-[600px]:m-0 max-[600px]:border-0 max-[600px]:p-0">2 boards, 7-day history</span></label>
          <label class="relative flex cursor-pointer flex-col gap-0.5 rounded-[14px] border border-[#cbd5e1] bg-white p-3.5 transition-[border-color,background-color,box-shadow] duration-150 motion-reduce:transition-none hover:not-has-checked:border-[#94a3b8] has-checked:border-[#2563eb] has-checked:bg-[#eff6ff] has-checked:shadow-[inset_0_0_0_1px_#2563eb] max-[600px]:grid max-[600px]:grid-cols-[1fr_auto] max-[600px]:gap-x-3 max-[600px]:pr-11"><input class="absolute top-3.5 right-3.5 m-0 size-[18px] cursor-pointer appearance-none rounded-full border-[1.5px] border-[#7c8aa0] bg-white checked:border-[5px] checked:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] max-[600px]:top-1/2 max-[600px]:-translate-y-1/2" type="radio" name="plan" value="team" checked><span class="flex items-center gap-1.5 text-[.95rem] font-bold text-[#0f172a]">Team <span class="rounded-full bg-[#dbeafe] px-[7px] py-px text-[.68rem] font-bold tracking-[.04em] text-[#1e40af] uppercase">Popular</span></span><span class="mt-1.5 text-[.8rem] text-[#475569] max-[600px]:col-start-2 max-[600px]:row-span-2 max-[600px]:row-start-1 max-[600px]:m-0 max-[600px]:self-center max-[600px]:text-right"><b class="block text-[1.5rem] leading-[1.2] font-bold tracking-[-.02em] text-[#0f172a]">£8</b>per user / month</span><span class="mt-2 border-t border-[#e2e8f0] pt-2 text-[.8rem] text-[#475569] max-[600px]:m-0 max-[600px]:border-0 max-[600px]:p-0">Unlimited boards and guests</span></label>
          <label class="relative flex cursor-pointer flex-col gap-0.5 rounded-[14px] border border-[#cbd5e1] bg-white p-3.5 transition-[border-color,background-color,box-shadow] duration-150 motion-reduce:transition-none hover:not-has-checked:border-[#94a3b8] has-checked:border-[#2563eb] has-checked:bg-[#eff6ff] has-checked:shadow-[inset_0_0_0_1px_#2563eb] max-[600px]:grid max-[600px]:grid-cols-[1fr_auto] max-[600px]:gap-x-3 max-[600px]:pr-11"><input class="absolute top-3.5 right-3.5 m-0 size-[18px] cursor-pointer appearance-none rounded-full border-[1.5px] border-[#7c8aa0] bg-white checked:border-[5px] checked:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] max-[600px]:top-1/2 max-[600px]:-translate-y-1/2" type="radio" name="plan" value="business"><span class="flex items-center gap-1.5 text-[.95rem] font-bold text-[#0f172a]">Business</span><span class="mt-1.5 text-[.8rem] text-[#475569] max-[600px]:col-start-2 max-[600px]:row-span-2 max-[600px]:row-start-1 max-[600px]:m-0 max-[600px]:self-center max-[600px]:text-right"><b class="block text-[1.5rem] leading-[1.2] font-bold tracking-[-.02em] text-[#0f172a]">£16</b>per user / month</span><span class="mt-2 border-t border-[#e2e8f0] pt-2 text-[.8rem] text-[#475569] max-[600px]:m-0 max-[600px]:border-0 max-[600px]:p-0">SSO, audit log, priority support</span></label>
        </fieldset>
      </div>
    </div>
    <div class="flex flex-wrap items-center justify-end gap-2.5 border-t border-[#e2e8f0] bg-[#f8fafc] px-6 py-4 max-[600px]:px-[18px]">
      <p class="m-0 text-[.85rem] font-semibold text-[#475569] mr-auto max-[600px]:basis-full" data-step-status aria-live="polite">Step 1 of 3</p>
      <button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#cbd5e1] bg-white text-[#0f172a] hover:border-[#94a3b8] hover:bg-[#f1f5f9] max-[600px]:flex-auto" type="button" data-back hidden>Back</button>
      <button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#2563eb] bg-[#2563eb] text-white shadow-[0_6px_16px_-8px_rgba(37,99,235,.7)] hover:border-[#1d4ed8] hover:bg-[#1d4ed8] max-[600px]:flex-auto" type="button" data-next>Continue<svg class="size-[18px] flex-none" aria-hidden="true"><use href="#st-i-arrow"/></svg></button>
      <button class="inline-flex min-h-[42px] cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-[9px] text-[.9rem] leading-[1.2] font-semibold no-underline transition-[background-color,border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#2563eb] bg-[#2563eb] text-white shadow-[0_6px_16px_-8px_rgba(37,99,235,.7)] hover:border-[#1d4ed8] hover:bg-[#1d4ed8] max-[600px]:flex-auto" value="done" data-finish hidden>Create workspace</button>
    </div>
  </form>
</dialog>
CSS
/* Entrance keyframes for the dialog and its backdrop (used through motion-safe:open:animate-[…]) */
@keyframes st-in { from { opacity: 0; transform: translateY(10px) scale(.97); } }
@keyframes st-fade { from { opacity: 0; } }
/* Step change: --st-dir is 1 going forward and -1 going back (set by the script) */
@keyframes st-slide { from { opacity: 0; transform: translateX(calc(var(--st-dir, 1) * 18px)); } }
/* Stop the page scrolling behind an open modal */
html:has(dialog:modal) { overflow: hidden; }
JavaScript
// Open a dialog from any button with data-open="dialog-id". showModal() traps focus, closes on Esc
// and makes the page behind inert. Buttons inside <form method="dialog"> close it with no script.
document.querySelectorAll('[data-open]').forEach(function (btn) {
  var dialog = document.getElementById(btn.getAttribute('data-open'));
  if (!dialog) return;
  btn.addEventListener('click', function () {
    dialog.showModal();
    dialog.addEventListener('close', function () { btn.focus(); }, { once: true }); // focus back to the trigger
  });
  // A click on the backdrop lands on the <dialog> itself (its form fills the box), so close it
  dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
});

// Step-by-step dialogs. Inside any element with data-steps:
//   [data-step]          one panel per step, hidden with the hidden attribute
//   [data-marker]        progress dots / stepper items: get data-state="done|current|todo" and aria-current="step"
//   [data-done]          visually hidden "completed" text inside a marker, shown once that step is done
//   [data-step-status]   polite live region that reads "Step 2 of 3"
//   [data-back] [data-next] [data-skip]   buttons that move between steps; [data-finish] shows on the last step only
document.querySelectorAll('[data-steps]').forEach(function (root) {
  var panels = root.querySelectorAll('[data-step]'), last = panels.length - 1, step = 0;
  var form = panels[0].closest('form');
  function show(sel, on) { root.querySelectorAll(sel).forEach(function (el) { el.hidden = !on; }); }

  function go(n, moveFocus) {
    root.style.setProperty('--st-dir', n < step ? -1 : 1); // slide direction for the CSS animation
    step = n;
    panels.forEach(function (p, i) { p.hidden = i !== n; });
    root.querySelectorAll('[data-marker]').forEach(function (m, i) {
      m.setAttribute('data-state', i < n ? 'done' : i === n ? 'current' : 'todo');
      if (i === n) m.setAttribute('aria-current', 'step'); else m.removeAttribute('aria-current');
      var done = m.querySelector('[data-done]'); if (done) done.hidden = i >= n;
    });
    show('[data-back]', n > 0); show('[data-next]', n < last); show('[data-finish]', n === last);
    var status = root.querySelector('[data-step-status]');
    if (status) status.textContent = 'Step ' + (n + 1) + ' of ' + panels.length;
    if (moveFocus) panels[n].querySelector('[tabindex="-1"]').focus(); // land on the new step's heading
  }

  function next(skip) {
    var fields = panels[step].querySelectorAll('input, select, textarea');
    if (skip) panels[step].querySelectorAll('input').forEach(function (f) { f.value = f.defaultValue; }); // "Skip for now" empties the step
    else if (![].every.call(fields, function (f) { return f.reportValidity(); })) return; // browser message on the first invalid field
    go(step + 1, true);
  }

  root.addEventListener('click', function (e) {
    var b = e.target.closest('[data-next], [data-back], [data-skip]');
    if (b) b.hasAttribute('data-back') ? go(step - 1, true) : next(b.hasAttribute('data-skip'));
  });
  if (form) form.addEventListener('submit', function (e) {
    if (e.submitter && e.submitter.formNoValidate) return; // Skip: <form method="dialog"> closes the dialog
    if (step < last) { e.preventDefault(); next(); }       // Enter in a field goes to the next step, not out
  });
  root.addEventListener('close', function () {
    if (root.returnValue === 'done' && form) form.reset(); // finished: clear the fields for next time
    root.returnValue = ''; step = 0; go(0);                // always reopen on step 1
  });
  go(0);
});

About this set

Two modals that walk the reader through more than one screen, for a team planning app called Tandem. Onboarding tour is a three-slide welcome: an illustrated panel drawn in inline SVG, a heading and one sentence per slide (Plan the week together, Assign work in one drag, Stay in sync without meetings), progress dots, a Step 1 of 3 line, and Skip, Back and Next buttons, with Next turning into Get started on the last slide. Multi-step form creates a workspace in three steps shown in a numbered stepper: a name and URL, up to three teammate emails with a role, and a plan chosen from radio cards. Use the tour once, after sign-up, and the form wherever one long form would put people off. A short generic script drives both from data attributes: panels are hidden with the hidden attribute, the current step gets aria-current=”step”, a polite live region announces the step, focus moves to the new step’s heading, and each step’s fields are checked with reportValidity() before moving on. Both reopen on step 1. The plain and Tailwind versions use the native dialog element; the Bootstrap version uses the Modal plugin with the same step script.

What’s included

  • Three-slide onboarding tour with inline SVG illustrations and progress dots
  • Three-step workspace form with a numbered stepper and radio-card plan choice
  • Each step validated with reportValidity() before Continue
  • Step changes announced in a polite live region, focus moved to the new heading
  • One small data-attribute script shared by both dialogs and all three versions

Accessibility

Both dialogs are labelled by their headings. The stepper is an ordered list with aria-current="step" on the current item and visually hidden completed text on finished ones, a polite live region reads Step 2 of 3 after each move, and focus lands on the new step's heading so screen reader and keyboard users start at the top. Every field has a label, plan cards are real radio inputs, every control shows a focus outline, and the slide transitions stop under prefers-reduced-motion.

Customise it

In plain CSS change the --st-* custom properties on .st (accent, ink, muted, line, soft, field, radius and --st-w for the width) and the gradients in the illustration panels. In Bootstrap edit --bs-modal-width and the --bs-btn-* variables; in Tailwind replace the #2563eb and slate hex utilities and the w-[min(…)] width.