All components solidsubtlexssmmdlg
Steps
Multi-step progress with numbered indicators, completion checks, and connecting separators.
123456789101112131415161718192021222324import { Steps } from "#components/ui/ark-ui/steps.tsx";
const steps = ["Account", "Details", "Review"];
export function Example() { return ( // size/variant/orientation resolve once on Root; parts inherit them via // context — step state, linearity, and keyboard come from the machine. // Indicator and separator restyle themselves per step state. <Steps.Root count={steps.length} defaultStep={1} linear={false} size="md"> <Steps.List> {steps.map((title, index) => ( <Steps.Item key={title} index={index}> <Steps.Trigger> <Steps.Indicator>{index + 1}</Steps.Indicator> <Steps.Title>{title}</Steps.Title> </Steps.Trigger> <Steps.Separator /> </Steps.Item> ))} </Steps.List> </Steps.Root> );}Variants
Sizes
Install
1npx stylexblocks add stepsSource
components/ui/ark-ui/steps.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397import * as stylex from "@stylexjs/stylex";import { Steps as ArkSteps, useStepsItemContext,} from "@ark-ui/react/steps";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles } from "#lib/recipe-props.ts";import { stepsIndicatorIconVars } from '@stylexblocks/ui/slot-recipes/steps.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const stepsRoot = stylex.create({ base: { display: 'flex', width: sizes.full, },})
const stepsRootSizes = stylex.create({ xs: { gap: spacing._2_5, }, sm: { gap: spacing._3, }, md: { gap: spacing._4, }, lg: { gap: spacing._6, },})
const stepsRootOrientations = stylex.create({ vertical: { flexDirection: 'row', height: '100%', }, horizontal: { flexDirection: 'column', width: '100%', },})
const stepsList = stylex.create({ base: { display: 'flex', justifyContent: 'space-between', '--steps-gutter': spacing._3, '--steps-thickness': '2px', },})
const stepsListSizes = stylex.create({ xs: { '--steps-size': sizes._6, '--steps-icon-size': sizes._3_5, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, }, sm: { '--steps-size': sizes._8, '--steps-icon-size': sizes._4, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, }, md: { '--steps-size': sizes._10, '--steps-icon-size': sizes._4, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, lg: { '--steps-size': sizes._12, '--steps-icon-size': sizes._5, fontSize: fontSizes.md, lineHeight: lineHeights.md, },})
const stepsListOrientations = stylex.create({ vertical: { flexDirection: 'column', alignItems: 'flex-start', }, horizontal: { flexDirection: 'row', alignItems: 'center', },})
const stepsTitle = stylex.create({ base: { fontWeight: fontWeights.medium, color: semanticColors.fg, },})
const stepsTitleSizes = stylex.create({ xs: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, md: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, lg: { fontSize: fontSizes.md, lineHeight: lineHeights.md, },})
const stepsDescription = stylex.create({ base: { color: semanticColors.fgMuted, },})
const stepsSeparator = stylex.create({ base: { backgroundColor: semanticColors.border, flex: 1, },})
const stepsSeparatorVariants = stylex.create({ solid: {}, subtle: {},})
const stepsSeparatorOrientations = stylex.create({ vertical: { position: 'absolute', width: 'var(--steps-thickness)', height: '100%', maxHeight: 'calc(100% - var(--steps-size) - var(--steps-gutter) * 2)', top: 'calc(var(--steps-size) + var(--steps-gutter))', insetInlineStart: 'calc(var(--steps-size) / 2 - 1px)', }, horizontal: { width: '100%', height: 'var(--steps-thickness)', marginInline: 'var(--steps-gutter)', },})
const stepsIndicator = stylex.create({ base: { display: 'flex', justifyContent: 'center', alignItems: 'center', flexShrink: 0, borderRadius: radii.full, fontWeight: fontWeights.medium, width: 'var(--steps-size)', height: 'var(--steps-size)', [stepsIndicatorIconVars.flexShrink]: 0, [stepsIndicatorIconVars.width]: 'var(--steps-icon-size)', [stepsIndicatorIconVars.height]: 'var(--steps-icon-size)', },})
const stepsIndicatorVariants = stylex.create({ solid: {}, subtle: {},})
const stepsIndicatorIcon = stylex.create({ base: { flexShrink: stepsIndicatorIconVars.flexShrink, width: stepsIndicatorIconVars.width, height: stepsIndicatorIconVars.height, },})
const stepsItem = stylex.create({ base: { position: 'relative', display: 'flex', flex: '1 0 0', },})
const stepsItemOrientations = stylex.create({ vertical: { alignItems: 'flex-start', }, horizontal: { alignItems: 'center', },})
const stepsTrigger = stylex.create({ base: { display: 'flex', alignItems: 'center', gap: spacing._3, textAlign: 'start', outlineWidth: { default: null, ':focus-visible': '1px', }, outlineOffset: { default: null, ':focus-visible': '2px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, borderRadius: semanticRadii.control, },})
const stepsContent = stylex.create({ base: { outlineWidth: { default: null, ':focus-visible': '1px', }, outlineOffset: { default: null, ':focus-visible': '2px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, },})
export type StepsOrientation = keyof typeof stepsRootOrientations
export type StepsVariant = keyof typeof stepsSeparatorVariants
export type StepsSize = keyof typeof stepsRootSizes
// What the generated recipe doesn't carry: list/trigger browser resets and// the per-state indicator/separator treatment (palette-aware). StyleX keys// those off `data-complete` / `data-current` / `data-last` — the machine// stamps complete/current; the separator wrapper copies `last`.const chrome = stylex.create({ list: { listStyle: "none", margin: 0, padding: 0 }, trigger: { borderWidth: 0, backgroundColor: "transparent", padding: 0, cursor: "pointer", }, indicator: { borderWidth: { default: "2px", ":is([data-complete])": 0, }, borderStyle: "solid", borderColor: { default: semanticColors.border, ":is([data-current])": colorPalette.solid, }, color: { default: semanticColors.fgMuted, ":is([data-current])": colorPalette.fg, ":is([data-complete])": colorPalette.contrast, }, backgroundColor: { default: null, ":is([data-complete])": colorPalette.solid, }, }, separatorComplete: { backgroundColor: { default: null, ":is([data-complete])": colorPalette.solid, }, }, separatorLast: { display: { default: null, ":is([data-last])": "none", }, },});
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axis.// Axis maps are looked up by pluralized axis name (`orientation` →// `orientations`). Nested `icon:` keys are skipped — Icon composes// `stepsIndicatorIcon` at the composition layer.const stepsSlotRecipe = defineSlotRecipe({ slots: { root: { styles: stepsRoot, sizes: stepsRootSizes, orientations: stepsRootOrientations, }, list: { styles: stepsList, sizes: stepsListSizes, orientations: stepsListOrientations, }, title: { styles: stepsTitle, sizes: stepsTitleSizes }, description: { styles: stepsDescription }, separator: { styles: stepsSeparator, variants: stepsSeparatorVariants, orientations: stepsSeparatorOrientations, }, indicator: { styles: stepsIndicator, variants: stepsIndicatorVariants }, item: { styles: stepsItem, orientations: stepsItemOrientations }, trigger: { styles: stepsTrigger }, content: { styles: stepsContent }, }, defaultVariants: { size: "md", variant: "solid", orientation: "horizontal" },});
const { withProvider, withContext } = createSlotRecipeContext(stepsSlotRecipe);
function RootNode( props: React.ComponentProps<typeof ArkSteps.Root> & { "data-orientation"?: "horizontal" | "vertical"; },) { return <ArkSteps.Root orientation={props["data-orientation"]} {...props} />;}
function SeparatorNode(props: React.ComponentProps<typeof ArkSteps.Separator>) { const item = useStepsItemContext(); return ( <ArkSteps.Separator {...props} data-last={item.last ? "" : undefined} /> );}
/** * Steps — Ark UI's steps machine wearing the generated recipe. `<Steps.Root * variant size>` resolves the recipe once and every part inherits it through * context; `orientation` styles the recipe axis and drives the machine. * Behavior (step state, linear progression, keyboard) is the machine's. * Composition per part via `asChild`. */export const Steps = { Root: withProvider(RootNode, "root", "StepsRoot"), List: withContext( ArkSteps.List, (v) => [...resolveSlotStyles(stepsSlotRecipe.slots.list, v), chrome.list], "StepsList", ), Item: withContext(ArkSteps.Item, "item", "StepsItem"), Trigger: withContext( ArkSteps.Trigger, (v) => [...resolveSlotStyles(stepsSlotRecipe.slots.trigger, v), chrome.trigger], "StepsTrigger", ), Indicator: withContext( ArkSteps.Indicator, (v) => [ ...resolveSlotStyles(stepsSlotRecipe.slots.indicator, v), chrome.indicator, ], "StepsIndicator", ), Separator: withContext( SeparatorNode, (v) => [ ...resolveSlotStyles(stepsSlotRecipe.slots.separator, v), chrome.separatorComplete, chrome.separatorLast, ], "StepsSeparator", ), Title: withContext("span", "title", "StepsTitle"), Description: withContext("span", "description", "StepsDescription"), Icon: withContext("svg", () => [stepsIndicatorIcon.base], "StepsIcon"), Content: withContext(ArkSteps.Content, "content", "StepsContent"),};