← All blocks

Onboarding

Auth & onboarding

A progress bar and step-by-step forms that remember your answers.

Install this section
npx shadcn@latest add https://pouf.worksonmy.dev/r/onboarding.json

Lands with 7 components: button, input, layout, progress, radio-group, surface, text.

Live preview — it works, try itsection
Step 1 of 3 · Account

Welcome to Pouf

View source
import { useState } from 'react'
import { Card } from '../surface'
import { Stack, Row, Spacer } from '../layout'
import { Heading, Text, Eyebrow } from '../text'
import { Field, Input } from '../Input'
import { RadioGroup } from '../radiogroup'
import { Button } from '../Button'
import { Progress } from '../progress'

const STEPS = ['Account', 'Workspace', 'Preferences']

const PLAN_OPTIONS = [
  { value: 'solo', label: 'Just me' },
  { value: 'team', label: 'With a team' },
  { value: 'org', label: 'Whole organization' },
]

interface StepErrors {
  name?: string
  workspace?: string
}

/** An example onboarding wizard: a progress bar, per-step forms, and
 * back/next navigation — each step's answer is held in state that outlives
 * the step, so paging back and forth (or finishing) never loses what was
 * typed. The finish screen recaps every answer to prove it. */
export function OnboardingBlock() {
  const [step, setStep] = useState(0)
  const [name, setName] = useState('')
  const [workspace, setWorkspace] = useState('')
  const [plan, setPlan] = useState('solo')
  const [errors, setErrors] = useState<StepErrors>({})
  const [done, setDone] = useState(false)

  const pct = done ? 100 : ((step + 1) / STEPS.length) * 100

  function validateStep(): boolean {
    if (step === 0 && !name.trim()) {
      setErrors({ name: 'Tell us what to call you.' })
      return false
    }
    if (step === 1 && !workspace.trim()) {
      setErrors({ workspace: 'Pick a name for your workspace.' })
      return false
    }
    setErrors({})
    return true
  }

  function goNext() {
    if (!validateStep()) return
    if (step === STEPS.length - 1) setDone(true)
    else setStep((s) => s + 1)
  }

  function goBack() {
    setErrors({})
    setStep((s) => Math.max(0, s - 1))
  }

  function reset() {
    setStep(0)
    setName('')
    setWorkspace('')
    setPlan('solo')
    setErrors({})
    setDone(false)
  }

  return (
    <div style={{ maxWidth: 480, margin: '0 auto', padding: 24 }}>
      <Card>
        <Stack gap={5}>
          <Stack gap={2}>
            <Eyebrow>{done ? 'All set' : `Step ${step + 1} of ${STEPS.length} · ${STEPS[step]}`}</Eyebrow>
            <Progress value={pct} tone="mint" label="Setup progress" />
          </Stack>

          {done ? (
            <Stack gap={4}>
              <Heading level={2}>You're all set, {name}</Heading>
              <Text muted>Nothing you typed on the way here was lost — here's what we kept.</Text>
              <Stack gap={2}>
                <Row justify="between" wrap={false}>
                  <Text size="sm" muted>Workspace</Text>
                  <Text size="sm">{workspace}</Text>
                </Row>
                <Row justify="between" wrap={false}>
                  <Text size="sm" muted>Plan</Text>
                  <Text size="sm">{PLAN_OPTIONS.find((o) => o.value === plan)?.label}</Text>
                </Row>
              </Stack>
            </Stack>
          ) : (
            <>
              {step === 0 && (
                <Stack gap={4}>
                  <Heading level={2}>Welcome to Pouf</Heading>
                  <Field label="Your name" error={errors.name}>
                    {(id, describedBy) => (
                      <Input
                        id={id}
                        describedBy={describedBy}
                        value={name}
                        onChange={(v) => {
                          setName(v)
                          if (errors.name) setErrors({})
                        }}
                        placeholder="Ada Lovelace"
                        invalid={!!errors.name}
                      />
                    )}
                  </Field>
                </Stack>
              )}
              {step === 1 && (
                <Stack gap={4}>
                  <Heading level={2}>Name your workspace</Heading>
                  <Field label="Workspace" hint="You can change this later." error={errors.workspace}>
                    {(id, describedBy) => (
                      <Input
                        id={id}
                        describedBy={describedBy}
                        value={workspace}
                        onChange={(v) => {
                          setWorkspace(v)
                          if (errors.workspace) setErrors({})
                        }}
                        placeholder="acme"
                        invalid={!!errors.workspace}
                      />
                    )}
                  </Field>
                </Stack>
              )}
              {step === 2 && (
                <Stack gap={4}>
                  <Heading level={2}>How will you use it?</Heading>
                  <RadioGroup label="Plan" value={plan} onChange={setPlan} options={PLAN_OPTIONS} />
                </Stack>
              )}
            </>
          )}

          {done ? (
            <Button variant="quiet" onClick={reset}>Start over</Button>
          ) : (
            <Row wrap={false}>
              <Button variant="quiet" disabled={step === 0} onClick={goBack}>Back</Button>
              <Spacer />
              {step < STEPS.length - 1 ? (
                <Button onClick={goNext}>Continue</Button>
              ) : (
                <Button tone="mint" onClick={goNext}>Finish</Button>
              )}
            </Row>
          )}
        </Stack>
      </Card>
    </div>
  )
}

More auth & onboarding blocks

Sign in →