Skip to content
Facade UI
blockmotion

Steps (motion)

The steps, appearing in order. Here the order of the animation matches the meaning.

Open in v0 (opens in a new tab)

This is the animated version of Steps. The static version looks the same with JavaScript turned off.

Preview

Open full width (opens in a new tab)
Preview width

Installation

Package manager
pnpm dlx shadcn@latest add https://facadeui.dev/r/steps-motion.json

This component needs motion-primitives, steps. The CLI installs them for you.

Usage

This is the code for the preview above. To install it as a file, run npx shadcn@latest add @facade/steps-motion-demo.

demos/steps-motion.tsx
"use client"

/**
 * The `steps` demo, with the motion variant swapped in.
 *
 * Identical props: the motion variant only changes which slot components
 * the static section renders through.
 *
 * `"use client"` is required here, not incidental: the motion variant is a
 * client component, and icon components cannot cross a server-to-client
 * boundary as props. See the note on `IconComponent`.
 */

import { StepsMotion } from "@registry/sections/steps-motion"

import { STEPS } from "./content"

export function Demo() {
  return (
    <StepsMotion
      eyebrow="Getting started"
      title="From nothing to a landing page"
      description="An ordered list, so a screen reader already announces 'step 2 of 3' without the numeral being read twice."
      items={STEPS}
    />
  )
}

Source

These are the files the CLI copies into your project.

components/sections/steps-motion.tsx
"use client"

/**
 * StepsMotion — the numbered sequence with a staggered entrance.
 *
 * Steps arrive in order, which is the one place a stagger carries meaning rather than decoration.
 *
 * A genuine wrapper: it renders the static `Steps` and only swaps the list
 * slots, so no markup, styling or accessibility behaviour is duplicated.
 *
 * The list slot renders an `ol` rather than a `ul`, because the sequence is the
 * content — which is also why the stagger belongs here at all. The item slot
 * still renders an `li`: an `ol` may only contain list items, and a `div` there
 * is invalid markup that axe reports as `list`.
 *
 * Dependencies: react, @/components/motion/slots, @/components/motion/stagger,
 * @/components/sections/steps.
 */

import type { ReactNode } from "react"

import { StaggerListItem } from "@/components/motion/slots"
import { Stagger } from "@/components/motion/stagger"
import { Steps, type StepsProps } from "@/components/sections/steps"

function StaggerOl({
  className,
  children,
}: {
  className?: string
  children?: ReactNode
}) {
  return (
    <Stagger as="ol" className={className}>
      {children}
    </Stagger>
  )
}

export type StepsMotionProps = Omit<StepsProps, "listAs" | "itemAs">

export function StepsMotion(props: StepsMotionProps) {
  return <Steps {...props} listAs={StaggerOl} itemAs={StaggerListItem} />
}

Props

StepsMotionProps

Extends Omit<StepsProps, "listAs" | "itemAs">.