A four-step flow that creates a workspace (name, URL, team and look) beside a skeleton of the app that becomes the workspace as you type and choose. Built on Onboarding Shell, the Onboarding core step engine, Field, Input and Base UI's Radio Group.
When to use
- The first screen after sign-up in a team product: create the workspace before anything else.
- When the workspace has a URL that must be unique, and you want the check to happen as people type.
- When showing the result live is worth more than a shorter form.
When not to use
- Joining an existing workspace: a list of workspaces with a Join button is enough.
- Settings for a workspace that already exists: a settings page with Field rows.
- A different set of questions: build your own flow on Onboarding Shell.
Installation
npx shadcn add @eased-pro/workspace-setupNeeds the @eased-pro registry and your Pro token in components.json. Create a token and copy the setup from your account .
Usage
import { WorkspaceSetup } from "@/components/eased/workspace-setup/workspace-setup";export function CreateWorkspace() { const router = useRouter(); return ( <WorkspaceSetup className="h-dvh" domain="lumen.app" checkSlug={(slug, signal) => fetch(`/api/slugs/${slug}`, { signal }).then((r) => r.status === 404)} onCreate={(workspace) => api.createWorkspace(workspace)} onInvite={(workspace) => router.push(`/${workspace.slug}/invite`)} onOpen={(workspace) => router.push(`/${workspace.slug}`)} /> );}Examples
Drive it from outside
useWorkspaceSetup returns the controller the component would make. Hold it yourself to prefill, script or test the flow.
const setup = useWorkspaceSetup({ domain: "lumen.app", checkSlug, onCreate });setup.setName("Fernhill");<WorkspaceSetup setup={setup} onInvite={openInvite} />Your own teams and colours
<WorkspaceSetup teams={[{ id: "clinic", label: "Clinic", description: "Patients and rotas", pages: ["Patients", "Rota", "Messages"] }]} colors={[{ id: "sage", label: "Sage", value: "oklch(0.55 0.08 150)" }]}/>API reference
WorkspaceSetup
| Prop | Type | Default | Description |
|---|---|---|---|
domain | string | "lumen.app" | The host the slug hangs off. |
checkSlug | (slug, signal) => Promise<boolean> | always free | Resolves true when the slug is free. Called 350ms after the slug settles; the signal aborts when it changes. When taken, it is asked about -hq, -team and -app and offers the first free one. A rejection says the check failed and holds the step. |
onCreate | (workspace: Workspace) => void | Promise | Runs on Create workspace. A promise shows the button loading; a rejection stays on the last step with its message. | |
onInvite | (workspace) => void | The success screen's hand-off. | |
onOpen | (workspace) => void | The success screen's quieter way out. | |
initial | Partial<WorkspaceDraft> | Prefill, e.g. { name: user.company }. | |
persist | StepsPersist | off | Save progress across reloads. |
teams | TeamKind[] | six teams | { id, label, description, pages: [three sidebar pages] }. |
colors | WorkspaceColor[] | eight colours | { id, label, value }. Pick values a white glyph reads on. |
setup | WorkspaceSetupController | From useWorkspaceSetup; the props above then go to the hook instead. | |
manageFocus | boolean | (() => boolean) | true | See Onboarding Shell. |
className | string | "" | Give it a height. |
Workspace is { name, slug, url, team, color, icon }. The slug follows the name ("Fernhill Labs" → fernhill-labs) until someone edits it; clearing it starts following again.
Also exported: useWorkspaceSetup, WorkspacePreview, WorkspaceBadge, WorkspaceTile, slugify, availabilityOf, TEAM_KINDS and WORKSPACE_COLORS.
Keyboard
| Key | Action |
|---|---|
Enter | Continues when the step is valid, from the field or a team card. |
Escape | Goes back a step. |
| Arrow keys | Move between teams, colours and icons. |
Accessibility
- Each step is a labelled field or radio group; colours and icons are named ("Teal", "Leaf").
- The availability line is part of the field's description, which is a polite live region, so "fernhill is taken" and "lumen.app/fernhill-hq is yours" are read as they settle. The suggestion is a real button.
- The preview is decorative (
aria-hidden): every value it shows is in the form. - Focus moves to each step's heading, and to "Fernhill is ready" at the end.
Motion
The preview is the signature: it mirrors the form with no lag. The name types into the switcher, the tile's colour cross-fades in 200ms and a new icon or first letter swaps with y and blur, the address bar takes the slug, and a new team's pages replace the old in a 35ms stagger. A ring with the field focus glow glides on smooth to whatever the open step changes: the switcher's name, the address bar, the team's pages, then the tile. In the form, the availability icon swaps from spinner to check or cross, and the line under the field swaps with it. Reduced motion: values change in place and the ring jumps.
Notes for AI
Use it as the workspace step of a sign-up for a team product. Wire checkSlug to your API with the signal, onCreate to the create call, and onInvite to your invite flow; that is all a typical app needs.
Don't add your own URL check, debounce or slug derivation; the hook does all three. Don't wrap it in a card or dialog with its own padding: it is a full surface, so give it a height and let it fill.
Related
- Onboarding Shell: the frame and step engine under this flow.
- Onboarding Checklist: what comes after setup, inside the product.