packages/codemod/docs/STEPS_MIGRATION.md
The Steps (Stepper) components in Chakra UI v3 have been redesigned to use a compound component pattern with dot notation. This guide covers the automatic transformations and manual considerations when migrating.
The steps transform automatically handles:
useSteps usage and chooses appropriate Root
componentindex to defaultStep in
useSteps callsindex prop on Stepper based
on hook usage| v2 Component | v3 Component | Notes |
|---|---|---|
Stepper | Steps.Root | Root container (direct usage) |
Stepper | Steps.RootProvider | Root with hook context (when using useSteps) |
Step | Steps.Item | Individual step item |
StepIndicator | Steps.Indicator | Step indicator wrapper |
StepStatus | Steps.Status | Status-based rendering |
StepTitle | Steps.Title | Step title text |
StepDescription | Steps.Description | Step description text |
StepSeparator | Steps.Separator | Visual separator between steps |
StepNumber | StepNumber | Step number (not transformed - used in StepStatus) |
StepIcon | StepIcon | Step icon (not transformed - used in StepStatus) |
Before:
<Stepper index={1}>
<Step>
<StepIndicator>
<StepStatus complete={<StepIcon />} incomplete={<StepNumber />} />
</StepIndicator>
<StepTitle>First</StepTitle>
</Step>
</Stepper>
After:
<Steps.Root step={1}>
<Steps.List>
<Steps.Item>
<Steps.Indicator>
<Steps.Status complete={<StepIcon />} incomplete={<StepNumber />} />
</Steps.Indicator>
<Steps.Title>First</Steps.Title>
</Steps.Item>
</Steps.List>
</Steps.Root>
Note:
index prop becomes stepSteps.ListBefore:
const { activeStep } = useSteps({
index: 1,
count: steps.length,
})
return (
<Stepper index={activeStep}>
<Step>
<StepTitle>First</StepTitle>
</Step>
</Stepper>
)
After:
const stepsApi = useSteps({
defaultStep: 1,
count: steps.length,
})
return (
<Steps.RootProvider value={stepsApi}>
<Steps.Item>
<Steps.Title>First</Steps.Title>
</Steps.Item>
</Steps.RootProvider>
)
Note:
useSteps is detected anywhere in the file, Stepper becomes
Steps.RootProviderconst stepsApi = useSteps(...))index prop in useSteps becomes defaultStepSteps.RootProvider via the value propindex prop on Stepper is removed (state is managed by hook)Steps.List wrapper (direct children pattern with provider)const { value, goToNext } = stepsApiBefore:
<Step>
<StepTitle>Contact Info</StepTitle>
<StepDescription>Your details</StepDescription>
</Step>
After:
<Steps.Item>
<Steps.Title>Contact Info</Steps.Title>
<Steps.Description>Your details</Steps.Description>
</Steps.Item>
Before:
<StepIndicator>
<StepStatus complete={<StepIcon />} incomplete={<StepNumber />} />
</StepIndicator>
After:
<Steps.Indicator>
<Steps.Status complete={<StepIcon />} incomplete={<StepNumber />} />
</Steps.Indicator>
Before:
<StepStatus
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
After:
<Steps.Status
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
Note: StepIcon and StepNumber are NOT transformed - they remain as-is
and are used within Steps.Status.
Before:
<StepTitle>First Step</StepTitle>
After:
<Steps.Title>First Step</Steps.Title>
Before:
<StepDescription>Contact Info</StepDescription>
After:
<Steps.Description>Contact Info</Steps.Description>
Before:
<StepSeparator />
After:
<Steps.Separator />
| v2 Prop | v3 Prop | Transformation |
|---|---|---|
index | defaultStep | Property rename |
count | count | No change |
Before:
const { activeStep } = useSteps({
index: 0,
count: 3,
})
After:
const stepsApi = useSteps({
defaultStep: 0,
count: 3,
})
// You can still destructure if needed:
// const { value, goToNext, goToPrev } = stepsApi
The codemod detects useSteps usage anywhere in the file and changes behavior:
Without useSteps:
Stepper → Steps.Rootindex prop → step propSteps.ListWith useSteps:
Stepper → Steps.RootProviderconst stepsApi = useSteps(...))value={stepsApi} prop added to Steps.RootProviderindex prop removed from Stepper (managed by hook)Steps.List wrapperindex → defaultStepBefore (v2):
import {
Box,
Step,
StepDescription,
StepIcon,
StepIndicator,
StepNumber,
StepSeparator,
StepStatus,
StepTitle,
Stepper,
} from "@chakra-ui/react"
const steps = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
{ title: "Third", description: "Select Rooms" },
]
export default function App() {
return (
<Stepper index={1}>
{steps.map((step, index) => (
<Step key={index}>
<StepIndicator>
<StepStatus
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
</StepIndicator>
<Box flexShrink="0">
<StepTitle>{step.title}</StepTitle>
<StepDescription>{step.description}</StepDescription>
</Box>
<StepSeparator />
</Step>
))}
</Stepper>
)
}
After (v3):
import { Box } from "@chakra-ui/react"
const steps = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
{ title: "Third", description: "Select Rooms" },
]
export default function App() {
return (
<Steps.Root step={1}>
<Steps.List>
{steps.map((step, index) => (
<Steps.Item key={index}>
<Steps.Indicator>
<Steps.Status
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
</Steps.Indicator>
<Box flexShrink="0">
<Steps.Title>{step.title}</Steps.Title>
<Steps.Description>{step.description}</Steps.Description>
</Box>
<Steps.Separator />
</Steps.Item>
))}
</Steps.List>
</Steps.Root>
)
}
Before (v2):
import {
Box,
Step,
StepDescription,
StepIcon,
StepIndicator,
StepNumber,
StepSeparator,
StepStatus,
StepTitle,
Stepper,
useSteps,
} from "@chakra-ui/react"
const steps = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
]
export default function App() {
const { activeStep } = useSteps({
index: 0,
count: steps.length,
})
return (
<Stepper index={activeStep}>
{steps.map((step, index) => (
<Step key={index}>
<StepIndicator>
<StepStatus
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
</StepIndicator>
<Box>
<StepTitle>{step.title}</StepTitle>
<StepDescription>{step.description}</StepDescription>
</Box>
<StepSeparator />
</Step>
))}
</Stepper>
)
}
After (v3):
import { Box, useSteps } from "@chakra-ui/react"
const steps = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
]
export default function App() {
const stepsApi = useSteps({
defaultStep: 0,
count: steps.length,
})
return (
<Steps.RootProvider value={stepsApi}>
{steps.map((step, index) => (
<Steps.Item key={index}>
<Steps.Indicator>
<Steps.Status
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
</Steps.Indicator>
<Box>
<Steps.Title>{step.title}</Steps.Title>
<Steps.Description>{step.description}</Steps.Description>
</Box>
<Steps.Separator />
</Steps.Item>
))}
</Steps.RootProvider>
)
}
All props on components are preserved during transformation:
Before:
<Stepper index={1} orientation="vertical" height="400px" gap="0">
<Step>First</Step>
</Stepper>
After:
<Steps.Root step={1} orientation="vertical" height="400px" gap="0">
<Steps.List>
<Steps.Item>First</Steps.Item>
</Steps.List>
</Steps.Root>
Props like orientation, height, gap, size, colorScheme, etc. are
preserved on the transformed components.
The codemod automatically updates imports:
Steps to use compound componentsuseSteps import if it was usedBefore:
import {
Box,
Step,
StepDescription,
StepIcon,
StepIndicator,
StepNumber,
StepSeparator,
StepStatus,
StepTitle,
Stepper,
useSteps,
} from "@chakra-ui/react"
After (without useSteps):
import { Box, Steps } from "@chakra-ui/react"
After (with useSteps):
import { Box, Steps, useSteps } from "@chakra-ui/react"
Note: StepIcon and StepNumber are not imported or transformed - they
remain as JSX components within Steps.Status.
npx @chakra-ui/codemod@latest steps path/to/files
The codemod automatically adds the Steps import to your file:
import { Steps } from "@chakra-ui/react"
If using snippets: You may want to change the import path to your snippet location:
import { Steps } from "@/components/ui/steps"
If using useSteps, verify:
Stepper was transformed to Steps.RootProviderconst stepsApi = useSteps(...))value={stepsApi} prop was added to Steps.RootProviderindex prop was removed from Stepperindex was renamed to defaultStep in the hook callSteps.List wrapper was added (direct children pattern)Note: If you need to access values from the hook (like value, goToNext,
goToPrev), you can destructure them from the result:
const stepsApi = useSteps({ defaultStep: 0, count: 3 })
const { value, goToNext, goToPrev } = stepsApi
If NOT using useSteps, verify:
Stepper was transformed to Steps.Rootindex prop was renamed to stepSteps.ListThese components are NOT transformed by the codemod. They remain as-is and are
typically used within Steps.Status:
<Steps.Status
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
If you need to replace them, do so manually based on your v3 implementation.
Before:
<Stepper orientation="vertical" index={1}>
<Step>First</Step>
<Step>Second</Step>
</Stepper>
After:
<Steps.Root orientation="vertical" step={1}>
<Steps.List>
<Steps.Item>First</Steps.Item>
<Steps.Item>Second</Steps.Item>
</Steps.List>
</Steps.Root>
Before:
<Stepper size="lg" colorScheme="blue" index={0}>
<Step>First</Step>
</Stepper>
After:
<Steps.Root size="lg" colorScheme="blue" step={0}>
<Steps.List>
<Steps.Item>First</Steps.Item>
</Steps.List>
</Steps.Root>
Before:
const steps = [{ title: 'First' }, { title: 'Second' }]
<Stepper index={1}>
{steps.map((step, index) => (
<Step key={index}>
<StepTitle>{step.title}</StepTitle>
</Step>
))}
</Stepper>
After:
const steps = [{ title: 'First' }, { title: 'Second' }]
<Steps.Root step={1}>
<Steps.List>
{steps.map((step, index) => (
<Steps.Item key={index}>
<Steps.Title>{step.title}</Steps.Title>
</Steps.Item>
))}
</Steps.List>
</Steps.Root>
Before:
<Stepper index={0}>
<Step>
<StepIndicator>
<StepStatus
complete={<CheckIcon />}
incomplete={<StepNumber />}
active={<Spinner />}
/>
</StepIndicator>
<StepTitle>Processing</StepTitle>
</Step>
</Stepper>
After:
<Steps.Root step={0}>
<Steps.List>
<Steps.Item>
<Steps.Indicator>
<Steps.Status
complete={<CheckIcon />}
incomplete={<StepNumber />}
active={<Spinner />}
/>
</Steps.Indicator>
<Steps.Title>Processing</Steps.Title>
</Steps.Item>
</Steps.List>
</Steps.Root>
The codemod only transforms Stepper components imported from @chakra-ui/react.
Custom stepper components are not affected:
import { Stepper } from "./custom-stepper"
// This will NOT be transformed
;<Stepper>
<Step>Custom</Step>
</Stepper>
If you have both hook-based and direct usage in the same file, the codemod will
treat ALL Stepper instances as hook-based (using Steps.RootProvider). This may
require manual adjustment:
// File has useSteps somewhere
const { activeStep } = useSteps({ index: 0, count: 3 })
// This will become Steps.RootProvider (may need manual adjustment)
<Stepper index={1}>
<Step>Static Step</Step>
</Stepper>
Fix: Manually change back to Steps.Root with step prop if this Stepper
doesn't use the hook.
Problem: After transformation, Steps.List wrapper is missing.
Solution: This should not happen with the latest codemod. If it does, verify you're using the latest version:
npx @chakra-ui/codemod@latest steps path/to/files
Problem: Steps.Root was used instead of Steps.RootProvider (or vice
versa).
Solution:
useSteps hook → should be Steps.RootProviderSteps.RootuseSteps is imported and called anywhere in the fileProblem: index prop wasn't renamed to step or removed.
Solution:
index should become stepindex should be removed from Stepperindex in useSteps({ index: 0 }) should become defaultStepProblem: Components not found after migration.
Solution: The codemod automatically adds Steps import. If you still see
errors:
Check the import was added:
import { Steps } from "@chakra-ui/react"
If using snippets, update the import path:
import { Steps } from "@/components/ui/steps"
If using useSteps, verify it's imported:
import { Steps, useSteps } from "@chakra-ui/react"
Problem: StepIcon or StepNumber components showing errors.
Solution: These are NOT automatically transformed. Verify they're used
correctly within Steps.Status:
<Steps.Status complete={<StepIcon />} incomplete={<StepNumber />} />
If you need different components, replace them manually based on your v3 implementation.
Problem: Steps.RootProvider is missing the value prop after
transformation.
Solution: This should be automatically added by the codemod. Verify:
const stepsApi = useSteps({ defaultStep: 0, count: 3 })
<Steps.RootProvider value={stepsApi}>
Problem: After transformation, references to destructured values (like
activeStep) are broken.
Solution: The codemod converts destructured patterns to full assignments. Update your code to destructure from the result:
Before:
const { activeStep, goToNext } = useSteps({ index: 0, count: 3 })
// activeStep used in JSX
After:
const stepsApi = useSteps({ defaultStep: 0, count: 3 })
const { value: activeStep, goToNext } = stepsApi
// Now activeStep works again
Stepper → Steps.RootProvidervalue prop added to Steps.RootProviderindex → defaultStep in hook callsStepper → Steps.Rootindex → step propSteps.List wrapper added