---
name: stepper
title: Stepper
url: https://ui.frontic.com/components/ui/stepper.md
tier: ui
install: npx @frontic/ui add stepper
files:
  - components/ui/stepper/Stepper.vue
  - components/ui/stepper/StepperDescription.vue
  - components/ui/stepper/StepperIndicator.vue
  - components/ui/stepper/StepperItem.vue
  - components/ui/stepper/StepperSeparator.vue
  - components/ui/stepper/StepperTitle.vue
  - components/ui/stepper/StepperTrigger.vue
  - components/ui/stepper/index.ts
dependencies:
  - "@vueuse/core"
  - class-variance-authority
  - reka-ui
---

# Stepper

Numbered steps through a flow such as checkout, marking done, current and upcoming, joined by a line that fills as each step is done; spans its row, and on a narrow screen names only the current step.

## Install

```sh
npx @frontic/ui add stepper
```

Used by: [Order Tracking](https://ui.frontic.com/components/sections/order-tracking.md), [Header Checkout](https://ui.frontic.com/components/templates/header-checkout.md)

npm packages: `@vueuse/core`, `class-variance-authority`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { CheckIcon } from '@lucide/vue'
import { Stepper, StepperDescription, StepperIndicator, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/components/ui/stepper'

const steps = [
  { step: 1, title: 'Cart', description: '2 items' },
  { step: 2, title: 'Address', description: 'Where it ships' },
  { step: 3, title: 'Payment', description: 'How you pay' },
  { step: 4, title: 'Review', description: 'Check and confirm' },
]
</script>

<template>
  <Stepper :default-value="3" aria-label="Checkout" class="w-3xl max-w-full">
    <StepperItem v-for="item in steps" :key="item.step" v-slot="{ state }" :step="item.step">
      <StepperTrigger>
        <StepperIndicator>
          <CheckIcon v-if="state === 'completed'" data-icon />
          <template v-else>{{ item.step }}</template>
        </StepperIndicator>
        <StepperTitle>{{ item.title }}</StepperTitle>
        <StepperDescription>{{ item.description }}</StepperDescription>
      </StepperTrigger>
      <StepperSeparator v-if="item.step !== steps.length" />
    </StepperItem>
  </Stepper>
</template>
```

## Anatomy

```vue
<Stepper>
  <StepperItem>
    <StepperTrigger>
      <StepperIndicator />
      <StepperTitle />
      <StepperDescription />
    </StepperTrigger>
    <StepperSeparator />
  </StepperItem>
</Stepper>
```

## API reference

### Stepper

Groups all parts of the stepper, which guides the user through a sequence of steps.

`data-slot="stepper"` · `Stepper.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultValue` | `number` | `1` | The value of the step that should be active when initially rendered. Use when you do not need to control the state of the steps. |
| `orientation` | `DataOrientation`: `'vertical' \| 'horizontal'` | `'horizontal'` | The orientation the steps are laid out. Mainly so arrow navigation is done accordingly (left & right vs. up & down). |
| `dir` | `Direction`: `'ltr' \| 'rtl'` |  | The reading direction of the combobox when applicable.<br><br>If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. |
| `modelValue` | `number` |  | The controlled value of the step to activate. Can be bound as `v-model`. |
| `linear` | `boolean` | `true` | Whether or not the steps must be completed in order. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `statusLabel` | `((step: number, total: number) => string)`: `(step: number, total: number): string` | ``Step ${step} of ${total}`` | Says where the user is when the row is too narrow to name every step. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:modelValue` | `payload: number \| undefined` | Event handler called when the value changes |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ modelValue: number \| undefined; totalSteps: number; isNextDisabled: boolean; isPrevDisabled: boolean; isFirstStep: boolean; isLastStep: boolean; goToStep: (step: number) => void; nextStep: () => void; prevStep: () => void; hasNext: () => boolean; hasPrev: () => boolean; }` | Holds the steps; gets the current step, the number of steps, where the user is in the sequence and functions that move between steps. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-horizontal` |  | Present when the part is laid out horizontally. |
| `data-linear` |  | Present when the steps must be completed in order. |
| `data-orientation` | `'vertical' \| 'horizontal'` | Carries the `orientation` prop. |
| `data-vertical` |  | Present when the part is laid out vertically. |

### StepperDescription

Explains in a short line what happens in a step. Renders a `<p>`.

`data-slot="stepper-description"` · `StepperDescription.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'p'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### StepperIndicator

Marks the number or progress state of a step. Renders a `<span>`.

`data-slot="stepper-indicator"` · `StepperIndicator.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ step: number; }` | Holds what the indicator shows, such as the step number or an icon; gets the step's number. |

### StepperItem

Groups the trigger, title and description of one step in the stepper.

`data-slot="stepper-item"` · `StepperItem.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `step` (required) | `number` |  | A unique value that associates the stepper item with an index |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the step. |
| `completed` | `boolean` | `false` | Shows whether the step is completed. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ state: StepperState; }` | Holds the step's trigger, title and description; gets whether the step is inactive, active or completed. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |
| `data-orientation` | `'horizontal' \| 'vertical'` | The direction the part is laid out in. |
| `data-state` | `'active' \| 'completed' \| 'inactive'` | The state the part is in, one value per state. |

### StepperSeparator

Joins one step of the stepper to the next with a line that fills once the step is done.

`data-slot="stepper-separator"` · `StepperSeparator.vue`

Styling: `stepper-separator`, which extends [`divider`](https://ui.frontic.com/DESIGN.md#divider). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `DataOrientation`: `'vertical' \| 'horizontal'` |  | Orientation of the component.<br><br>Either `vertical` or `horizontal`. Defaults to `horizontal`. |
| `decorative` | `boolean` | `false` | Whether or not the component is purely decorative.<br><br>When `true`, accessibility-related attributes are updated so that that the rendered element is removed from the accessibility tree. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-state` | `'active' \| 'completed' \| 'inactive'` | The state the part is in, one value per state. |

