diff --git a/.changeset/empty-coins-dig.md b/.changeset/empty-coins-dig.md
new file mode 100644
index 0000000000..bb8bcdae36
--- /dev/null
+++ b/.changeset/empty-coins-dig.md
@@ -0,0 +1,7 @@
+---
+'@swisspost/design-system-styles': major
+'@swisspost/design-system-components': minor
+'@swisspost/design-system-documentation': patch
+---
+
+Removed the HTML stepper component and replaced it with new `post-stepper` and `post-stepper-item` web components.
diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html
index 5ffb87877a..26a81835e2 100644
--- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html
+++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html
@@ -140,3 +140,18 @@
Post Tooltip
Hi there 👋
+
+
+
Post Stepper
+
+ Step 1
+ Step 2
+ Step 3
+ Step 4
+
+
diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts
index ebc96d67a3..7262c74683 100644
--- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts
+++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts
@@ -21,6 +21,8 @@ import {
PostTabs,
PostTooltip,
PostTooltipTrigger,
+ PostStepper,
+ PostStepperItem,
} from 'components';
@Component({
@@ -49,6 +51,8 @@ import {
PostTabs,
PostTooltip,
PostTooltipTrigger,
+ PostStepper,
+ PostStepperItem,
],
})
export class HomeComponent {
diff --git a/packages/components/cypress/e2e/stepper.cy.ts b/packages/components/cypress/e2e/stepper.cy.ts
new file mode 100644
index 0000000000..e3ec22f525
--- /dev/null
+++ b/packages/components/cypress/e2e/stepper.cy.ts
@@ -0,0 +1,194 @@
+describe('stepper', { baseUrl: null }, () => {
+ beforeEach(() => {
+ cy.visit('./cypress/fixtures/post-stepper.test.html');
+ });
+
+ afterEach(() => {
+ cy.reload();
+ });
+
+ it('should render the post-stepper component', () => {
+ cy.get('post-stepper').should('exist');
+ });
+
+ // Errors
+
+ it('should log an error if the current label is missing', () => {
+ cy.window().then(win => {
+ cy.spy(win.console, 'error').as('consoleError');
+ });
+ cy.get('post-stepper').invoke('attr', 'current-label', null);
+ cy.get('@consoleError').should('be.called');
+ });
+
+ it('should log an error if the completed label is missing', () => {
+ cy.window().then(win => {
+ cy.spy(win.console, 'error').as('consoleError');
+ });
+ cy.get('post-stepper').invoke('attr', 'completed-label', null);
+ cy.get('@consoleError').should('be.called');
+ });
+
+ it('should log an error if the mobile step label does not contain #index', () => {
+ cy.window().then(win => {
+ cy.spy(win.console, 'error').as('consoleError');
+ });
+ cy.get('post-stepper').invoke('attr', 'active-step-label', 'Step:');
+ cy.get('@consoleError').should('be.called');
+ });
+
+ it('should log an error if the mobile step label is missing', () => {
+ cy.window().then(win => {
+ cy.spy(win.console, 'error').as('consoleError');
+ });
+ cy.get('post-stepper').invoke('attr', 'active-step-label', null);
+ cy.get('@consoleError').should('be.called');
+ });
+
+ // Dynamic classes
+
+ it('should set all inactive classes when current-index is negative', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', -1)
+ .wait(100)
+ .find('post-stepper-item')
+ .then($items => {
+ expect($items.filter('.stepper-item-inactive').length).to.equal(5);
+ });
+ });
+
+ it('should set first step as active when current-index is 0', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 0)
+ .wait(100)
+ .find('post-stepper-item')
+ .then($items => {
+ expect($items[0]).to.have.class('stepper-item-current');
+ expect($items.filter('.stepper-item-inactive').length).to.equal(4);
+ });
+ });
+
+ it('should set third step as active when current-index is 2', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 2)
+ .wait(100)
+ .find('post-stepper-item')
+ .then($items => {
+ expect($items[2]).to.have.class('stepper-item-current');
+ expect($items.filter('.stepper-item-completed').length).to.equal(2);
+ expect($items.filter('.stepper-item-inactive').length).to.equal(2);
+ });
+ });
+
+ it('should set all steps as complete but the last when current-index is last element', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 4)
+ .wait(100)
+ .find('post-stepper-item')
+ .then($items => {
+ expect($items[4]).to.have.class('stepper-item-current');
+ expect($items.filter('.stepper-item-completed').length).to.equal(4);
+ expect($items.filter('.stepper-item-inactive').length).to.equal(0);
+ });
+ });
+
+ it('should set all steps as complete when current-index is bigger than the steps length', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 5)
+ .wait(100)
+ .find('post-stepper-item')
+ .then($items => {
+ expect($items.filter('.stepper-item-current').length).to.equal(0);
+ expect($items.filter('.stepper-item-completed').length).to.equal(5);
+ expect($items.filter('.stepper-item-inactive').length).to.equal(0);
+ });
+ });
+
+ // Accessibility and labels
+
+ it('should set aria-current="step" on the current step', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 1)
+ .wait(100)
+ .find('post-stepper-item')
+ .eq(1)
+ .should('have.attr', 'aria-current', 'step');
+ });
+
+ it('should set aria-live="polite" on the current step', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 1)
+ .wait(100)
+ .find('post-stepper-item')
+ .eq(1)
+ .should('have.attr', 'aria-live', 'polite');
+ });
+
+ it('should not have aria-live="polite" on future step', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 1)
+ .wait(100)
+ .find('post-stepper-item')
+ .eq(2)
+ .should('not.have.attr', 'aria-live');
+ });
+
+ it('should set current label on the current step', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 1)
+ .wait(100)
+ .find('post-stepper-item')
+ .eq(1)
+ .find('.step-hidden-label')
+ .should('have.text', 'Current step:');
+ });
+
+ it('should set completed label on completed step', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 1)
+ .wait(100)
+ .find('post-stepper-item')
+ .eq(0)
+ .find('.step-hidden-label')
+ .should('have.text', 'Completed step:');
+ });
+
+ it('should set correct mobile label on stepper', () => {
+ cy.get('post-stepper')
+ .invoke('attr', 'current-index', 1)
+ .wait(100)
+ .find('.active-step')
+ .should('have.text', 'Step 2 : Step 2');
+ });
+
+ // Dynamically added/removed steps
+
+ it('should add correct class when a new step is added dynamically', () => {
+ cy.get('post-stepper').find('post-stepper-item').should('have.length', 5);
+ cy.get('post-stepper').then($stepper => {
+ $stepper[0].appendChild(document.createElement('post-stepper-item'));
+ cy.get('post-stepper')
+ .wait(100)
+ .find('post-stepper-item')
+ .should('have.length', 6)
+ .last()
+ .should('have.class', 'stepper-item-inactive');
+ });
+ });
+
+ it('should throw an error if there is only one step', () => {
+ cy.window().then(win => {
+ cy.spy(win.console, 'error').as('consoleError');
+ });
+ cy.get('post-stepper').find('post-stepper-item').should('have.length', 5);
+
+ cy.get('post-stepper').then($stepper => {
+ const stepper = $stepper[0];
+ const allButFirstStep = stepper.querySelectorAll('post-stepper-item:not(:first-child)');
+ allButFirstStep.forEach(step => step.remove());
+ });
+
+ cy.get('post-stepper').find('post-stepper-item').should('have.length', 1);
+ cy.get('@consoleError').should('be.called');
+ });
+});
diff --git a/packages/components/cypress/fixtures/post-stepper.test.html b/packages/components/cypress/fixtures/post-stepper.test.html
new file mode 100644
index 0000000000..951fc8a2d9
--- /dev/null
+++ b/packages/components/cypress/fixtures/post-stepper.test.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Document
+
+
+
+
+
+ Step 1
+ Step 2
+ Step 3
+ Step 4
+ Step 5
+
+
+
diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts
index c61dd61335..737f376abd 100644
--- a/packages/components/src/components.d.ts
+++ b/packages/components/src/components.d.ts
@@ -477,6 +477,27 @@ export namespace Components {
*/
"stars": number;
}
+ interface PostStepper {
+ /**
+ * Label for the "Step N:" indicator for mobile view. Use `#index` as a placeholder — it will be replaced with the current step number at runtime.
+ */
+ "activeStepLabel": string;
+ /**
+ * "Completed step" label for accessibility
+ */
+ "completedLabel": string;
+ /**
+ * Defines the currently active step
+ * @default -1
+ */
+ "currentIndex": number;
+ /**
+ * "Current step" label for accessibility
+ */
+ "currentLabel": string;
+ }
+ interface PostStepperItem {
+ }
interface PostTabHeader {
/**
* The name of the panel controlled by the tab header.
@@ -877,6 +898,18 @@ declare global {
prototype: HTMLPostRatingElement;
new (): HTMLPostRatingElement;
};
+ interface HTMLPostStepperElement extends Components.PostStepper, HTMLStencilElement {
+ }
+ var HTMLPostStepperElement: {
+ prototype: HTMLPostStepperElement;
+ new (): HTMLPostStepperElement;
+ };
+ interface HTMLPostStepperItemElement extends Components.PostStepperItem, HTMLStencilElement {
+ }
+ var HTMLPostStepperItemElement: {
+ prototype: HTMLPostStepperItemElement;
+ new (): HTMLPostStepperItemElement;
+ };
interface HTMLPostTabHeaderElement extends Components.PostTabHeader, HTMLStencilElement {
}
var HTMLPostTabHeaderElement: {
@@ -955,6 +988,8 @@ declare global {
"post-popover-trigger": HTMLPostPopoverTriggerElement;
"post-popovercontainer": HTMLPostPopovercontainerElement;
"post-rating": HTMLPostRatingElement;
+ "post-stepper": HTMLPostStepperElement;
+ "post-stepper-item": HTMLPostStepperItemElement;
"post-tab-header": HTMLPostTabHeaderElement;
"post-tab-panel": HTMLPostTabPanelElement;
"post-tabs": HTMLPostTabsElement;
@@ -1374,6 +1409,27 @@ declare namespace LocalJSX {
*/
"stars"?: number;
}
+ interface PostStepper {
+ /**
+ * Label for the "Step N:" indicator for mobile view. Use `#index` as a placeholder — it will be replaced with the current step number at runtime.
+ */
+ "activeStepLabel": string;
+ /**
+ * "Completed step" label for accessibility
+ */
+ "completedLabel": string;
+ /**
+ * Defines the currently active step
+ * @default -1
+ */
+ "currentIndex"?: number;
+ /**
+ * "Current step" label for accessibility
+ */
+ "currentLabel": string;
+ }
+ interface PostStepperItem {
+ }
interface PostTabHeader {
/**
* The name of the panel controlled by the tab header.
@@ -1471,6 +1527,8 @@ declare namespace LocalJSX {
"post-popover-trigger": PostPopoverTrigger;
"post-popovercontainer": PostPopovercontainer;
"post-rating": PostRating;
+ "post-stepper": PostStepper;
+ "post-stepper-item": PostStepperItem;
"post-tab-header": PostTabHeader;
"post-tab-panel": PostTabPanel;
"post-tabs": PostTabs;
@@ -1519,6 +1577,8 @@ declare module "@stencil/core" {
"post-popover-trigger": LocalJSX.PostPopoverTrigger & JSXBase.HTMLAttributes;
"post-popovercontainer": LocalJSX.PostPopovercontainer & JSXBase.HTMLAttributes;
"post-rating": LocalJSX.PostRating & JSXBase.HTMLAttributes;
+ "post-stepper": LocalJSX.PostStepper & JSXBase.HTMLAttributes;
+ "post-stepper-item": LocalJSX.PostStepperItem & JSXBase.HTMLAttributes;
"post-tab-header": LocalJSX.PostTabHeader & JSXBase.HTMLAttributes;
"post-tab-panel": LocalJSX.PostTabPanel & JSXBase.HTMLAttributes;
"post-tabs": LocalJSX.PostTabs & JSXBase.HTMLAttributes;
diff --git a/packages/components/src/components/post-popover/readme.md b/packages/components/src/components/post-popover/readme.md
index f9e1110c89..7acc272cb6 100644
--- a/packages/components/src/components/post-popover/readme.md
+++ b/packages/components/src/components/post-popover/readme.md
@@ -1,5 +1,7 @@
# post-popover
+
+
diff --git a/packages/components/src/components/post-stepper-item/post-stepper-item.scss b/packages/components/src/components/post-stepper-item/post-stepper-item.scss
new file mode 100644
index 0000000000..19fe286343
--- /dev/null
+++ b/packages/components/src/components/post-stepper-item/post-stepper-item.scss
@@ -0,0 +1,258 @@
+@use '@swisspost/design-system-styles/mixins/icons';
+@use '@swisspost/design-system-styles/mixins/utilities';
+@use '@swisspost/design-system-styles/mixins/media';
+@use '@swisspost/design-system-styles/functions/tokens';
+@use '@swisspost/design-system-styles/tokens/components';
+@use '@swisspost/design-system-styles/tokens/elements';
+
+tokens.$default-map: components.$post-stepper;
+
+@include icons.custom-property('success-solid');
+
+post-stepper-item {
+ grid-row: 1;
+ position: relative;
+ padding-inline-start: 0;
+
+ &:not(:first-child) {
+ padding-inline-start: calc(#{tokens.get('stepper-indicator')} / 2);
+ }
+
+ &:not(:last-child) {
+ padding-inline-end: calc(#{tokens.get('stepper-indicator')} / 2);
+ grid-column: span 2;
+ }
+
+ // progress bar
+ &::before,
+ &::after {
+ content: '';
+ display: block;
+ position: absolute;
+ inset-block-start: calc((#{tokens.get('stepper-indicator')} - 2px) / 2);
+ height: 2px;
+ background-color: tokens.get('stepper-connector-next');
+ inset-inline: 0;
+ }
+
+ &:not(:first-child, :last-child) {
+ &::before {
+ inset-inline-end: 50%;
+ }
+
+ &::after {
+ inset-inline-start: 50%;
+ }
+ }
+
+ &.stepper-item-current:not(:last-child)::after,
+ &.stepper-item-current:first-child::before,
+ &.stepper-item-current + *:last-child::after,
+ &.stepper-item-current + *::before {
+ background-color: tokens.get('stepper-connector-active');
+ }
+
+ &.stepper-item-current:not(:first-child)::before,
+ &.stepper-item-current:last-child::after,
+ &.stepper-item-completed::after,
+ &.stepper-item-completed::before,
+ &.stepper-item-completed + *::before {
+ background-color: tokens.get('stepper-connector-completed');
+ z-index: 1;
+ }
+}
+
+.stepper-item-content {
+ // stylelint-disable-next-line value-no-vendor-prefix
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ position: relative;
+ z-index: 2;
+ overflow: hidden;
+ text-decoration: none;
+ color: tokens.get('stepper-text-disabled-fg');
+ width: fit-content;
+ word-break: break-word;
+ font-size: tokens.get('stepper-step-label-font-size');
+
+ post-stepper-item:not(:first-child, :last-child) > & {
+ margin-inline: auto;
+ text-align: center;
+ }
+
+ post-stepper-item:last-child > & {
+ margin-inline-start: auto;
+ margin-inline-end: calc(40px / -2); // negative margin matching the container padding
+ text-align: end;
+ }
+
+ // Completed steps
+ .stepper-item-completed > & {
+ color: tokens.get('stepper-text-enabled-fg');
+ }
+
+ // Current step
+ .stepper-item-current > & {
+ font-weight: tokens.get('stepper-step-label-selected-font-weight');
+ color: tokens.get('stepper-text-enabled-fg');
+
+ &::before {
+ font-weight: tokens.get('body-font-weight', elements.$post-body);
+ color: tokens.get('stepper-enabled-fg');
+ }
+ }
+}
+
+// step indicator
+.stepper-item-content::before {
+ counter-increment: step-index;
+ content: counter(step-index);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1;
+ height: tokens.get('stepper-indicator');
+ width: tokens.get('stepper-indicator');
+ box-sizing: border-box;
+ margin-block-end: tokens.get('stepper-gap-text-start');
+ color: tokens.get('stepper-enabled-fg');
+ background-color: tokens.get('stepper-enabled-bg');
+ border: tokens.get('stepper-border-width') solid tokens.get('stepper-enabled-stroke');
+ border-radius: tokens.get('stepper-border-radius-round');
+ text-indent: initial;
+
+ post-stepper-item:not(:first-child, :last-child) > & {
+ margin-inline: auto;
+ }
+
+ post-stepper-item:last-child > & {
+ margin-inline-start: auto;
+ }
+
+ // Completed steps
+ .stepper-item-completed > & {
+ color: transparent; // Hides the number on completed step
+ background-color: tokens.get('stepper-completed-stroke');
+ border-color: tokens.get('stepper-completed-stroke');
+ }
+
+ // Future steps
+ .stepper-item-inactive > & {
+ color: tokens.get('stepper-disabled-fg');
+ background-color: tokens.get('stepper-disabled-bg');
+ border-color: tokens.get('stepper-disabled-stroke');
+ }
+}
+
+$checkIconSize: calc(
+ #{tokens.get('stepper-indicator')} - (#{tokens.get('stepper-border-width')} * 2)
+);
+
+// check icon
+.stepper-item-content::after {
+ display: block;
+ position: absolute;
+ inset-block-start: tokens.get('stepper-border-width');
+ z-index: 1;
+ background-color: tokens.get('stepper-enabled-fg');
+ @include icons.mask-image('success-solid');
+ width: $checkIconSize;
+ height: $checkIconSize;
+ color: tokens.get('stepper-completed-bg');
+
+ post-stepper-item:first-child > & {
+ inset-inline-start: tokens.get('stepper-border-width');
+ }
+
+ post-stepper-item:not(:first-child, :last-child) > & {
+ inset-inline-start: calc(50% - ($checkIconSize / 2));
+ }
+
+ post-stepper-item:last-child > & {
+ inset-inline-end: tokens.get('stepper-border-width');
+ }
+
+ // show only for completed steps
+ .stepper-item-completed > & {
+ content: '';
+ }
+}
+
+// smaller screens
+@include media.max(md) {
+ post-stepper-item {
+ flex-grow: 1;
+ margin-inline: 0;
+ padding: 0 !important;
+
+ &::before {
+ content: unset;
+ }
+
+ &::after {
+ inset-inline-start: 0 !important;
+ }
+
+ &:last-child {
+ flex: 0 0 32px;
+
+ &::after {
+ content: unset;
+ }
+ }
+ .stepper-item-content {
+ margin-inline: 0 !important;
+
+ > span {
+ display: none;
+ }
+ }
+ }
+}
+
+@include utilities.high-contrast-mode {
+ post-stepper-item {
+ &::before,
+ &::after {
+ background-color: CanvasText !important;
+ }
+
+ &.stepper-item-current:not(:first-child)::before,
+ &.stepper-item-current:last-child::after,
+ &.stepper-item-completed::after,
+ &.stepper-item-completed::before,
+ &.stepper-item-completed + *::before {
+ background-color: Highlight !important;
+ }
+ }
+
+ .stepper-item-content {
+ &::before {
+ forced-color-adjust: none;
+ }
+
+ .stepper-item-current > &::before {
+ background-color: Canvas;
+ color: CanvasText;
+ }
+
+ .stepper-item-completed > &::before,
+ .stepper-item-inactive > &::before {
+ border-color: Canvas;
+ background-color: CanvasText;
+ }
+
+ .stepper-item-inactive > &::before {
+ color: Canvas;
+ }
+
+ &::after {
+ color: Canvas;
+ }
+ }
+}
+
+.visually-hidden {
+ @include utilities.visuallyhidden();
+}
diff --git a/packages/components/src/components/post-stepper-item/post-stepper-item.tsx b/packages/components/src/components/post-stepper-item/post-stepper-item.tsx
new file mode 100644
index 0000000000..584b310fef
--- /dev/null
+++ b/packages/components/src/components/post-stepper-item/post-stepper-item.tsx
@@ -0,0 +1,22 @@
+import { Component, h, Host } from '@stencil/core';
+import { version } from '@root/package.json';
+
+@Component({
+ tag: 'post-stepper-item',
+ styleUrl: 'post-stepper-item.scss',
+ shadow: false,
+})
+export class PostStepperItem {
+ render() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/packages/components/src/components/post-stepper-item/readme.md b/packages/components/src/components/post-stepper-item/readme.md
new file mode 100644
index 0000000000..70033f179c
--- /dev/null
+++ b/packages/components/src/components/post-stepper-item/readme.md
@@ -0,0 +1,8 @@
+# post-stepper-item
+
+
+
+
+----------------------------------------------
+
+*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/components/src/components/post-stepper/post-stepper.scss b/packages/components/src/components/post-stepper/post-stepper.scss
new file mode 100644
index 0000000000..6e6f82f79b
--- /dev/null
+++ b/packages/components/src/components/post-stepper/post-stepper.scss
@@ -0,0 +1,40 @@
+@use '@swisspost/design-system-styles/mixins/utilities';
+@use '@swisspost/design-system-styles/functions/tokens';
+@use '@swisspost/design-system-styles/tokens/components';
+@use '@swisspost/design-system-styles/mixins/media';
+
+tokens.$default-map: components.$post-stepper;
+
+ol {
+ @include utilities.reset-list;
+ // start a counter for the step numbers
+ counter-reset: step-index;
+
+ display: grid;
+ position: relative;
+ overflow: hidden;
+
+ // the first column is half a step wide to make sure the separators are the same size even on small screens
+ grid-template-columns: calc(40px / 2);
+
+ // all other columns are 1 fraction of the available space ase we don't know the number of steps
+ grid-auto-columns: minmax(0, 1fr);
+
+ // we use a padding and negative margin on the last step for the same reason we need the first column
+ padding-inline-end: calc(40px / 2);
+
+ @include media.max(md) {
+ display: flex;
+ padding-inline-end: 0;
+ }
+}
+
+.active-step {
+ font-size: tokens.get('stepper-step-label-font-size');
+ font-weight: tokens.get('stepper-step-label-selected-font-weight');
+ color: tokens.get('stepper-text-enabled-fg');
+
+ @include media.min(md) {
+ display: none;
+ }
+}
diff --git a/packages/components/src/components/post-stepper/post-stepper.tsx b/packages/components/src/components/post-stepper/post-stepper.tsx
new file mode 100644
index 0000000000..2d4094818d
--- /dev/null
+++ b/packages/components/src/components/post-stepper/post-stepper.tsx
@@ -0,0 +1,154 @@
+import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core';
+import { version } from '@root/package.json';
+import { checkRequiredAndPattern, checkRequiredAndType } from '@/utils';
+
+@Component({
+ tag: 'post-stepper',
+ styleUrl: 'post-stepper.scss',
+ shadow: true,
+})
+export class PostStepper {
+ @Element() host: HTMLPostStepperElement;
+
+ private stepItems: NodeListOf;
+
+ /**
+ * Active step label is for visual purposes on mobile only
+ */
+ @State() mobileActiveStepLabel: string;
+
+ /**
+ * Active step name is for visual purposes on mobile only
+ */
+ @State() mobileActiveStepName: string;
+
+ /**
+ * "Current step" label for accessibility
+ */
+ @Prop({ reflect: true }) currentLabel!: string;
+
+ @Watch('currentLabel')
+ validateCurrentLabel() {
+ checkRequiredAndType(this, 'currentLabel', 'string');
+ }
+
+ /**
+ * "Completed step" label for accessibility
+ */
+ @Prop({ reflect: true }) completedLabel!: string;
+
+ @Watch('completedLabel')
+ validateCompletedLabel() {
+ checkRequiredAndType(this, 'completedLabel', 'string');
+ }
+
+ /**
+ * Label for the "Step N:" indicator for mobile view.
+ * Use `#index` as a placeholder — it will be replaced with the current step number at runtime.
+ */
+ @Prop({ reflect: true }) activeStepLabel!: string;
+
+ @Watch('activeStepLabel')
+ validateActiveStepLabel() {
+ checkRequiredAndPattern(this, 'activeStepLabel', /#index\b/);
+ this.updateActiveStepLabel();
+ }
+
+ /**
+ * Defines the currently active step
+ */
+ @Prop() currentIndex: number = -1;
+
+ @Watch('currentIndex')
+ validateCurrentIndex() {
+ checkRequiredAndType(this, 'currentIndex', 'number');
+ this.updateSteps();
+ }
+
+ componentDidLoad() {
+ this.validateCompletedLabel();
+ this.validateCurrentLabel();
+ this.validateActiveStepLabel();
+
+ // Wait for slotchange
+ setTimeout(() => {
+ this.validateCurrentIndex();
+ });
+ }
+
+ private updateActiveStepLabel() {
+ if (this.activeStepLabel) {
+ const labelTemplate = this.activeStepLabel;
+ this.mobileActiveStepLabel = labelTemplate.replace(/#index/g, `${this.currentIndex + 1}`);
+ this.updateMobileActiveStepVisibility();
+ }
+ }
+
+ private updateSteps() {
+ this.stepItems = this.host.querySelectorAll('post-stepper-item');
+
+ if (!this.stepItems || this.stepItems.length < 3) {
+ console.error('The post-stepper component should have at least three steps.');
+ return;
+ }
+
+ this.updateActiveStepLabel();
+
+ this.stepItems.forEach((el, i) => {
+ if (this.currentIndex === i) {
+ this.mobileActiveStepName = el.querySelector('.label').innerHTML;
+ }
+
+ // Update "post-stepper-item" classes to show correct status
+ el.classList.toggle('stepper-item-completed', this.currentIndex > i);
+ el.classList.toggle('stepper-item-current', this.currentIndex === i);
+ el.classList.toggle('stepper-item-inactive', this.currentIndex < i);
+
+ // Update accessibility label depending on status (Completed/Current/-)
+ const hiddenLabel = el.querySelector('.step-hidden-label');
+ if (hiddenLabel) {
+ let labelText = '';
+
+ if (this.currentIndex > i) {
+ labelText = `${this.completedLabel}:`;
+ } else if (this.currentIndex === i) {
+ labelText = `${this.currentLabel}:`;
+ }
+
+ hiddenLabel.textContent = labelText;
+ }
+
+ // Update accessibility aria attributes
+ if (this.currentIndex === i) {
+ el.setAttribute('aria-current', 'step');
+ el.setAttribute('aria-live', 'polite');
+ } else {
+ el.removeAttribute('aria-current');
+ el.removeAttribute('aria-live');
+ }
+ });
+
+ this.updateMobileActiveStepVisibility();
+ }
+
+ private updateMobileActiveStepVisibility() {
+ if (this.currentIndex >= this.stepItems.length || this.currentIndex < 0) {
+ this.mobileActiveStepLabel = '';
+ this.mobileActiveStepName = '';
+ }
+ }
+
+ render() {
+ return (
+
+
+ this.updateSteps()}>
+
+
+ {this.mobileActiveStepLabel}
+
+
+
+ );
+ }
+}
diff --git a/packages/components/src/components/post-stepper/readme.md b/packages/components/src/components/post-stepper/readme.md
new file mode 100644
index 0000000000..2f6f7ecfd8
--- /dev/null
+++ b/packages/components/src/components/post-stepper/readme.md
@@ -0,0 +1,20 @@
+# post-stepper
+
+
+
+
+
+
+## Properties
+
+| Property | Attribute | Description | Type | Default |
+| ------------------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- |
+| `activeStepLabel` _(required)_ | `active-step-label` | Label for the "Step N:" indicator for mobile view. Use `#index` as a placeholder — it will be replaced with the current step number at runtime. | `string` | `undefined` |
+| `completedLabel` _(required)_ | `completed-label` | "Completed step" label for accessibility | `string` | `undefined` |
+| `currentIndex` | `current-index` | Defines the currently active step | `number` | `-1` |
+| `currentLabel` _(required)_ | `current-label` | "Current step" label for accessibility | `string` | `undefined` |
+
+
+----------------------------------------------
+
+*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/documentation/src/stories/components/stepper/stepper.docs.mdx b/packages/documentation/src/stories/components/stepper/stepper.docs.mdx
index ef3a9dee62..d18b8adf4a 100644
--- a/packages/documentation/src/stories/components/stepper/stepper.docs.mdx
+++ b/packages/documentation/src/stories/components/stepper/stepper.docs.mdx
@@ -1,5 +1,4 @@
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
-import StylesPackageImport from '@/shared/styles-package-import.mdx';
import meta, * as StepperStories from './stepper.stories';
import PackageTag from '@/shared/package-tag';
@@ -8,15 +7,15 @@ import PackageTag from '@/shared/package-tag';
# Stepper
-
+
+
-
+
- The stepped progression component provides an interactive visual overview of a process. It shows
- at a glance the amount of steps a user is required to go through, and serves as a guide for each
- step, indicating its status.
+ A stepper guides users through any linear, multistep process by showing the user their completed,
+ current, and future steps in an interactive manner.
@@ -24,30 +23,7 @@ import PackageTag from '@/shared/package-tag';
-
-
-## Examples
-
-### Navigational Stepper
-
-You can use a stepper to allow users to navigate to specific steps.
-To do so, use `a` elements for the `.stepper-link` of the steps you want to be navigable, and use `span` for the steps you want not to be navigable.
-Then, wrap the stepper inside a `