]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Refine Stepper component (#42165)
authorpricop <pricop.info@gmail.com>
Wed, 18 Mar 2026 02:26:23 +0000 (04:26 +0200)
committerGitHub <noreply@github.com>
Wed, 18 Mar 2026 02:26:23 +0000 (19:26 -0700)
* Add stepper font size variable to tokens map

Since the `--stepper-size` container is customizable, it makes sense for the `font-size` to be as well, when using larger sizes, larger font size may also be required.

This also changes the default `font-size` to `-sm` size for the counter inside the bubble.

This is the perfect example where if we use the `-sm` variant, it makes the text feel more relaxed inside the bubble.

* Reduce stepper track size from 0.25rem to 0.125rem

This further relaxes the component by taking away the attention from the track itself, allowing bubbles and text to be more easily readable at a glance without feeling overcrowded.

 The industry standard is 1px or 2px at max.

As a result, the stepper tracker has been reduced to down to `2px`  from `4px`

For reference:
- Tailwind UI practices `2px` width: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/progress-bars#component-4b1efed043d1ab5688c705f2e27524f3
- Material UI practices `1px` width: https://material.angular.dev/components/stepper/overview
- Preline UI practices `1px` width: https://preline.co/docs/stepper.html
- Flowbite UI practices `1px` width: https://flowbite.com/docs/components/stepper/
- ReUI practices `1px` width: https://reui.io/patterns/stepper

* Add align-items start to horizontal stepper

This will make the text to always start at the top, regardless of whether other bubbles have a multi-line text or not.

* Fixed horizontal stepper text gap & text overflow

Fixes the horizontal bottom row being fixed in height, causing the spacing between the count bubble and the text not be the same as intended (as the gap dictates), nor as its vertical stepper counterpart.

This also fixes the text overflowing outside fixed boundaries, causing it to move in places it shouldn't be.

* Refactor to use logical properties

Fixed RTL mode by making use of logical properties instead of physical ones.

* Colored the right side stepper line

Colored the right side stepper line instead of the left side for active elements.

This is the natural way, if I reached the next step, then the edge leading to it should be colored in the color of the completed step.

* Fixed stepper width not account for stepper's gap

Fixed stepper width not account for stepper's gap

* Added complex stepper support for vertical steppers

- Added complex stepper support for vertical steppers.
- Added --stepper-text-gap CSS var (customizable spacing between text & bubble)
- Fixed text overflow for vertical steppers when the text is too long

* Restored 1fr for the rows template

Restored 1fr for the rows template. While I believed it looked better before, I'd rather keep the row size across all steps.

* tweaks

* make container query instead, add new utility to make container queries easier

* Remove stepper playground

---------

Co-authored-by: Mark Otto <markdotto@gmail.com>
scss/_stepper.scss
scss/_utilities.scss
site/src/components/shortcodes/StepperPlayground.astro [deleted file]
site/src/content/docs/components/stepper.mdx

index 9ba2921ed22ec6805f830b11e327de6d0d62eff6..ba0322cb0133f108d9fdd42e334a9f5e1f13c5b8 100644 (file)
@@ -1,4 +1,3 @@
-@use "sass:map";
 @use "config" as *;
 @use "functions" as *;
 @use "layout/breakpoints" as *;
@@ -13,8 +12,10 @@ $stepper-tokens: defaults(
   (
     --stepper-size: 2rem,
     --stepper-gap: 1rem,
+    --stepper-font-size: var(--font-size-sm),
+    --stepper-text-gap: .5rem,
+    --stepper-track-size: .125rem,
     --stepper-bg: var(--bg-2),
-    --stepper-track-size: .25rem,
     --stepper-active-color: var(--primary-contrast),
     --stepper-active-bg: var(--primary-bg),
   ),
@@ -29,117 +30,127 @@ $stepper-tokens: defaults(
   grid-auto-flow: column;
 
   .stepper-item {
-    grid-template-rows: repeat(2, var(--stepper-size));
+    grid-template-rows: var(--stepper-size) auto;
     grid-template-columns: auto;
+    align-items: start;
     justify-items: center;
+    text-align: 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;
+      inset-block-start: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5));
+      inset-block-end: auto;
+      inset-inline-start: 50%;
+      inset-inline-end: 100%;
+      width: calc(100% + var(--stepper-gap));
       height: var(--stepper-track-size);
     }
 
     &:last-child::after {
-      right: 50%;
+      right: 100%;
     }
   }
 }
 // scss-docs-end stepper-horizontal-mixin
 
-// scss-docs-start stepper-css
-.stepper {
-  // scss-docs-start stepper-css-vars
-  @include tokens($stepper-tokens);
-  // 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;
-}
+@layer components {
+  .stepper {
+    @include tokens($stepper-tokens);
+
+    display: grid;
+    grid-auto-rows: 1fr;
+    grid-auto-flow: row;
+    gap: var(--stepper-gap);
+    padding-inline-start: 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 {
+  .stepper-item {
     position: relative;
-    z-index: 1;
-    display: flex;
-    flex-shrink: 0;
+    display: grid;
+    grid-template-rows: auto;
+    grid-template-columns: var(--stepper-size) auto;
+    gap: var(--stepper-text-gap);
     align-items: center;
-    justify-content: center;
-    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%);
+    text-decoration: none;
+
+    // The counter
+    &::before {
+      position: relative;
+      z-index: 1;
+      display: flex;
+      flex-shrink: 0;
+      align-items: center;
+      justify-content: center;
+      width: var(--stepper-size);
+      height: var(--stepper-size);
+      padding: .5rem;
+      font-size: var(--stepper-font-size);
+      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;
+      inset-block-start: 50%;
+      inset-block-end: 100%;
+      inset-inline-start: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5));
+      width: var(--stepper-track-size);
+      height: calc(100% + var(--stepper-gap));
+      content: "";
+      background-color: var(--stepper-bg);
+    }
+
+    // Avoid sibling selector for easier CSS overrides
+    &:last-child::after {
+      display: none;
+    }
+
+    &.active {
+      &::before,
+      &::after {
+        color: var(--theme-contrast, var(--stepper-active-color));
+        background-color: var(--theme-bg, var(--stepper-active-bg));
+      }
+    }
   }
 