### StepperTitle

Names a step of the stepper. Renders a `<h4>`.

`data-slot="stepper-title"` · `StepperTitle.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'h4'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-current` |  | Present on the current step's title, the one a narrow row still shows. |

### StepperTrigger

Lets the user jump to its step. Renders a `<button>`.

`data-slot="stepper-trigger"` · `StepperTrigger.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |
| `data-orientation` | `'horizontal' \| 'vertical'` | The direction the part is laid out in. |
| `data-state` | `'active' \| 'completed' \| 'inactive'` | The state the part is in, one value per state. |

## Styling

| Part | Recipe |
| --- | --- |
| `separator-root` | [`stepper-separator-root`](https://ui.frontic.com/DESIGN.md#stepper-separator-root) |
| `item` | [`stepper-item`](https://ui.frontic.com/DESIGN.md#stepper-item) |
| `default` | [`stepper`](https://ui.frontic.com/DESIGN.md#stepper) |
| `trigger` | [`stepper-trigger`](https://ui.frontic.com/DESIGN.md#stepper-trigger) |
| `indicator` | [`stepper-indicator`](https://ui.frontic.com/DESIGN.md#stepper-indicator) |
| `separator` | [`stepper-separator`](https://ui.frontic.com/DESIGN.md#stepper-separator) |
| `separator-fill` | [`stepper-separator-fill`](https://ui.frontic.com/DESIGN.md#stepper-separator-fill) |
| `title` | [`stepper-title`](https://ui.frontic.com/DESIGN.md#stepper-title) |
| `description` | [`stepper-description`](https://ui.frontic.com/DESIGN.md#stepper-description) |
| `status` | [`stepper-status`](https://ui.frontic.com/DESIGN.md#stepper-status) |
| `focus-ring` | [`focus-ring`](https://ui.frontic.com/DESIGN.md#focus-ring) |

States with a look of their own: focus and disabled.

## Source

`components/ui/stepper/Stepper.vue`

```vue
<script lang="ts">
/** Groups all parts of the stepper, which guides the user through a sequence of steps. */
export default {}
</script>

