Single Pane Stepper
The SinglePaneStepper is a single-column flow component for multi-step guided processes. It combines a vertical stepper timeline with cards rendered inline within each step — all in one scrollable column.
Use this component for linear onboarding flows, setup wizards, and configuration processes where each step follows sequentially. It shares the same FlowConfig and useFlowCard API as DualPaneStepper, so flows can be migrated between layouts with minimal changes.
Step States
Steps render distinct states in the timeline — completed (green check), skipped (arrow), and errored (red). A step group whose failed step is retried and completed shows as completed overall, with the failed step still marked in the timeline. This example settles into a fully resolved flow on load.
Interactive Example
This example demonstrates a full CI pipeline onboarding flow with connector drawers, connectivity checks, delegate installation detours, dynamic steps during code analysis, and pipeline YAML generation.
Usage
import { SinglePaneStepper, useFlowCard } from "@harnessio/ui/components";import type { FlowConfig } from "@harnessio/ui/components";
const flow: FlowConfig = { stepGroups: { configure: { title: "Configure", description: "Set up your project" }, generate: { title: "Generate", description: "Create pipeline" }, }, steps: { "choose-language": { step: "configure", title: "Choose language", component: LanguageCard, next: "generate-pipeline", }, "generate-pipeline": { step: "generate", title: "Generate pipeline", component: GeneratePipelineCard, }, }, initialStep: "choose-language",};
<SinglePaneStepper.Root flow={flow} showStepperHeader stepperTitle="Setup Steps" contentTitle="Pipeline Configuration" onComplete={(state) => console.log("Done!", state)}/>Anatomy
The Single Pane Stepper uses a compound component pattern:
<SinglePaneStepper.Root flow={flowConfig} drawers={{ config: ConfigDrawer }}> {/* Cards are rendered automatically from flowConfig.steps */}</SinglePaneStepper.Root>Cards use the useFlowCard hook to interact with the flow engine:
function MyCard() { const { state, status, complete, error, skip, openDrawer } = useFlowCard();
return ( <SinglePaneStepper.Card title="My Card"> {/* Card content */} </SinglePaneStepper.Card> );}Flow Configuration
The FlowConfig object defines the structure of your flow:
type FlowConfig = GroupedFlowConfig | FlatFlowConfig;
interface GroupedFlowConfig { stepGroups: Record<string, StepGroupConfig>; // Timeline step groups steps: Record<string, GroupedStepConfig>; // Card definitions, one group per step initialStep: string; // First card to show}
interface FlatFlowConfig { // Omit stepGroups entirely for a flat timeline — no top-level grouping, each step renders // directly under the timeline root. steps: Record<string, FlatStepConfig>; initialStep: string;}
interface StepGroupConfig { title: string; description?: string;}
interface GroupedStepConfig { step: string; // Which step group this step belongs to title: string; // Shown in the timeline description?: string; // Shown in the timeline component: ComponentType; // React component to render as a card next?: string; // Next step ID (for routing) terminal?: boolean; // Auto-complete on entry (for summary cards) visualCompleted?: boolean; // Always render step as finished, regardless of actual status — presentation-only hint dynamicNext?: true; // This step's real continuation is decided at runtime, not statically — see showStepBadge below}
interface FlatStepConfig { // No `step` field — a flat step belongs to no group. title: string; description?: string; component: ComponentType; next?: string; terminal?: boolean; visualCompleted?: boolean; dynamicNext?: true;}Cards
Use SinglePaneStepper.Card inside your card components. The card shows a status indicator (active dot, check, error, skip), title, optional description, and content. Completed cards show a restart button on hover.
Pass blockedMessage to show an inline warning (warning-triangle icon + text) when the step cannot proceed — for example, when a preselect card has no valid selection. The card body owns the message; disable Continue in your card content when blocked.
function SelectProviderCard() { const { complete } = useFlowCard(); const [selected, setSelected] = useState<string | null>(null); const blockedMessage = selected ? undefined : 'Select a provider to continue';
return ( <SinglePaneStepper.Card title="Select Provider" description="Choose your version control provider" blockedMessage={blockedMessage} > <ProviderList selected={selected} onSelect={setSelected} /> <Button disabled={!selected} onClick={() => complete({ provider: selected })}> Continue </Button> </SinglePaneStepper.Card> );}useFlowCard Hook
The useFlowCard hook provides access to the flow engine from within a card component:
interface FlowCardContext<TState = Record<string, unknown>> { state: TState; status: CardStatus; // 'active' | 'completed' | 'error' | 'skipped' complete: (statePatch?: Partial<TState>, nextStepId?: string) => void; error: (nextStepId?: string) => void; skip: (nextStepId?: string) => void; openDrawer: ( drawerId: string, props?: Record<string, unknown>, ) => Promise<DrawerResult>;}Methods:
complete(statePatch?, nextStepId?)— Mark the card as completed, merge state, and advance. IfnextStepIdis omitted, resolves from the flow config’snextfield.error(nextStepId?)— Mark the card as errored. Pass anextStepIdto advance while keeping the step red in the timeline (error-and-continue).skip(nextStepId?)— Mark the card as skipped and advance.openDrawer(drawerId, props?)— Open a registered drawer and await the result.
State Management
Each card can read the accumulated state and add to it:
function MyCard() { const { state, complete } = useFlowCard<{ provider?: string; repo?: string; }>();
const handleNext = () => { complete({ repo: "my-repo" }); };}Steps and Status Progression
All declared step groups and their steps render up front in the stepper timeline. As the flow advances, each step’s status updates to reflect its current state (active, completed, error, or skipped) — visibility is not gated on reaching the step.
The next field in your flow config drives default routing when cards complete, but does not preview future steps in the timeline beyond their rendered structure.
Drawer Integration
Register drawers via the drawers prop on SinglePaneStepper.Root and open them from any card:
function ConnectorSetupCard() { const { openDrawer, complete } = useFlowCard();
const handleConnect = async () => { const result = await openDrawer('connector');
if (result.success && result.data) { complete({ connectorRef: result.data.connectorRef }); } };
return ( <SinglePaneStepper.Card title="Connect Provider"> <button onClick={handleConnect}>Connect</button> </SinglePaneStepper.Card> );}Navigation & Reactivation
Forward Navigation
Call complete(statePatch?) to navigate to the next card. The timeline advances and the card stack scrolls to center the active card.
Back Navigation
Users can click completed steps in the timeline to scroll back to earlier cards. When navigating backward to reactivate a card, the reactivation guard shows a confirmation dialog:
<SinglePaneStepper.Root flow={flow} reactivationPrompt={{ title: "Go back?", description: "Going back to this step will discard your progress on subsequent steps." }}/>Review Flows
Pass disableAutoScroll to render a fully resolved flow from the top without chasing the last card — useful for documentation demos and review states:
<SinglePaneStepper.Root flow={flow} disableAutoScroll />CardAction Component
Use CardAction to show contextual alerts or prompts within cards:
<SinglePaneStepper.Card title="Connectivity Check"> <SinglePaneStepper.CardAction variant="warning" message="Cannot reach git provider." actionLabel="Install Delegate" onAction={() => openDrawer('delegate')} secondaryLabel="Skip for now" onSecondary={() => skip()} /></SinglePaneStepper.Card>API Reference
Root
The root component that sets up the flow engine, renders the single-column layout, and manages navigation.
<SinglePaneStepper.Root flow={flowConfig} showStepperHeader stepperTitle="Setup Steps" contentTitle="Configuration" contentSubtitle="Follow the steps below" onClose={() => {}} drawers={{ connector: ConnectorDrawer }} onComplete={(state) => console.log(state)} reactivationPrompt={{ title: "Go back?", description: "Progress will be lost." }} disableAutoScroll={false} hideUpcomingGroups={false} hidePredictedSteps={false}/>Prop | Required | Default | Type |
|---|---|---|---|
| flow | true | FlowConfig | |
| icon | false | ReactNode | |
| title | false | string | |
| stepperTitle | false | string | |
| showStepperHeader | false | false | boolean |
| contentTitle | false | string | |
| contentSubtitle | false | string | |
| drawers | false | Record<string, ComponentType<DrawerComponentProps>> | |
| onComplete | false | (state: Record<string, unknown>) => void | |
| onClose | false | () => void | |
| showRootHeader | false | true | boolean |
| hideHeader | false | false | boolean |
| reactivationPrompt | false | { title: "Go back?", description: "Going back will discard progress..." } | { title: string; description: string } |
| disableAutoScroll | false | false | boolean |
| showStepBadge | false | false | boolean |
| hideUpcomingGroups | false | false | boolean |
| hidePredictedSteps | false | false | boolean |
| initialEngineState | false | InitialEngineState | |
| children | false | ReactNode | |
| className | false | string | |
| style | false | CSSProperties |
Card
A card rendered inline within a step. Shows status indicator, title, optional description, and content.
Prop | Required | Default | Type |
|---|---|---|---|
| title | true | string | |
| description | false | string | |
| children | true | ReactNode | |
| className | false | string |
CardAction
An alert/action component for showing contextual prompts within cards.
Prop | Required | Default | Type |
|---|---|---|---|
| variant | true | 'info' | 'warning' | 'danger' | 'success' | |
| message | true | string | |
| actionLabel | false | string | |
| onAction | false | () => void | |
| secondaryLabel | false | string | |
| onSecondary | false | () => void |
useFlowCard
Hook for accessing the flow engine from within a card component.
Prop | Required | Default | Type |
|---|---|---|---|
| state | true | TState (generic type) | |
| status | true | 'active' | 'completed' | 'error' | 'skipped' | |
| complete | true | (statePatch?: Partial<TState>, nextStepId?: string) => void | |
| error | true | (nextStepId?: string) => void | |
| skip | true | (nextStepId?: string) => void | |
| openDrawer | true | (drawerId: string, props?: Record<string, unknown>) => Promise<DrawerResult> |
Best Practices
-
Type Your State — Use TypeScript generics with
useFlowCard<StateType>()to get autocomplete for your accumulated state. -
One Concern Per Card — Each card should represent a single logical step. If a card is doing too much, split it into multiple cards.
-
Use
nextfor Linear Flows — Definenextin your flow config for the happy path. Only pass explicitnextStepIdtocomplete()orskip()for conditional branching. -
Handle Drawer Cancellation — Always check
result.successafter awaitingopenDrawer(). -
Choose the Right Layout — Use
SinglePaneStepperfor focused, linear flows. UseDualPaneStepperwhen users need a persistent stepper rail alongside a larger card workspace. -
Test Back Navigation — Verify that your flow works correctly when users navigate backward via the timeline.
Accessibility
The Single Pane Stepper implements the following accessibility features:
- ARIA: Uses
aria-current="step"on the active step,aria-labelon step buttons, andaria-live="polite"for step change announcements - Keyboard Navigation: Supports ArrowUp/ArrowDown, Home/End on the stepper; standard tab/focus flow on cards
- Reduced Motion: Respects
prefers-reduced-motionfor card transitions and stepper animations - Focus Management: Scrolls the active card into view when navigating forward
- Semantic HTML: Uses
<nav>for the stepper and proper heading hierarchy in cards