All blocks

Device Pairing

Discover, pair, configure, and ready a device over an async transition.

Installation

Add it with the shadcn CLI, open it in v0, or read the source.

$ npx shadcn@latest add https://stepperize.com/r/device-pairing.json
Dependencies
  • @stepperize/react
  • lucide-react
Requirements
  • React 18 or later
  • Tailwind CSS

Source

import { defineStepper } from "@stepperize/react";
import { Check, Loader2, Speaker, Wifi } from "lucide-react";
import { useState } from "react";

const { Stepper } = defineStepper([
	{ id: "discover", title: "Discover" },
	{ id: "pair", title: "Pair" },
	{ id: "configure", title: "Configure" },
	{ id: "ready", title: "Ready" },
]);

export function DevicePairingBlock() {
	const [finished, setFinished] = useState(false);

	return (
		<Stepper.Root className="w-full max-w-sm overflow-hidden rounded-xl border bg-background shadow-sm">
			{({ stepper }) => (
				<>
					<div className="relative grid h-32 place-items-center bg-muted/40">
						<Stepper.Content step="discover">
							<span className="relative grid size-16 place-items-center">
								<span className="absolute inset-0 animate-ping rounded-full bg-primary/20" />
								<Wifi className="relative size-9 text-primary" />
							</span>
						</Stepper.Content>
						<Stepper.Content step="pair">
							<Loader2 className="size-9 animate-spin text-primary" />
						</Stepper.Content>
						<Stepper.Content step="configure">
							<Speaker className="size-9 text-primary" />
						</Stepper.Content>
						<Stepper.Content step="ready">
							<span className="grid size-12 place-items-center rounded-full bg-chart-2/15 text-chart-2">
								<Check className="size-6" />
							</span>
						</Stepper.Content>
					</div>

					<div className="p-5">
						<div className="mb-3 flex items-center justify-between">
							<h3 className="text-sm font-semibold">{stepper.current.title}</h3>
							<Stepper.List className="flex gap-1.5">
								<Stepper.Items>
									{(step) => (
										<Stepper.Item key={step.id} step={step.id}>
											<Stepper.Indicator className="block size-1.5 rounded-full transition-colors data-[status=active]:bg-primary data-[status=previous]:bg-primary data-[status=upcoming]:bg-muted" />
										</Stepper.Item>
									)}
								</Stepper.Items>
							</Stepper.List>
						</div>

						<div className="min-h-14 text-sm text-muted-foreground">
							<Stepper.Content step="discover">
								Searching for nearby devices…
							</Stepper.Content>
							<Stepper.Content step="pair">
								Pairing with{" "}
								<b className="text-foreground">Living Room Speaker</b>…
							</Stepper.Content>
							<Stepper.Content step="configure" className="space-y-2">
								<input
									defaultValue="Living Room Speaker"
									className="h-9 w-full rounded-lg border bg-background px-3 text-sm text-foreground outline-none transition-colors focus:border-primary focus:ring-2 focus:ring-primary/20"
								/>
							</Stepper.Content>
							<Stepper.Content step="ready">
								{finished
									? "Setup finished. Living Room Speaker is ready to play."
									: "Your device is connected and ready to use."}
							</Stepper.Content>
						</div>

						<Stepper.Actions className="mt-4 flex justify-between">
							<Stepper.Prev className="inline-flex h-9 items-center rounded-lg border bg-background px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50">
								Back
							</Stepper.Prev>
							{finished ? (
								<button
									type="button"
									onClick={() => {
										setFinished(false);
										stepper.reset();
									}}
									className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
								>
									Pair another
								</button>
							) : stepper.isLast ? (
								<button
									type="button"
									onClick={() => setFinished(true)}
									className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
								>
									Finish pairing
								</button>
							) : (
								<Stepper.Next className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50">
									{stepper.current.id === "discover" ? "Connect" : "Next"}
								</Stepper.Next>
							)}
						</Stepper.Actions>
					</div>
				</>
			)}
		</Stepper.Root>
	);
}

When to use it

Hardware/IoT setup where pairing is an async step with a pending state before configuration.

Accessibility

The pending state is announced; controls disable via `isPending` so users can't double-submit.

Customization

The pairing step awaits real work in `beforeStepChange`; surface progress with `stepper.isPending`.

Related blocks