<script setup lang="ts">
import type { StepperRootEmits, StepperRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { StepperRoot, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'

const props = withDefaults(
  defineProps<
    StepperRootProps & {
      class?: HTMLAttributes['class']
      /** Says where the user is when the row is too narrow to name every step. */
      statusLabel?: (step: number, total: number) => string
    }
  >(),
  { statusLabel: (step: number, total: number) => `Step ${step} of ${total}` },
)
const emits = defineEmits<StepperRootEmits>()

const delegatedProps = reactiveOmit(props, 'class', 'statusLabel')
const forwarded = useForwardPropsEmits(delegatedProps, emits)

defineSlots<{
  /** Holds the steps; gets the current step, the number of steps, where the user is in the sequence and functions that move between steps. */
  default?: (props: {
    modelValue: number | undefined
    totalSteps: number
    isNextDisabled: boolean
    isPrevDisabled: boolean
    isFirstStep: boolean
    isLastStep: boolean
    goToStep: (step: number) => void
    nextStep: () => void
    prevStep: () => void
    hasNext: () => boolean
    hasPrev: () => boolean
  }) => unknown
}>()
const vertical = computed(() => props.orientation === 'vertical')
</script>

<template>
  <StepperRoot
    v-slot="slotProps"
    data-slot="stepper"
    :data-horizontal="vertical ? undefined : ''"
    :data-vertical="vertical ? '' : undefined"
    :class="['fui-stepper group/stepper @container relative gap-y-3 data-vertical:flex-col data-vertical:flex-nowrap data-vertical:gap-y-0', props.class]"
    v-bind="forwarded"
  >
    <span v-if="!vertical && slotProps.modelValue !== undefined" data-slot="stepper-status" aria-hidden="true" class="fui-stepper-status order-last basis-full text-end @md:hidden">
      {{ props.statusLabel(slotProps.modelValue, slotProps.totalSteps) }}
    </span>
    <slot v-bind="slotProps" />
  </StepperRoot>
</template>
```

`components/ui/stepper/StepperDescription.vue`

```vue
<script lang="ts">
/** Explains in a short line what happens in a step. */
export default {}
</script>

<script setup lang="ts">
import type { StepperDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { StepperDescription, useForwardProps } from 'reka-ui'

const props = defineProps<StepperDescriptionProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <StepperDescription
    v-slot="slotProps"
    data-slot="stepper-description"
    v-bind="forwarded"
    :class="[
      'fui-stepper-description group-data-horizontal/stepper:@max-md:sr-only group-data-vertical/stepper:col-start-1 group-data-vertical/stepper:row-start-2 group-data-vertical/stepper:translate-y-0.5',
      props.class,
    ]"
  >
    <slot v-bind="slotProps" />
  </StepperDescription>
</template>
```

`components/ui/stepper/StepperIndicator.vue`

```vue
<script lang="ts">
/** Marks the number or progress state of a step. */
export default {}
</script>

<script setup lang="ts">
import type { StepperIndicatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { cva } from 'class-variance-authority'
import { injectStepperItemContext, StepperIndicator, useForwardProps } from 'reka-ui'
import { computed } from 'vue'

const stepperIndicator = cva('fui-stepper-indicator', {
  variants: {
    state: {
      inactive: 'fui-stepper-indicator-inactive',
      active: 'fui-stepper-indicator-active',
      completed: 'fui-stepper-indicator-completed',
    },
  },
  defaultVariants: { state: 'inactive' },
})

const props = defineProps<StepperIndicatorProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
const item = injectStepperItemContext()
const state = computed(() => item.state.value)

defineSlots<{
  /** Holds what the indicator shows, such as the step number or an icon; gets the step's number. */
  default?: (props: { step: number }) => unknown
}>()
</script>

<template>
  <StepperIndicator
    v-slot="slotProps"
    data-slot="stepper-indicator"
    v-bind="forwarded"
    :class="[stepperIndicator({ state }), 'relative z-10 group-data-vertical/stepper:col-end-1 group-data-vertical/stepper:row-span-2 group-data-vertical/stepper:row-start-1', props.class]"
  >
    <slot v-bind="slotProps" />
  </StepperIndicator>
</template>
```

`components/ui/stepper/StepperItem.vue`

```vue
<script lang="ts">
/** Groups the trigger, title and description of one step in the stepper. */
export default {}
</script>

<script setup lang="ts">
import type { StepperItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { StepperItem, useForwardProps } from 'reka-ui'

const props = defineProps<StepperItemProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)

type StepperState = 'completed' | 'active' | 'inactive'

defineSlots<{
  /** Holds the step's trigger, title and description; gets whether the step is inactive, active or completed. */
  default?: (props: { state: StepperState }) => unknown
}>()
</script>

<template>
  <StepperItem
    v-slot="slotProps"
    data-slot="stepper-item"
    v-bind="forwarded"
    :class="[
      'fui-stepper-item group data-disabled:pointer-events-none group-data-horizontal/stepper:@md:items-start group-data-horizontal/stepper:has-[[data-slot=stepper-separator]]:flex-1 group-data-horizontal/stepper:@md:relative group-data-horizontal/stepper:@md:flex-1 group-data-horizontal/stepper:@md:justify-center group-data-vertical/stepper:relative group-data-vertical/stepper:flex-col group-data-vertical/stepper:items-stretch group-data-vertical/stepper:has-[[data-slot=stepper-separator]]:pb-3',
      props.class,
    ]"
  >
    <slot v-bind="slotProps" />
  </StepperItem>
</template>
```

`components/ui/stepper/StepperSeparator.vue`

```vue
<script lang="ts">
/** Joins one step of the stepper to the next with a line that fills once the step is done. */
export default {}
</script>

<script setup lang="ts">
import type { StepperSeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { injectStepperItemContext, StepperSeparator, useForwardProps } from 'reka-ui'
import { computed } from 'vue'

const props = defineProps<StepperSeparatorProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
const item = injectStepperItemContext()
const filled = computed(() => item.state.value === 'completed')
</script>

<template>
  <StepperSeparator
    data-slot="stepper-separator"
    v-bind="forwarded"
    :class="[
      'fui-stepper-separator-root relative group-data-horizontal/stepper:@md:absolute group-data-horizontal/stepper:@md:inset-s-1/2 group-data-horizontal/stepper:@md:top-5 group-data-horizontal/stepper:@md:mx-0 group-data-horizontal/stepper:@md:w-full group-data-vertical/stepper:absolute group-data-vertical/stepper:inset-s-5 group-data-vertical/stepper:top-5 group-data-vertical/stepper:mx-0 group-data-vertical/stepper:h-full group-data-vertical/stepper:w-px',
      props.class,
    ]"
  >
    <span data-slot="stepper-separator-track" class="fui-stepper-separator absolute inset-0">
      <span
        data-slot="stepper-separator-fill"
        :data-filled="filled ? '' : undefined"
        class="fui-stepper-separator-fill absolute inset-0 origin-left scale-x-0 transition-transform duration-500 ease-out motion-reduce:transition-none data-filled:scale-x-100 group-data-horizontal/stepper:rtl:origin-right group-data-vertical/stepper:origin-top group-data-vertical/stepper:scale-x-100 group-data-vertical/stepper:scale-y-0 group-data-vertical/stepper:data-filled:scale-y-100"
      />
    </span>
  </StepperSeparator>
</template>
```

`components/ui/stepper/StepperTitle.vue`

```vue
<script lang="ts">
/** Names a step of the stepper. */
export default {}
</script>

<script setup lang="ts">
import type { StepperTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { cva } from 'class-variance-authority'
import { injectStepperItemContext, StepperTitle, useForwardProps } from 'reka-ui'
import { computed } from 'vue'

const stepperTitle = cva('fui-stepper-title whitespace-nowrap', {
  variants: {
    state: {
      inactive: 'fui-stepper-title-inactive',
      active: 'fui-stepper-title-active',
      completed: 'fui-stepper-title-completed',
    },
  },
  defaultVariants: { state: 'inactive' },
})

const props = defineProps<StepperTitleProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
const item = injectStepperItemContext()
const state = computed(() => item.state.value)
const current = computed(() => state.value === 'active')
</script>

<template>
  <StepperTitle
    data-slot="stepper-title"
    :data-current="current ? '' : undefined"
    v-bind="forwarded"
    :class="[
      stepperTitle({ state }),
      'group-data-horizontal/stepper:@max-md:not-data-current:sr-only group-data-horizontal/stepper:@max-md:data-current:absolute group-data-horizontal/stepper:@max-md:data-current:inset-s-0 group-data-horizontal/stepper:@max-md:data-current:bottom-0 group-data-vertical/stepper:col-start-1 group-data-vertical/stepper:row-start-1 group-data-vertical/stepper:translate-y-1.5',
      props.class,
    ]"
  >
    <slot />
  </StepperTitle>
</template>
```

`components/ui/stepper/StepperTrigger.vue`

```vue
<script lang="ts">
/** Lets the user jump to its step. */
export default {}
</script>

<script setup lang="ts">
import type { StepperTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { StepperTrigger, useForwardProps } from 'reka-ui'

const props = defineProps<StepperTriggerProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <StepperTrigger
    data-slot="stepper-trigger"
    v-bind="forwarded"
    :class="[
      'fui-stepper-trigger outline-none focus-visible:fui-focus-ring group-data-vertical/stepper:grid group-data-vertical/stepper:auto-cols-min group-data-vertical/stepper:grid-cols-1 group-data-vertical/stepper:items-start group-data-vertical/stepper:text-start',
      props.class,
    ]"
  >
    <slot />
  </StepperTrigger>
</template>
```
