Stepper

Compact Steps

Small new-project card that walks three steps (name, details, access) with a row of dot indicators and the current step title beside them, for dialogs where a full stepper is too loud.

Free, MIT licenseOpen in Browse
"use client"

import { useId, useState } from "react"

import {
  Stepper,
  StepperContent,
  StepperIndicator,
  StepperItem,
  StepperList,
  StepperTitle,
  StepperTrigger,
} from "@/components/stepper/stepper"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"

// The copy: change text here, not in the markup.
const COPY = {
  title: "New project",
  body: "A name, a description, who can see it.",
  back: "Back",
  next: "Next",
  create: "Create project",
  steps: ["Name", "Details", "Access"],
  name: "Project name",
  desc: "Description",
  access: "Who can see it",
  accessHint: "Everyone in Orbcore Design. Change this per project later.",
  created: "Project created. Opening it now.",
} as const

// The small one: three dots and the current title in the footer of a dialog
// or card, under the content, where a full stepper would be too loud.
export default function CompactSteps() {
  const id = useId()
  const [step, setStep] = useState(0)
  // Local stand-in: a real block posts the fields and routes to the project.
  const [created, setCreated] = useState(false)
  const last = COPY.steps.length - 1
  return (
    <Card className="mx-auto w-full max-w-sm">
      <CardHeader>
        <CardTitle>{COPY.title}</CardTitle>
        <CardDescription>{COPY.body}</CardDescription>
      </CardHeader>
      <CardContent>
        <Stepper value={step} onValueChange={setStep} className="gap-5">
          <StepperContent keepMounted value={0}>
            <Field>
              <FieldLabel htmlFor={`${id}cs-name`}>{COPY.name}</FieldLabel>
              <Input id={`${id}cs-name`} placeholder="Registry refresh" />
            </Field>
          </StepperContent>
          <StepperContent keepMounted value={1}>
            <Field>
              <FieldLabel htmlFor={`${id}cs-desc`}>{COPY.desc}</FieldLabel>
              <Textarea id={`${id}cs-desc`} rows={3} />
            </Field>
          </StepperContent>
          <StepperContent keepMounted value={2}>
            <p className="text-sm font-medium">{COPY.access}</p>
            <p className="text-sm text-muted-foreground" aria-live="polite">
              {created ? COPY.created : COPY.accessHint}
            </p>
          </StepperContent>
          <div className="flex items-center">
            <StepperList className="w-auto gap-1.5">
              {COPY.steps.map((title, i) => (
                <StepperItem key={title} value={i} className="flex-none">
                  <StepperTrigger disabled={created} className="size-auto p-1">
                    <StepperIndicator className="size-2 bg-border ring-0 data-[state=active]:w-5 data-[state=active]:bg-foreground data-[state=active]:ring-0 data-[state=completed]:bg-foreground/50" />
                    <StepperTitle className="sr-only">{title}</StepperTitle>
                  </StepperTrigger>
                </StepperItem>
              ))}
            </StepperList>
            <span className="ml-2 text-xs text-muted-foreground">
              {COPY.steps[step]}
            </span>
          </div>
        </Stepper>
      </CardContent>
      <CardFooter className="justify-between">
        <Button
          variant="outline"
          size="sm"
          disabled={step === 0 || created}
          onClick={() => setStep(step - 1)}
        >
          {COPY.back}
        </Button>
        <Button
          size="sm"
          disabled={created}
          onClick={() => (step < last ? setStep(step + 1) : setCreated(true))}
        >
          {step < last ? COPY.next : COPY.create}
        </Button>
      </CardFooter>
    </Card>
  )
}

Installation

pnpm dlx shadcn add @01orbital/compact-steps
npx shadcn add @01orbital/compact-steps
yarn shadcn add @01orbital/compact-steps
bunx --bun shadcn add @01orbital/compact-steps

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/blocks/stepper/compact-steps/index.tsx, as source you own and edit.

Usage

import CompactSteps from "@/components/blocks/stepper/compact-steps"

export default function Example() {
  return <CompactSteps />
}

Dependencies

Registry pieces it installs with it:

  • button
  • card
  • field
  • input
  • stepper
  • textarea