--- /dev/null
+@use "sass:map";
+@use "config" as *;
+@use "variables" as *;
+@use "layout/breakpoints" as *;
+@use "mixins/border-radius" as *;
+@use "mixins/box-shadow" as *;
+@use "mixins/gradients" as *;
+@use "mixins/transition" as *;
+
+// scss-docs-start stepper-variables
+$stepper-size: 2rem !default;
+$stepper-gap: 1rem !default;
+$stepper-track-size: .25rem !default;
+$stepper-bg: var(--bg-2) !default;
+$stepper-active-fg: var(--primary-contrast) !default;
+$stepper-active-bg: var(--primary-bg) !default;
+// $stepper-vertical-gap: .5rem !default;
+// scss-docs-end stepper-variables
+
+// scss-docs-start stepper-horizontal-mixin
+@mixin stepper-horizontal() {
+ display: inline-grid;
+ grid-auto-columns: 1fr;
+ grid-auto-flow: column;
+
+ .stepper-item {
+ grid-template-rows: repeat(2, var(--stepper-size));
+ grid-template-columns: auto;
+ justify-items: center;
+
+ &::after {
+ top: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5));
+ right: 0;
+ bottom: auto;
+ left: calc(-50% - var(--stepper-gap));
+ width: auto;
+ height: var(--stepper-track-size);
+ }
+
+ &:last-child::after {
+ right: 50%;
+ }
+ }
+}
+// scss-docs-end stepper-horizontal-mixin
+
+// scss-docs-start stepper-css
+.stepper {
+ // scss-docs-start stepper-css-vars
+ --stepper-size: #{$stepper-size};
+ --stepper-gap: #{$stepper-gap};
+ --stepper-bg: #{$stepper-bg};
+ --stepper-track-size: #{$stepper-track-size};
+ --stepper-active-color: #{$stepper-active-fg};
+ --stepper-active-bg: #{$stepper-active-bg};
+ // scss-docs-end stepper-css-vars
+
+ display: grid;
+ grid-auto-rows: 1fr;
+ grid-auto-flow: row;
+ gap: var(--stepper-gap);
+ padding-left: 0;
+ list-style: none;
+ counter-reset: stepper;
+}
+
+.stepper-item {
+ position: relative;
+ display: grid;
+ grid-template-rows: auto;
+ grid-template-columns: var(--stepper-size) auto;
+ gap: .5rem;
+ place-items: center;
+ justify-items: start;
+ text-align: center;
+ text-decoration: none;
+
+
+ // The counter
+ &::before {
+ position: relative;
+ z-index: 1;
+ display: inline-block;
+ width: var(--stepper-size);
+ height: var(--stepper-size);
+ padding: .5rem;
+ font-weight: 600;
+ line-height: 1;
+ text-align: center;
+ content: counter(stepper);
+ counter-increment: stepper;
+ background-color: var(--stepper-bg);
+ @include border-radius(50%);
+ }
+
+ // Connecting lines
+ &::after {
+ position: absolute;
+ top: calc(var(--stepper-gap) * -1);
+ bottom: 100%;
+ left: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5));
+ width: var(--stepper-track-size);
+ content: "";
+ background-color: var(--stepper-bg);
+ }
+
+ // Avoid sibling selector for easier CSS overrides
+ &:first-child::after {
+ display: none;
+ }
+
+ &.active {
+ &::before,
+ &::after {
+ color: var(--theme-contrast, var(--stepper-active-color));
+ background-color: var(--theme-bg, var(--stepper-active-bg));
+ }
+ }
+}
+
+@each $breakpoint in map.keys($grid-breakpoints) {
+ @include media-breakpoint-up($breakpoint) {
+ $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+
+ .stepper-horizontal#{$infix} {
+ @include stepper-horizontal();
+ }
+ }
+}
+
+// scss-docs-start stepper-overflow
+.stepper-overflow {
+ container-type: inline-size;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ -webkit-overflow-scrolling: touch;
+
+ > .stepper {
+ width: max-content;
+ min-width: 100%;
+ }
+}
+// scss-docs-end stepper-overflow
--- /dev/null
+---
+import { getData } from '@libs/data'
+import Example from '@components/shortcodes/Example.astro'
+
+const breakpoints = getData('breakpoints')
+const orientations = [
+ { value: 'vertical', label: 'Vertical' },
+ { value: 'horizontal', label: 'Horizontal' }
+]
+const stepCounts = [3, 4, 5, 6]
+---
+
+<div class="bg-1 p-3 fs-sm rounded-3">
+ <div class="d-flex flex-wrap gap-3 align-items-end">
+ <div class="vstack gap-1">
+ <label class="form-label fw-semibold mb-0">Orientation</label>
+ <div class="btn-group" role="group" aria-label="Stepper orientation">
+ {orientations.map((orientation) => (
+ <label class="btn-check btn-outline theme-secondary">
+ <input
+ type="radio"
+ name="stepper-orientation"
+ value={orientation.value}
+ checked={orientation.value === 'vertical'}
+ data-orientation={orientation.value}
+ />
+ {orientation.label}
+ </label>
+ ))}
+ </div>
+ </div>
+
+ <div class="vstack gap-1" id="breakpoint-control">
+ <label class="form-label fw-semibold mb-0">Breakpoint</label>
+ <div class="dropdown">
+ <button
+ type="button"
+ class="btn btn-outline theme-secondary dropdown-toggle w-100 justify-content-between"
+ id="stepper-breakpoint-dropdown"
+ data-bs-toggle="dropdown"
+ aria-expanded="false"
+ data-breakpoint=""
+ disabled
+ style="min-width: 120px;"
+ >
+ <span>All sizes</span>
+ <svg class="bi ms-1" width="16" height="16" aria-hidden="true">
+ <use href="#chevron-expand" />
+ </svg>
+ </button>
+ <ul class="dropdown-menu" aria-labelledby="stepper-breakpoint-dropdown">
+ {breakpoints.map((bp) => (
+ <li>
+ <a
+ class:list={['dropdown-item', { 'active': bp.abbr === '' }]}
+ href="#"
+ data-breakpoint={bp.abbr}
+ >
+ {bp.abbr === '' ? 'All sizes' : `${bp.name} (${bp.breakpoint})`}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+
+ <div class="vstack gap-1">
+ <label class="form-label fw-semibold mb-0">Steps</label>
+ <div class="dropdown">
+ <button
+ type="button"
+ class="btn btn-outline theme-secondary dropdown-toggle w-100 justify-content-between"
+ id="stepper-count-dropdown"
+ data-bs-toggle="dropdown"
+ aria-expanded="false"
+ data-count="4"
+ style="min-width: 80px;"
+ >
+ <span>4</span>
+ <svg class="bi ms-1" width="16" height="16" aria-hidden="true">
+ <use href="#chevron-expand" />
+ </svg>
+ </button>
+ <ul class="dropdown-menu" aria-labelledby="stepper-count-dropdown">
+ {stepCounts.map((count) => (
+ <li>
+ <a
+ class:list={['dropdown-item', { 'active': count === 4 }]}
+ href="#"
+ data-count={count}
+ >
+ {count}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+
+ <div class="vstack gap-1">
+ <label class="form-label fw-semibold mb-0">Active step</label>
+ <div class="dropdown">
+ <button
+ type="button"
+ class="btn btn-outline theme-secondary dropdown-toggle w-100 justify-content-between"
+ id="stepper-active-dropdown"
+ data-bs-toggle="dropdown"
+ aria-expanded="false"
+ data-active="2"
+ style="min-width: 80px;"
+ >
+ <span>2</span>
+ <svg class="bi ms-1" width="16" height="16" aria-hidden="true">
+ <use href="#chevron-expand" />
+ </svg>
+ </button>
+ <ul class="dropdown-menu" aria-labelledby="stepper-active-dropdown">
+ {stepCounts.map((count) => (
+ <li>
+ <a
+ class:list={['dropdown-item', { 'active': count === 2 }]}
+ href="#"
+ data-active={count}
+ >
+ {count}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+
+ <div class="vstack gap-1">
+ <label class="form-label fw-semibold mb-0 user-select-none"> </label>
+ <b-checkgroup class="py-2">
+ <div class="switch">
+ <input type="checkbox" value="" id="stepper-fullwidth" switch>
+ </div>
+ <label for="stepper-fullwidth">Full width</label>
+ </b-checkgroup>
+ </div>
+ </div>
+</div>
+
+<Example
+ code={`<ol class="stepper">
+ <li class="stepper-item active">Create account</li>
+ <li class="stepper-item active">Confirm email</li>
+ <li class="stepper-item">Update profile</li>
+ <li class="stepper-item">Finish</li>
+</ol>`}
+ id="stepper-preview"
+/>
+
+<script>
+ const orientationInputs = document.querySelectorAll('input[name="stepper-orientation"]')
+ const breakpointDropdownButton = document.querySelector('#stepper-breakpoint-dropdown') as HTMLButtonElement
+ const breakpointDropdownItems = document.querySelectorAll('#stepper-breakpoint-dropdown + .dropdown-menu .dropdown-item')
+ const countDropdownButton = document.querySelector('#stepper-count-dropdown') as HTMLButtonElement
+ const countDropdownItems = document.querySelectorAll('#stepper-count-dropdown + .dropdown-menu .dropdown-item')
+ const activeDropdownButton = document.querySelector('#stepper-active-dropdown') as HTMLButtonElement
+ const activeDropdownItems = document.querySelectorAll('#stepper-active-dropdown + .dropdown-menu .dropdown-item')
+ const fullwidthSwitch = document.querySelector('#stepper-fullwidth') as HTMLInputElement
+ const breakpointControl = document.querySelector('#breakpoint-control') as HTMLElement
+ const previewContainer = document.querySelector('#stepper-preview') as HTMLElement
+ const codeSnippet = document.querySelector('#stepper-preview')?.closest('.bd-example-snippet')?.querySelector('.highlight code') as HTMLElement
+
+ const stepLabels = ['Create account', 'Confirm email', 'Update profile', 'Finish', 'Complete', 'Done']
+
+ function getOrientation(): string {
+ return (document.querySelector('input[name="stepper-orientation"]:checked') as HTMLInputElement)?.value || 'vertical'
+ }
+
+ function getBreakpoint(): string {
+ return breakpointDropdownButton?.dataset.breakpoint || ''
+ }
+
+ function getStepCount(): number {
+ return parseInt(countDropdownButton?.dataset.count || '4', 10)
+ }
+
+ function getActiveStep(): number {
+ return parseInt(activeDropdownButton?.dataset.active || '2', 10)
+ }
+
+ function isFullWidth(): boolean {
+ return fullwidthSwitch?.checked || false
+ }
+
+ function buildStepperClass(): string {
+ const classes = ['stepper']
+ const orientation = getOrientation()
+ const breakpoint = getBreakpoint()
+ const fullWidth = isFullWidth()
+
+ if (orientation === 'horizontal') {
+ classes.push(`stepper-horizontal${breakpoint}`)
+ }
+
+ if (fullWidth && orientation === 'horizontal') {
+ classes.push('w-100')
+ }
+
+ return classes.join(' ')
+ }
+
+ function generateHTML(): string {
+ const stepCount = getStepCount()
+ const activeStep = getActiveStep()
+ const stepperClass = buildStepperClass()
+
+ let html = `<ol class="${stepperClass}">\n`
+
+ for (let i = 1; i <= stepCount; i++) {
+ const isActive = i <= activeStep
+ const activeClass = isActive ? ' active' : ''
+ const label = stepLabels[i - 1] || `Step ${i}`
+ html += ` <li class="stepper-item${activeClass}">${label}</li>\n`
+ }
+
+ html += '</ol>'
+ return html
+ }
+
+ function updatePreview() {
+ if (!previewContainer) return
+
+ const stepperElement = previewContainer.querySelector('.stepper')
+ if (!stepperElement) return
+
+ const stepCount = getStepCount()
+ const activeStep = getActiveStep()
+ const stepperClass = buildStepperClass()
+
+ // Update stepper classes
+ stepperElement.className = stepperClass
+
+ // Update step items
+ let stepsHtml = ''
+ for (let i = 1; i <= stepCount; i++) {
+ const isActive = i <= activeStep
+ const activeClass = isActive ? ' active' : ''
+ const label = stepLabels[i - 1] || `Step ${i}`
+ stepsHtml += `<li class="stepper-item${activeClass}">${label}</li>`
+ }
+ stepperElement.innerHTML = stepsHtml
+ }
+
+ function updateCodeSnippet() {
+ if (!codeSnippet) return
+
+ const htmlCode = generateHTML()
+ codeSnippet.className = 'language-html'
+ codeSnippet.textContent = htmlCode
+
+ if (typeof window !== 'undefined' && (window as any).Prism) {
+ (window as any).Prism.highlightElement(codeSnippet)
+ }
+ }
+
+ function updateActiveDropdown() {
+ const stepCount = getStepCount()
+ const currentActive = getActiveStep()
+
+ // Update dropdown items visibility
+ activeDropdownItems.forEach((item) => {
+ const value = parseInt((item as HTMLElement).dataset.active || '0', 10)
+ const li = item.parentElement as HTMLElement
+ if (value > stepCount) {
+ li.classList.add('d-none')
+ } else {
+ li.classList.remove('d-none')
+ }
+ })
+
+ // Reset active step if current is higher than step count
+ if (currentActive > stepCount) {
+ activeDropdownButton.dataset.active = String(stepCount)
+ const labelSpan = activeDropdownButton.querySelector('span')
+ if (labelSpan) labelSpan.textContent = String(stepCount)
+ activeDropdownItems.forEach((item) => {
+ const value = parseInt((item as HTMLElement).dataset.active || '0', 10)
+ item.classList.toggle('active', value === stepCount)
+ })
+ }
+ }
+
+ function updateBreakpointState() {
+ const orientation = getOrientation()
+ const isHorizontal = orientation === 'horizontal'
+
+ breakpointDropdownButton.disabled = !isHorizontal
+ breakpointControl.classList.toggle('opacity-50', !isHorizontal)
+
+ // Disable fullwidth when vertical
+ fullwidthSwitch.disabled = !isHorizontal
+ fullwidthSwitch.closest('.vstack')?.classList.toggle('opacity-50', !isHorizontal)
+ }
+
+ function update() {
+ updateBreakpointState()
+ updateActiveDropdown()
+ updatePreview()
+ updateCodeSnippet()
+ }
+
+ // Initialize dropdowns
+ if (breakpointDropdownButton) {
+ const breakpointDropdown = bootstrap.Dropdown.getOrCreateInstance(breakpointDropdownButton)
+
+ breakpointDropdownItems.forEach((item) => {
+ item.addEventListener('click', (e) => {
+ e.preventDefault()
+ const breakpoint = (item as HTMLElement).dataset.breakpoint || ''
+ const label = item.textContent?.trim() || 'All sizes'
+
+ const labelSpan = breakpointDropdownButton.querySelector('span')
+ if (labelSpan) labelSpan.textContent = label
+ breakpointDropdownButton.dataset.breakpoint = breakpoint
+
+ breakpointDropdownItems.forEach((i) => i.classList.remove('active'))
+ item.classList.add('active')
+
+ breakpointDropdown.hide()
+ update()
+ })
+ })
+ }
+
+ if (countDropdownButton) {
+ const countDropdown = bootstrap.Dropdown.getOrCreateInstance(countDropdownButton)
+
+ countDropdownItems.forEach((item) => {
+ item.addEventListener('click', (e) => {
+ e.preventDefault()
+ const count = (item as HTMLElement).dataset.count || '4'
+
+ const labelSpan = countDropdownButton.querySelector('span')
+ if (labelSpan) labelSpan.textContent = count
+ countDropdownButton.dataset.count = count
+
+ countDropdownItems.forEach((i) => i.classList.remove('active'))
+ item.classList.add('active')
+
+ countDropdown.hide()
+ update()
+ })
+ })
+ }
+
+ if (activeDropdownButton) {
+ const activeDropdown = bootstrap.Dropdown.getOrCreateInstance(activeDropdownButton)
+
+ activeDropdownItems.forEach((item) => {
+ item.addEventListener('click', (e) => {
+ e.preventDefault()
+ const active = (item as HTMLElement).dataset.active || '2'
+
+ const labelSpan = activeDropdownButton.querySelector('span')
+ if (labelSpan) labelSpan.textContent = active
+ activeDropdownButton.dataset.active = active
+
+ activeDropdownItems.forEach((i) => i.classList.remove('active'))
+ item.classList.add('active')
+
+ activeDropdown.hide()
+ update()
+ })
+ })
+ }
+
+ orientationInputs.forEach((input) => {
+ input.addEventListener('change', update)
+ })
+
+ fullwidthSwitch?.addEventListener('change', update)
+
+ // Initial update
+ update()
+</script>
--- /dev/null
+---
+title: Stepper
+description: Create timelines, wizards, or step-by-step progress bars. Ideal for shopping carts, sign-up forms, and more.
+toc: true
+---
+
+## Examples
+
+Stepper is built with CSS Grid and `<ol>` elements. By default, steps are displayed vertically. You can transform them into horizontal lists with responsive modifier classes.
+
+### Basic
+
+Here's a simple example of a vertical stepper.
+
+<Example code={`<ol class="stepper">
+ <li class="stepper-item active">Create account</li>
+ <li class="stepper-item active">Confirm email</li>
+ <li class="stepper-item">Update profile</li>
+ <li class="stepper-item">Finish</li>
+ </ol>`} />
+
+### Responsive
+
+Steppers sometimes need to be responsive, so you can use the responsive modifier classes to change them from vertical to horizontal at different breakpoints. Responsive modifier classes are available for `sm`, `md`, `lg`, `xl`, and `2xl` breakpoints.
+
+<Example code={`<ol class="stepper stepper-horizontal-md">
+ <li class="stepper-item active">Create account</li>
+ <li class="stepper-item active">Confirm email</li>
+ <li class="stepper-item">Update profile</li>
+ <li class="stepper-item">Finish</li>
+ </ol>`} />
+
+### Gap
+
+Customize the gap with styles that override the `--bs-stepper-gap` CSS variable.
+
+<Example code={`<ol class="stepper" style="--bs-stepper-gap: 3rem">
+ <li class="stepper-item active">Create account</li>
+ <li class="stepper-item active">Confirm email</li>
+ <li class="stepper-item">Update profile</li>
+ <li class="stepper-item">Finish</li>
+ </ol>`} />
+
+### Variants
+
+<Example code={`<ol class="stepper stepper-horizontal">
+ <li class="stepper-item active theme-accent">Create account</li>
+ <li class="stepper-item">Confirm email</li>
+ <li class="stepper-item active theme-success">Update profile</li>
+ <li class="stepper-item active theme-danger">Finish</li>
+ </ol>`} />
+
+### Overflow
+
+Wrap your horizontal stepper in a `.stepper-overflow` container to enable horizontal scrolling when the stepper overflows its parent. Uses `container-type: inline-size` for container query support as opposed to a viewport-based media query.
+
+<Example code={`<div class="stepper-overflow">
+ <ol class="stepper stepper-horizontal">
+ <li class="stepper-item active">Create account</li>
+ <li class="stepper-item active">Verify email address</li>
+ <li class="stepper-item">Complete profile setup</li>
+ <li class="stepper-item">Add payment method</li>
+ <li class="stepper-item">Review and confirm</li>
+ <li class="stepper-item">Finish onboarding</li>
+ </ol>
+ </div>`} />
+
+## Playground
+
+Experiment with stepper options including orientation, breakpoints, step count, and more.
+
+<StepperPlayground />
+
+### Alignment
+
+Use [text alignment utilities]([[docsref:/utilities/text-alignment]]) (because we use `display: inline-grid`) to align the steps. The inline grid arrangement allows us to keep the steps equal width and ensures the connecting lines are rendered correctly.
+
+<Example code={`<ol class="stepper stepper-horizontal">
+ <li class="stepper-item active">Default stepper</li>
+ <li class="stepper-item active">Confirm email</li>
+ <li class="stepper-item">Update profile</li>
+ <li class="stepper-item">Finish</li>
+ </ol>`} />
+
+<Example class="text-center" code={`<ol class="stepper stepper-horizontal">
+ <li class="stepper-item active">Center stepper</li>
+ <li class="stepper-item active">Confirm email</li>
+ <li class="stepper-item">Update profile</li>
+ <li class="stepper-item">Finish</li>
+ </ol>`} />
+
+<Example class="text-end" code={`<ol class="stepper stepper-horizontal">
+ <li class="stepper-item active">End stepper</li>
+ <li class="stepper-item active">Confirm email</li>
+ <li class="stepper-item">Update profile</li>
+ <li class="stepper-item">Finish</li>
+ </ol>`} />
+
+Apply `.w-100` to the stepper to make it full width. Stepper items will be stretched to fill the available space. Alignment doesn't affect full-width steppers.
+
+<Example code={`<ol class="stepper stepper-horizontal w-100">
+ <li class="stepper-item active">Create account</li>
+ <li class="stepper-item active">Confirm email</li>
+ <li class="stepper-item">Update profile</li>
+ <li class="stepper-item">Finish</li>
+ </ol>`} />
+
+### With anchors
+
+Use anchor elements to build your stepper if it links across multiple pages. Add `role="button"` or use `<button>` elements if you're linking across sections in the same document.
+
+Consider using our [link utilities]([[docsref:/utilities/link]]) for quick color control.
+
+<Example code={`<div class="stepper">
+ <a href="#" role="button" class="stepper-item link-body-emphasis active">Create account</a>
+ <a href="#" role="button" class="stepper-item link-body-emphasis active">Confirm email</a>
+ <a href="#" role="button" class="stepper-item link-secondary">Update profile</a>
+ <a href="#" role="button" class="stepper-item link-secondary">Finish</a>
+ </div>`} />
+
+## CSS
+
+### Variables
+
+Steppers use [CSS variables]([[docsref:/customize/css-variables]]) for easier customization.
+
+<ScssDocs name="stepper-css-vars" file="scss/_stepper.scss" />
+
+### Sass variables
+
+<ScssDocs name="stepper-variables" file="scss/_stepper.scss" />
+
+### Sass mixin
+
+<ScssDocs name="stepper-horizontal-mixin" file="scss/_stepper.scss" />
+
+### Overflow wrapper
+
+<ScssDocs name="stepper-overflow" file="scss/_stepper.scss" />