-  // 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: "";
+  // Targets the last .active element from a sequence of active elements
+  .stepper-item.active:not(:has(+ .stepper-item.active))::after {
     background-color: var(--stepper-bg);
   }
 
-  // Avoid sibling selector for easier CSS overrides
-  &:first-child::after {
-    display: none;
+  .stepper-horizontal {
+    @include stepper-horizontal();
   }
 
-  &.active {
-    &::before,
-    &::after {
-      color: var(--theme-contrast, var(--stepper-active-color));
-      background-color: var(--theme-bg, var(--stepper-active-bg));
+  @include loop-breakpoints-down() using ($breakpoint, $next, $infix) {
+    @if $next {
+      .stepper-horizontal#{$infix} {
+        @include container-breakpoint-up($next) {
+          @include stepper-horizontal();
+        }
+      }
     }
   }
-}
 
-@each $breakpoint in map.keys($breakpoints) {
-  @include media-breakpoint-up($breakpoint) {
-    $infix: breakpoint-infix($breakpoint, $breakpoints);
+  // scss-docs-start stepper-overflow
+  .stepper-overflow {
+    container-type: inline-size;
+    overflow-x: auto;
+    overscroll-behavior-x: contain;
+    -webkit-overflow-scrolling: touch;
 
-    .stepper-horizontal#{$infix} {
-      @include stepper-horizontal();
+    > .stepper {
+      width: max-content;
+      min-width: 100%;
     }
   }
+  // scss-docs-end stepper-overflow
 }
-
-// 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 a25cafa49d6f2e699412e7fecdda4ea84ea16543..39211802d2540e31b085c5841b47ce4197cfefeb 100644 (file)
@@ -86,6 +86,14 @@ $utilities: map.merge(
       values: auto hidden visible scroll,
     ),
     // scss-docs-end utils-overflow
+    "container": (
+      property: container-type,
+      class: contains,
+      values: (
+        "inline": inline-size,
+        "size": size,
+      )
+    ),
     // scss-docs-start utils-display
     "display": (
       responsive: true,
diff --git a/site/src/components/shortcodes/StepperPlayground.astro b/site/src/components/shortcodes/StepperPlayground.astro
deleted file mode 100644 (file)
index e30c7cf..0000000
+++ /dev/null
@@ -1,378 +0,0 @@
----
-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-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-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-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>
-      <div class="checkgroup py-2">
-        <div class="switch">
-          <input type="checkbox" value="" id="stepper-fullwidth" role="switch" switch>
-        </div>
-        <label for="stepper-fullwidth">Full width</label>
-      </div>
-    </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>
-  import { Dropdown } from '../../../../dist/js/bootstrap.bundle.js'
-
-  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
-  }
-
-  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 = 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 = 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 = 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>
index 464ac721bd8b2479b057df78636e1e40346701a4..6bcecfdf2ae8180d56ff34b0bdaa4befbf9274a9 100644 (file)
@@ -21,14 +21,16 @@ Here's a simple example of a vertical stepper.
 
 ### 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.
+Steppers can be made horizontal at specific breakpoints using the `contains-inline` utility on a parent element and by adding the responsive `.stepper-horizontal-{breakpoint}` modifier classes on the stepper. The extra parent element is required when using container queries.
 
-<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>`} />
+<ResizableExample code={`<div class="contains-inline">
+    <ol class="stepper stepper-horizontal-sm">
+      <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>
+  </div>`} />
 
 ### Gap
 
@@ -41,7 +43,7 @@ Customize the gap with styles that override the `--bs-stepper-gap` CSS variable.
     <li class="stepper-item">Finish</li>
   </ol>`} />
 
-### Variants
+## Variants
 
 <Example code={`<ol class="stepper stepper-horizontal">
     <li class="stepper-item active theme-accent">Create account</li>
@@ -50,7 +52,7 @@ Customize the gap with styles that override the `--bs-stepper-gap` CSS variable.
     <li class="stepper-item active theme-danger">Finish</li>
   </ol>`} />
 
-### Overflow
+## 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.
 
@@ -64,14 +66,7 @@ Wrap your horizontal stepper in a `.stepper-overflow` container to enable horizo
       <li class="stepper-item">Finish onboarding</li>
     </ol>
   </div>`} />
-
-## Playground
-
-Experiment with stepper options including orientation, breakpoints, step count, and more.
-
-<StepperPlayground />
-
-### Alignment
+## 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.
 
@@ -105,17 +100,17 @@ Apply `.w-100` to the stepper to make it full width. Stepper items will be stret
     <li class="stepper-item">Finish</li>
   </ol>`} />
 
-### With anchors
+## 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>
+    <a href="#" role="button" class="stepper-item active">Create account</a>
+    <a href="#" role="button" class="stepper-item active">Confirm email</a>
+    <a href="#" role="button" class="stepper-item theme-secondary">Update profile</a>
+    <a href="#" role="button" class="stepper-item theme-secondary">Finish</a>
   </div>`} />
 
 ## CSS