Skip to content

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. If nextStepId is omitted, resolves from the flow config’s next field.
  • error(nextStepId?) — Mark the card as errored. Pass a nextStepId to 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>
);
}

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
flowtrueFlowConfig
iconfalseReactNode
titlefalsestring
stepperTitlefalsestring
showStepperHeaderfalsefalseboolean
contentTitlefalsestring
contentSubtitlefalsestring
drawersfalseRecord<string, ComponentType<DrawerComponentProps>>
onCompletefalse(state: Record<string, unknown>) => void
onClosefalse() => void
showRootHeaderfalsetrueboolean
hideHeaderfalsefalseboolean
reactivationPromptfalse{ title: "Go back?", description: "Going back will discard progress..." }{ title: string; description: string }
disableAutoScrollfalsefalseboolean
showStepBadgefalsefalseboolean
hideUpcomingGroupsfalsefalseboolean
hidePredictedStepsfalsefalseboolean
initialEngineStatefalseInitialEngineState
childrenfalseReactNode
classNamefalsestring
stylefalseCSSProperties

Card

A card rendered inline within a step. Shows status indicator, title, optional description, and content.

Prop
Required
Default
Type
titletruestring
descriptionfalsestring
childrentrueReactNode
classNamefalsestring

CardAction

An alert/action component for showing contextual prompts within cards.

Prop
Required
Default
Type
varianttrue'info' | 'warning' | 'danger' | 'success'
messagetruestring
actionLabelfalsestring
onActionfalse() => void
secondaryLabelfalsestring
onSecondaryfalse() => void

useFlowCard

Hook for accessing the flow engine from within a card component.

Prop
Required
Default
Type
statetrueTState (generic type)
statustrue'active' | 'completed' | 'error' | 'skipped'
completetrue(statePatch?: Partial<TState>, nextStepId?: string) => void
errortrue(nextStepId?: string) => void
skiptrue(nextStepId?: string) => void
openDrawertrue(drawerId: string, props?: Record<string, unknown>) => Promise<DrawerResult>

Best Practices

  1. Type Your State — Use TypeScript generics with useFlowCard<StateType>() to get autocomplete for your accumulated state.

  2. One Concern Per Card — Each card should represent a single logical step. If a card is doing too much, split it into multiple cards.

  3. Use next for Linear Flows — Define next in your flow config for the happy path. Only pass explicit nextStepId to complete() or skip() for conditional branching.

  4. Handle Drawer Cancellation — Always check result.success after awaiting openDrawer().

  5. Choose the Right Layout — Use SinglePaneStepper for focused, linear flows. Use DualPaneStepper when users need a persistent stepper rail alongside a larger card workspace.

  6. 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-label on step buttons, and aria-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-motion for 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