From: pricop Date: Wed, 18 Mar 2026 02:26:23 +0000 (+0200) Subject: Refine Stepper component (#42165) X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e867fcfaac2d0364dad0afa192b574abe805a45f;p=thirdparty%2Fbootstrap.git Refine Stepper component (#42165) * 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 --- diff --git a/scss/_stepper.scss b/scss/_stepper.scss index 9ba2921ed2..ba0322cb01 100644 --- a/scss/_stepper.scss +++ b/scss/_stepper.scss @@ -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 diff --git a/scss/_utilities.scss b/scss/_utilities.scss index a25cafa49d..39211802d2 100644 --- a/scss/_utilities.scss +++ b/scss/_utilities.scss @@ -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 index e30c7cf1a8..0000000000 --- a/site/src/components/shortcodes/StepperPlayground.astro +++ /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] ---- - -
-
-
- -
- {orientations.map((orientation) => ( - - ))} -
-
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- -
- -
-
-
-
- - -
  • Create account
  • -
  • Confirm email
  • -
  • Update profile
  • -
  • Finish
  • -`} - id="stepper-preview" -/> - - diff --git a/site/src/content/docs/components/stepper.mdx b/site/src/content/docs/components/stepper.mdx index 464ac721bd..6bcecfdf2a 100644 --- a/site/src/content/docs/components/stepper.mdx +++ b/site/src/content/docs/components/stepper.mdx @@ -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. - -
  • Create account
  • -
  • Confirm email
  • -
  • Update profile
  • -
  • Finish
  • - `} /> + +
      +
    1. Create account
    2. +
    3. Confirm email
    4. +
    5. Update profile
    6. +
    7. Finish
    8. +
    + `} /> ### Gap @@ -41,7 +43,7 @@ Customize the gap with styles that override the `--bs-stepper-gap` CSS variable.
  • Finish
  • `} /> -### Variants +## Variants
  • Create account
  • @@ -50,7 +52,7 @@ Customize the gap with styles that override the `--bs-stepper-gap` CSS variable.
  • Finish
  • `} /> -### 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
  • Finish onboarding
  • `} /> - -## Playground - -Experiment with stepper options including orientation, breakpoints, step count, and more. - - - -### 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
  • Finish
  • `} /> -### With anchors +## With anchors Use anchor elements to build your stepper if it links across multiple pages. Add `role="button"` or use `