]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
New Stepper component (#41999)
authorMark Otto <markd.otto@gmail.com>
Fri, 9 Jan 2026 03:57:36 +0000 (19:57 -0800)
committerMark Otto <markdotto@gmail.com>
Fri, 9 Jan 2026 04:14:09 +0000 (20:14 -0800)
* New Stepper component

* more

* Improvements to stepper, fix other playgrounds while here

* bump bundlewatch

.bundlewatch.config.json
scss/_stepper.scss [new file with mode: 0644]
scss/bootstrap.scss
site/data/sidebar.yml
site/src/components/shortcodes/ButtonPlayground.astro
site/src/components/shortcodes/DropdownPlacementPlayground.astro
site/src/components/shortcodes/StepperPlayground.astro [new file with mode: 0644]
site/src/content/docs/components/stepper.mdx [new file with mode: 0644]
site/src/types/auto-import.d.ts

index 53a01dd33397fd16bf0dc53ac64c9cbd29c7fae4..6995d6a0aa016125d923939d3d4f8f7ed59c1fd1 100644 (file)
     },
     {
       "path": "./dist/css/bootstrap.css",
-      "maxSize": "38.0 kB"
+      "maxSize": "38.75 kB"
     },
     {
       "path": "./dist/css/bootstrap.min.css",
-      "maxSize": "36.75 kB"
+      "maxSize": "37.25 kB"
     },
     {
       "path": "./dist/js/bootstrap.bundle.js",
diff --git a/scss/_stepper.scss b/scss/_stepper.scss
new file mode 100644 (file)
index 0000000..00041ac
--- /dev/null
@@ -0,0 +1,143 @@
+@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
index 764d9f3149a84be97efc4e6178fc709accffd141..167095d0639561fea2c56915f9f7aa25d8a656a4 100644 (file)
@@ -30,6 +30,7 @@
 @forward "popover";
 @forward "progress";
 @forward "spinner";
+@forward "stepper";
 @forward "toasts";
 @forward "tooltip";
 @forward "transitions";
index ca6b349be9a68a916852b6b2b1b447c7e9220287..3bb8358d07a0814633aa53757a5973e6ca2a19a4 100644 (file)
     - title: Progress
     - title: Scrollspy
     - title: Spinner
+    - title: Stepper
     - title: Toasts
     - title: Toggler
     - title: Tooltip
index 7d8d590430456d896d3327ae4e628fdbd7731410..359e4d011454e2adc35647be0bba48f82f4f8423 100644 (file)
@@ -25,7 +25,7 @@ const rounded = ['default', 'pill', 'square']
   </symbol>
 </svg>
 
-<div class="bg-1 p-3 rounded-3">
+<div class="bg-1 p-3 fs-sm rounded-3">
   <div class="d-flex flex-wrap gap-3">
     <div class="vstack gap-1">
       <label class="form-label fw-semibold mb-0">Color</label>
@@ -38,7 +38,7 @@ const rounded = ['default', 'pill', 'square']
           aria-expanded="false"
           data-color="primary"
         >
-          Primary
+          <span>Primary</span>
           <svg class="bi ms-1" width="16" height="16" aria-hidden="true">
             <use href="#chevron-expand" />
           </svg>
@@ -89,7 +89,7 @@ const rounded = ['default', 'pill', 'square']
           aria-expanded="false"
           data-size=""
         >
-          Medium
+          <span>Medium</span>
           <svg class="bi ms-1" width="16" height="16" aria-hidden="true">
             <use href="#chevron-expand" />
           </svg>
@@ -270,7 +270,8 @@ const rounded = ['default', 'pill', 'square']
         const colorTitle = item.textContent?.trim() || 'Primary'
 
         // Update button text and data attribute
-        colorDropdownButton.textContent = colorTitle
+        const labelSpan = colorDropdownButton.querySelector('span')
+        if (labelSpan) labelSpan.textContent = colorTitle
         colorDropdownButton.dataset.color = colorName
 
         // Update selected state
@@ -301,7 +302,8 @@ const rounded = ['default', 'pill', 'square']
         const sizeLabel = item.textContent?.trim() || 'Medium'
 
         // Update button text and data attribute
-        sizeDropdownButton.textContent = sizeLabel
+        const labelSpan = sizeDropdownButton.querySelector('span')
+        if (labelSpan) labelSpan.textContent = sizeLabel
         sizeDropdownButton.dataset.size = sizeValue
 
         // Update selected state
index 5528d7c9178cea71fef76d5261b48dae8e1c8ef3..e37c08e52c633f7ac4db2ebb5f0c3a3ec7c28a19 100644 (file)
@@ -28,7 +28,7 @@ const logicalPlacements = [
 ]
 ---
 
-<div class="bg-1 p-3 rounded-3 mb-3">
+<div class="bg-1 p-3 fs-sm rounded-3 mb-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">Placement type</label>
@@ -67,7 +67,10 @@ const logicalPlacements = [
           data-placement="bottom-start"
           style="min-width: 160px;"
         >
-          bottom-start
+          <span>bottom-start</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="placement-dropdown">
           {physicalPlacements.map((p) => (
@@ -148,7 +151,8 @@ const logicalPlacements = [
     if (!placementDropdownButton || !previewToggle) return
 
     // Update the placement selector button
-    placementDropdownButton.textContent = placement
+    const labelSpan = placementDropdownButton.querySelector('span')
+    if (labelSpan) labelSpan.textContent = placement
     placementDropdownButton.dataset.placement = placement
 
     // Update active state in dropdown
diff --git a/site/src/components/shortcodes/StepperPlayground.astro b/site/src/components/shortcodes/StepperPlayground.astro
new file mode 100644 (file)
index 0000000..57ebef4
--- /dev/null
@@ -0,0 +1,380 @@
+---
+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">&nbsp;</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>
diff --git a/site/src/content/docs/components/stepper.mdx b/site/src/content/docs/components/stepper.mdx
new file mode 100644 (file)
index 0000000..1840924
--- /dev/null
@@ -0,0 +1,139 @@
+---
+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" />
index 2a0cd6f4a019a7d3dae0c14260816cb173d94ade..3019f6b46cbe8216addc7a6e7beb9e5965f7114d 100644 (file)
@@ -21,6 +21,7 @@ export declare global {
   export const JsDocs: typeof import('@shortcodes/JsDocs.astro').default
   export const Placeholder: typeof import('@shortcodes/Placeholder.astro').default
   export const ScssDocs: typeof import('@shortcodes/ScssDocs.astro').default
+  export const StepperPlayground: typeof import('@shortcodes/StepperPlayground.astro').default
   export const Swatch: typeof import('@shortcodes/Swatch.astro').default
   export const Table: typeof import('@shortcodes/Table.astro').default
 }