diff --git a/dev/stepper.html b/dev/stepper.html
new file mode 100644
index 00000000000..d71fefceb3e
--- /dev/null
+++ b/dev/stepper.html
@@ -0,0 +1,166 @@
+
+
+
+
+
+ vaadin-stepper
+
+
+
+
+
+
+ Stepper Examples
+
+
+
Vertical Stepper (Default)
+
+
+
+
+
+
+
+
+
+
Horizontal Stepper
+
+
+
+
+
+
+
+
+
+
Small Stepper
+
+
+
+
+
+
+
+
+
+
Horizontal Small Stepper
+
+
+
+
+
+
+
+
+
+
Stepper with Error State
+
+
+
+
+
+
+
+
+
+
Disabled Steps
+
+
+
+
+
+
+
+
+
Steps without Links
+
+
+
+
+
+
+
+
+
+
Interactive Stepper
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/stepper/package.json b/packages/stepper/package.json
new file mode 100644
index 00000000000..1266e680572
--- /dev/null
+++ b/packages/stepper/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@vaadin/stepper",
+ "version": "25.0.0-dev",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "Web component for step-by-step process",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/vaadin/web-components.git",
+ "directory": "packages/stepper"
+ },
+ "author": "Vaadin Ltd",
+ "homepage": "https://vaadin.com/components",
+ "bugs": {
+ "url": "https://github.com/vaadin/web-components/issues"
+ },
+ "main": "vaadin-stepper.js",
+ "module": "vaadin-stepper.js",
+ "type": "module",
+ "files": [
+ "src",
+ "vaadin-*.d.ts",
+ "vaadin-*.js",
+ "web-types.json",
+ "web-types.lit.json"
+ ],
+ "web-types": [
+ "web-types.json",
+ "web-types.lit.json"
+ ],
+ "keywords": [
+ "Vaadin",
+ "stepper",
+ "step",
+ "progress",
+ "wizard",
+ "web-components",
+ "web-component"
+ ],
+ "dependencies": {
+ "@vaadin/a11y-base": "25.0.0-alpha16",
+ "@vaadin/component-base": "25.0.0-alpha16",
+ "@vaadin/vaadin-themable-mixin": "25.0.0-alpha16",
+ "lit": "^3.0.0"
+ },
+ "devDependencies": {
+ "@vaadin/chai-plugins": "25.0.0-alpha16",
+ "@vaadin/testing-helpers": "^2.0.0"
+ }
+}
\ No newline at end of file
diff --git a/packages/stepper/src/styles/vaadin-step-styles.js b/packages/stepper/src/styles/vaadin-step-styles.js
new file mode 100644
index 00000000000..509021cecf1
--- /dev/null
+++ b/packages/stepper/src/styles/vaadin-step-styles.js
@@ -0,0 +1,213 @@
+/**
+ * @license
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { css } from 'lit';
+
+export const stepStyles = css`
+ :host {
+ display: block;
+ position: relative;
+ min-width: 0;
+ font-family: var(--lumo-font-family);
+ }
+
+ :host([hidden]) {
+ display: none !important;
+ }
+
+ :host([disabled]) {
+ pointer-events: none;
+ opacity: 0.5;
+ }
+
+ /* Content wrapper */
+ a,
+ div {
+ display: flex;
+ align-items: center;
+ gap: var(--lumo-space-m);
+ padding: var(--lumo-space-s);
+ text-decoration: none;
+ color: inherit;
+ cursor: pointer;
+ outline: none;
+ transition: color 0.2s;
+ }
+
+ :host([small]) a,
+ :host([small]) div {
+ gap: var(--lumo-space-s);
+ }
+
+ div {
+ cursor: default;
+ }
+
+ /* Indicator (circle) */
+ [part='indicator'] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: var(--lumo-size-m);
+ height: var(--lumo-size-m);
+ border: 2px solid var(--lumo-contrast-30pct);
+ border-radius: 50%;
+ background: var(--lumo-base-color);
+ font-size: var(--lumo-font-size-s);
+ font-weight: 500;
+ color: var(--lumo-secondary-text-color);
+ transition: all 0.2s;
+ }
+
+ :host([small]) [part='indicator'] {
+ width: var(--lumo-size-xs);
+ height: var(--lumo-size-xs);
+ font-size: var(--lumo-font-size-xs);
+ }
+
+ /* Active state */
+ :host([active]) [part='indicator'] {
+ border-color: var(--lumo-primary-color);
+ color: var(--lumo-primary-color);
+ }
+
+ /* Completed state */
+ :host([completed]) [part='indicator'] {
+ background: var(--lumo-primary-color);
+ border-color: var(--lumo-primary-color);
+ color: var(--lumo-primary-contrast-color);
+ }
+
+ /* Error state */
+ :host([error]) [part='indicator'] {
+ background: var(--lumo-error-color);
+ border-color: var(--lumo-error-color);
+ color: var(--lumo-error-contrast-color);
+ }
+
+ /* Content */
+ [part='content'] {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ flex: 1;
+ }
+
+ /* Label */
+ [part='label'] {
+ font-weight: 500;
+ color: var(--lumo-body-text-color);
+ }
+
+ :host([small]) [part='label'] {
+ font-size: var(--lumo-font-size-s);
+ }
+
+ :host([active]) [part='label'] {
+ color: var(--lumo-primary-color);
+ }
+
+ :host([completed]) [part='label'] {
+ color: var(--lumo-body-text-color);
+ }
+
+ :host([error]) [part='label'] {
+ color: var(--lumo-error-color);
+ }
+
+ :host(:not([active]):not([completed]):not([error])) [part='label'] {
+ color: var(--lumo-secondary-text-color);
+ }
+
+ /* Description */
+ [part='description'] {
+ font-size: var(--lumo-font-size-s);
+ color: var(--lumo-secondary-text-color);
+ margin-top: var(--lumo-space-xs);
+ }
+
+ :host([small]) [part='description'] {
+ font-size: var(--lumo-font-size-xs);
+ }
+
+ /* Horizontal orientation specific styles */
+ :host([orientation='horizontal']) [part='label'],
+ :host([orientation='horizontal']) [part='description'] {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ /* Connector line */
+ [part='connector'] {
+ position: absolute;
+ background: var(--lumo-contrast-30pct);
+ }
+
+ /* Vertical connector */
+ :host([orientation='vertical']) [part='connector'] {
+ position: absolute;
+ left: calc(var(--lumo-size-m) / 2 + var(--lumo-space-s) - 1px);
+ top: calc(var(--lumo-size-m) + var(--lumo-space-s));
+ width: 2px;
+ height: var(--lumo-space-l);
+ }
+
+ :host([orientation='vertical'][small]) [part='connector'] {
+ left: calc(var(--lumo-size-xs) / 2 + var(--lumo-space-s) - 1px);
+ top: calc(var(--lumo-size-xs) + var(--lumo-space-s));
+ height: var(--lumo-space-m);
+ }
+
+ /* Horizontal connector */
+ :host([orientation='horizontal']) [part='connector'] {
+ top: 50%;
+ left: 100%;
+ right: calc(var(--lumo-space-l) * -1);
+ height: 2px;
+ transform: translateY(-50%);
+ }
+
+ /* Hide connector for last step */
+ :host([last]) [part='connector'] {
+ display: none;
+ }
+
+ /* Hover effects */
+ a:hover [part='indicator']:not(:host([disabled]) [part='indicator']) {
+ border-color: var(--lumo-primary-color-50pct);
+ }
+
+ a:hover [part='label']:not(:host([disabled]) [part='label']) {
+ color: var(--lumo-primary-text-color);
+ }
+
+ /* Focus styles */
+ a:focus-visible {
+ outline: 2px solid var(--lumo-primary-color);
+ outline-offset: 2px;
+ border-radius: var(--lumo-border-radius-m);
+ }
+
+ /* Icons in indicator */
+ .checkmark,
+ .error-icon,
+ .step-number {
+ line-height: 1;
+ }
+
+ /* RTL support */
+ :host([dir='rtl']) [part='connector'] {
+ left: auto;
+ right: calc(var(--lumo-size-m) / 2);
+ transform: translateX(50%);
+ }
+
+ :host([dir='rtl'][orientation='horizontal']) [part='connector'] {
+ left: calc(var(--lumo-space-l) * -1);
+ right: 100%;
+ }
+`;
diff --git a/packages/stepper/src/styles/vaadin-stepper-styles.js b/packages/stepper/src/styles/vaadin-stepper-styles.js
new file mode 100644
index 00000000000..9ca60aec9a5
--- /dev/null
+++ b/packages/stepper/src/styles/vaadin-stepper-styles.js
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { css } from 'lit';
+
+export const stepperStyles = css`
+ :host {
+ display: block;
+ font-family: var(--lumo-font-family);
+ }
+
+ :host([hidden]) {
+ display: none !important;
+ }
+
+ [part='nav'] {
+ display: block;
+ }
+
+ [part='list'] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--lumo-space-l);
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ counter-reset: step;
+ }
+
+ /* Horizontal orientation */
+ :host([orientation='horizontal']) [part='list'] {
+ flex-direction: row;
+ align-items: center;
+ }
+
+ /* Small theme */
+ :host([theme~='small']) [part='list'] {
+ gap: var(--lumo-space-m);
+ }
+
+ /* Counter for step numbers */
+ ::slotted(vaadin-step) {
+ counter-increment: step;
+ }
+
+ /* Responsive behavior */
+ @media (max-width: 1023px) {
+ :host([orientation='horizontal']) [part='list'] {
+ flex-direction: column;
+ align-items: stretch;
+ }
+ }
+
+ /* RTL support */
+ :host([dir='rtl']) [part='list'] {
+ direction: rtl;
+ }
+`;
diff --git a/packages/stepper/src/vaadin-step.js b/packages/stepper/src/vaadin-step.js
new file mode 100644
index 00000000000..fca4cb446e2
--- /dev/null
+++ b/packages/stepper/src/vaadin-step.js
@@ -0,0 +1,370 @@
+/**
+ * @license
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { html, LitElement } from 'lit';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
+import { defineCustomElement } from '@vaadin/component-base/src/define.js';
+import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
+import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
+import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
+import { matchPaths } from '@vaadin/component-base/src/url-utils.js';
+import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
+import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+import { stepStyles } from './styles/vaadin-step-styles.js';
+
+/**
+ * `` is a Web Component for displaying a single step in a stepper.
+ *
+ * ```html
+ * Step 1
+ * ```
+ *
+ * ### Styling
+ *
+ * The following shadow DOM parts are available for styling:
+ *
+ * Part name | Description
+ * -------------|----------------
+ * `indicator` | The step indicator (circle with number/icon)
+ * `content` | The content wrapper containing label and description
+ * `label` | The step label
+ * `description`| The step description
+ * `connector` | The connector line to the next step
+ *
+ * The following state attributes are available for styling:
+ *
+ * Attribute | Description
+ * -------------|-------------
+ * `disabled` | Set when the element is disabled
+ * `completed` | Set when the step is completed
+ * `error` | Set when the step has an error
+ * `active` | Set when the step is active
+ * `last` | Set when this is the last step
+ * `orientation`| The orientation of the parent stepper (horizontal or vertical)
+ * `small` | Set when using small size variant
+ *
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
+ *
+ * @customElement
+ * @extends HTMLElement
+ * @mixes DisabledMixin
+ * @mixes DirMixin
+ * @mixes ElementMixin
+ * @mixes ThemableMixin
+ */
+class Step extends DisabledMixin(DirMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement)))))) {
+ static get is() {
+ return 'vaadin-step';
+ }
+
+ static get styles() {
+ return stepStyles;
+ }
+
+ static get properties() {
+ return {
+ /**
+ * The URL to navigate to
+ */
+ href: {
+ type: String,
+ },
+
+ /**
+ * The target of the link
+ */
+ target: {
+ type: String,
+ },
+
+ /**
+ * The label text
+ */
+ label: {
+ type: String,
+ },
+
+ /**
+ * The description text
+ */
+ description: {
+ type: String,
+ },
+
+ /**
+ * The state of the step
+ * @type {string}
+ */
+ state: {
+ type: String,
+ value: 'inactive',
+ reflectToAttribute: true,
+ observer: '_stateChanged',
+ },
+
+ /**
+ * Whether to exclude the item from client-side routing
+ * @type {boolean}
+ * @attr {boolean} router-ignore
+ */
+ routerIgnore: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * The orientation from parent stepper
+ * @type {string}
+ * @private
+ */
+ _orientation: {
+ type: String,
+ value: 'vertical',
+ reflectToAttribute: true,
+ attribute: 'orientation',
+ },
+
+ /**
+ * Whether this is the last step
+ * @type {boolean}
+ * @private
+ */
+ _last: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ attribute: 'last',
+ },
+
+ /**
+ * Whether using small size variant
+ * @type {boolean}
+ * @private
+ */
+ _small: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ attribute: 'small',
+ },
+
+ /**
+ * The step number
+ * @type {number}
+ * @private
+ */
+ _stepNumber: {
+ type: Number,
+ value: 0,
+ },
+
+ /**
+ * Whether the step's href matches the current page
+ * @type {boolean}
+ */
+ current: {
+ type: Boolean,
+ value: false,
+ readOnly: true,
+ reflectToAttribute: true,
+ },
+
+ /**
+ * Whether the step is completed
+ * @type {boolean}
+ */
+ completed: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+
+ /**
+ * Whether the step has an error
+ * @type {boolean}
+ */
+ error: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+
+ /**
+ * Whether the step is active
+ * @type {boolean}
+ */
+ active: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ };
+ }
+
+ constructor() {
+ super();
+ this.__boundUpdateCurrent = this.__updateCurrent.bind(this);
+ }
+
+ /** @protected */
+ render() {
+ const hasLink = !!this.href && !this.disabled;
+ const showConnector = !this._last;
+
+ return html`
+ ${hasLink
+ ? html`
+
+ ${this._renderContent()}
+
+ `
+ : html` ${this._renderContent()}
`}
+ ${showConnector ? html`` : ''}
+ `;
+ }
+
+ /** @private */
+ _renderContent() {
+ return html`
+
+ ${this.completed
+ ? html`✓`
+ : this.error
+ ? html`!`
+ : html`${this._stepNumber}`}
+
+
+ ${this.label || html``}
+ ${this.description ? html`${this.description}` : ''}
+
+ `;
+ }
+
+ /** @protected */
+ firstUpdated() {
+ super.firstUpdated();
+
+ if (!this.hasAttribute('role')) {
+ this.setAttribute('role', 'listitem');
+ }
+ }
+
+ /** @protected */
+ updated(props) {
+ super.updated(props);
+
+ if (props.has('href')) {
+ this.__updateCurrent();
+ }
+ }
+
+ /** @protected */
+ connectedCallback() {
+ super.connectedCallback();
+ this.__updateCurrent();
+
+ window.addEventListener('popstate', this.__boundUpdateCurrent);
+ window.addEventListener('vaadin-navigated', this.__boundUpdateCurrent);
+ }
+
+ /** @protected */
+ disconnectedCallback() {
+ super.disconnectedCallback();
+
+ window.removeEventListener('popstate', this.__boundUpdateCurrent);
+ window.removeEventListener('vaadin-navigated', this.__boundUpdateCurrent);
+ }
+
+ /**
+ * @param {boolean} last
+ * @private
+ */
+ _setLast(last) {
+ this._last = last;
+ }
+
+ /**
+ * @param {string} orientation
+ * @private
+ */
+ _setOrientation(orientation) {
+ this._orientation = orientation;
+ }
+
+ /**
+ * @param {boolean} small
+ * @private
+ */
+ _setSmall(small) {
+ this._small = small;
+ }
+
+ /**
+ * @param {number} stepNumber
+ * @private
+ */
+ _setStepNumber(stepNumber) {
+ this._stepNumber = stepNumber;
+ }
+
+ /** @private */
+ _stateChanged() {
+ this._updateStateAttributes();
+ }
+
+ /** @private */
+ __updateCurrent() {
+ if (!this.href) {
+ this._setCurrent(false);
+ return;
+ }
+
+ const browserPath = `${location.pathname}${location.search}`;
+ const isCurrent = matchPaths(browserPath, this.href);
+ this._setCurrent(isCurrent);
+
+ // Set active state if current
+ if (isCurrent) {
+ this.active = true;
+ this.completed = false;
+ this.error = false;
+ }
+ }
+
+ /** @private */
+ _updateStateAttributes() {
+ // Clear all states first
+ this.active = false;
+ this.completed = false;
+ this.error = false;
+
+ // Set the appropriate state
+ switch (this.state) {
+ case 'active':
+ this.active = true;
+ break;
+ case 'completed':
+ this.completed = true;
+ break;
+ case 'error':
+ this.error = true;
+ break;
+ default:
+ // inactive state - all flags remain false
+ break;
+ }
+ }
+}
+
+defineCustomElement(Step);
+
+export { Step };
diff --git a/packages/stepper/src/vaadin-stepper.js b/packages/stepper/src/vaadin-stepper.js
new file mode 100644
index 00000000000..bacf28de0b7
--- /dev/null
+++ b/packages/stepper/src/vaadin-stepper.js
@@ -0,0 +1,195 @@
+/**
+ * @license
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import './vaadin-step.js';
+import { html, LitElement } from 'lit';
+import { defineCustomElement } from '@vaadin/component-base/src/define.js';
+import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
+import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
+import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
+import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+import { stepperStyles } from './styles/vaadin-stepper-styles.js';
+
+/**
+ * `` is a Web Component for displaying a step-by-step process.
+ *
+ * ```html
+ *
+ * Step 1
+ * Step 2
+ * Step 3
+ *
+ * ```
+ *
+ * ### Styling
+ *
+ * The following shadow DOM parts are available for styling:
+ *
+ * Part name | Description
+ * -----------|----------------
+ * `list` | The ordered list element containing steps
+ *
+ * The following attributes are available for styling:
+ *
+ * Attribute | Description
+ * --------------|-------------
+ * `orientation` | The orientation of the stepper (horizontal or vertical)
+ * `theme` | Can be set to `small` for compact size
+ *
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
+ *
+ * @customElement
+ * @extends HTMLElement
+ * @mixes ElementMixin
+ * @mixes ThemableMixin
+ */
+class Stepper extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement)))) {
+ static get is() {
+ return 'vaadin-stepper';
+ }
+
+ static get styles() {
+ return stepperStyles;
+ }
+
+ static get properties() {
+ return {
+ /**
+ * The orientation of the stepper
+ * @type {string}
+ * @attr {string} orientation
+ */
+ orientation: {
+ type: String,
+ value: 'vertical',
+ reflectToAttribute: true,
+ },
+
+ /**
+ * The list of steps
+ * @type {!Array}
+ * @private
+ */
+ _steps: {
+ type: Array,
+ },
+ };
+ }
+
+ constructor() {
+ super();
+ this._steps = [];
+ }
+
+ /** @protected */
+ render() {
+ return html`
+
+ `;
+ }
+
+ /** @protected */
+ firstUpdated() {
+ super.firstUpdated();
+
+ if (!this.hasAttribute('role')) {
+ this.setAttribute('role', 'navigation');
+ }
+
+ this.setAttribute('aria-label', 'Progress');
+ }
+
+ /** @protected */
+ updated(props) {
+ super.updated(props);
+
+ if (props.has('orientation')) {
+ this._updateStepsOrientation();
+ }
+ }
+
+ /** @private */
+ _onSlotChange() {
+ const slot = this.shadowRoot.querySelector('slot');
+ const steps = slot.assignedElements().filter((el) => el.localName === 'vaadin-step');
+
+ this._steps = steps;
+
+ // Update step properties
+ steps.forEach((step, index) => {
+ const isLast = index === steps.length - 1;
+ step._setLast(isLast);
+ step._setStepNumber(index + 1);
+ step._setOrientation(this.orientation);
+
+ // Check if step has small theme
+ const theme = this.getAttribute('theme');
+ const hasSmallTheme = theme && theme.includes('small');
+ step._setSmall(hasSmallTheme);
+ });
+ }
+
+ /** @private */
+ _updateStepsOrientation() {
+ if (this._steps) {
+ this._steps.forEach((step) => {
+ step._setOrientation(this.orientation);
+ });
+ }
+ }
+
+ /**
+ * Sets the state of a specific step
+ * @param {string} state - The state to set ('active', 'completed', 'error', 'inactive')
+ * @param {number} stepIndex - The index of the step to update
+ */
+ setStepState(state, stepIndex) {
+ if (this._steps && this._steps[stepIndex]) {
+ this._steps[stepIndex].state = state;
+ this._steps[stepIndex].requestUpdate();
+ }
+ }
+
+ /**
+ * Marks steps up to the specified index as completed
+ * @param {number} untilIndex - Complete steps up to this index (exclusive)
+ */
+ completeStepsUntil(untilIndex) {
+ if (this._steps) {
+ this._steps.forEach((step, index) => {
+ if (index < untilIndex) {
+ step.state = 'completed';
+ }
+ });
+ }
+ }
+
+ /**
+ * Gets the current active step
+ * @return {Step|null} The active step or null if none is active
+ */
+ getActiveStep() {
+ return this._steps.find((step) => step.active) || null;
+ }
+
+ /**
+ * Resets all steps to inactive state
+ */
+ reset() {
+ if (this._steps) {
+ this._steps.forEach((step) => {
+ step.state = 'inactive';
+ });
+ }
+ }
+}
+
+defineCustomElement(Stepper);
+
+export { Stepper };
diff --git a/packages/stepper/test/dom/__snapshots__/stepper.test.snap.js b/packages/stepper/test/dom/__snapshots__/stepper.test.snap.js
new file mode 100644
index 00000000000..70a1d4d5abc
--- /dev/null
+++ b/packages/stepper/test/dom/__snapshots__/stepper.test.snap.js
@@ -0,0 +1,487 @@
+/* @web/test-runner snapshot v1 */
+export const snapshots = {};
+
+snapshots["vaadin-stepper stepper host default"] =
+`
+
+
+
+
+
+
+
+`;
+/* end snapshot vaadin-stepper stepper host default */
+
+snapshots["vaadin-stepper stepper host horizontal"] =
+`
+
+
+
+
+
+
+
+`;
+/* end snapshot vaadin-stepper stepper host horizontal */
+
+snapshots["vaadin-stepper stepper host small theme"] =
+`
+
+
+
+
+
+
+
+`;
+/* end snapshot vaadin-stepper stepper host small theme */
+
+snapshots["vaadin-stepper stepper shadow default"] =
+`
+`;
+/* end snapshot vaadin-stepper stepper shadow default */
+
+snapshots["vaadin-step step host default with href"] =
+`
+
+`;
+/* end snapshot vaadin-step step host default with href */
+
+snapshots["vaadin-step step host default without href"] =
+`
+
+`;
+/* end snapshot vaadin-step step host default without href */
+
+snapshots["vaadin-step step host with description"] =
+`
+
+`;
+/* end snapshot vaadin-step step host with description */
+
+snapshots["vaadin-step step host active state"] =
+`
+
+`;
+/* end snapshot vaadin-step step host active state */
+
+snapshots["vaadin-step step host completed state"] =
+`
+
+`;
+/* end snapshot vaadin-step step host completed state */
+
+snapshots["vaadin-step step host error state"] =
+`
+
+`;
+/* end snapshot vaadin-step step host error state */
+
+snapshots["vaadin-step step host disabled"] =
+`
+
+`;
+/* end snapshot vaadin-step step host disabled */
+
+snapshots["vaadin-step step host with target"] =
+`
+
+`;
+/* end snapshot vaadin-step step host with target */
+
+snapshots["vaadin-step step host router-ignore"] =
+`
+
+`;
+/* end snapshot vaadin-step step host router-ignore */
+
+snapshots["vaadin-step step host focused"] =
+`
+
+`;
+/* end snapshot vaadin-step step host focused */
+
+snapshots["vaadin-step step host focus-ring"] =
+`
+
+`;
+/* end snapshot vaadin-step step host focus-ring */
+
+snapshots["vaadin-step step host last step"] =
+`
+
+`;
+/* end snapshot vaadin-step step host last step */
+
+snapshots["vaadin-step step host horizontal orientation"] =
+`
+
+`;
+/* end snapshot vaadin-step step host horizontal orientation */
+
+snapshots["vaadin-step step host small size"] =
+`
+
+`;
+/* end snapshot vaadin-step step host small size */
+
+snapshots["vaadin-step step host with step number"] =
+`
+
+`;
+/* end snapshot vaadin-step step host with step number */
+
+snapshots["vaadin-step step host current"] =
+`
+
+`;
+/* end snapshot vaadin-step step host current */
+
+snapshots["vaadin-step step shadow default with href"] =
+`
+
+
+ 0
+
+
+
+
+ Test Step
+
+
+
+
+
+`;
+/* end snapshot vaadin-step step shadow default with href */
+
+snapshots["vaadin-step step shadow default without href"] =
+`
+
+
+ 0
+
+
+
+
+ Test Step
+
+
+
+
+
+`;
+/* end snapshot vaadin-step step shadow default without href */
+
+snapshots["vaadin-step step shadow with description"] =
+`
+
+
+ 0
+
+
+
+
+ Test Step
+
+
+ Step description
+
+
+
+
+
+`;
+/* end snapshot vaadin-step step shadow with description */
+
+snapshots["vaadin-step step shadow completed state"] =
+`
+
+
+ 0
+
+
+
+
+ Completed Step
+
+
+
+
+
+`;
+/* end snapshot vaadin-step step shadow completed state */
+
+snapshots["vaadin-step step shadow error state"] =
+`
+
+
+ 0
+
+
+
+
+ Error Step
+
+
+
+
+
+`;
+/* end snapshot vaadin-step step shadow error state */
+
+snapshots["vaadin-step step shadow disabled"] =
+`
+
+
+ 0
+
+
+
+
+ Disabled Step
+
+
+
+
+
+`;
+/* end snapshot vaadin-step step shadow disabled */
+
+snapshots["vaadin-step step shadow last step"] =
+`
+
+
+ 0
+
+
+
+
+ Last Step
+
+
+
+
+
+`;
+/* end snapshot vaadin-step step shadow last step */
+
diff --git a/packages/stepper/test/dom/stepper.test.js b/packages/stepper/test/dom/stepper.test.js
new file mode 100644
index 00000000000..940809c3b2e
--- /dev/null
+++ b/packages/stepper/test/dom/stepper.test.js
@@ -0,0 +1,189 @@
+import { expect } from '@vaadin/chai-plugins';
+import { sendKeys } from '@vaadin/test-runner-commands';
+import { fixtureSync } from '@vaadin/testing-helpers';
+import '../../src/vaadin-stepper.js';
+import '../../src/vaadin-step.js';
+
+describe('vaadin-stepper', () => {
+ let stepper;
+
+ describe('stepper host', () => {
+ beforeEach(() => {
+ stepper = fixtureSync(`
+
+
+
+
+
+ `);
+ });
+
+ it('default', async () => {
+ await expect(stepper).dom.to.equalSnapshot();
+ });
+
+ it('horizontal', async () => {
+ stepper.orientation = 'horizontal';
+ await expect(stepper).dom.to.equalSnapshot();
+ });
+
+ it('small theme', async () => {
+ stepper.setAttribute('theme', 'small');
+ await expect(stepper).dom.to.equalSnapshot();
+ });
+ });
+
+ describe('stepper shadow', () => {
+ beforeEach(() => {
+ stepper = fixtureSync(`
+
+
+
+
+
+ `);
+ });
+
+ it('default', async () => {
+ await expect(stepper).shadowDom.to.equalSnapshot();
+ });
+ });
+});
+
+describe('vaadin-step', () => {
+ let step;
+
+ describe('step host', () => {
+ it('default with href', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('default without href', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('with description', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('active state', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('completed state', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('error state', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('disabled', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('with target', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('router-ignore', async () => {
+ step = fixtureSync('');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('focused', async () => {
+ step = fixtureSync('');
+ step.focus();
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('focus-ring', async () => {
+ step = fixtureSync('');
+ await sendKeys({ press: 'Tab' });
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('last step', async () => {
+ step = fixtureSync('');
+ step._setLast(true);
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('horizontal orientation', async () => {
+ step = fixtureSync(
+ '',
+ );
+ step._setOrientation('horizontal');
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('small size', async () => {
+ step = fixtureSync('');
+ step._setSmall(true);
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('with step number', async () => {
+ step = fixtureSync('');
+ step._setStepNumber(3);
+ await expect(step).dom.to.equalSnapshot();
+ });
+
+ it('current', async () => {
+ step = fixtureSync('');
+ step._setCurrent(true);
+ await expect(step).dom.to.equalSnapshot();
+ });
+ });
+
+ describe('step shadow', () => {
+ it('default with href', async () => {
+ step = fixtureSync('');
+ step._setStepNumber(1);
+ await expect(step).shadowDom.to.equalSnapshot();
+ });
+
+ it('default without href', async () => {
+ step = fixtureSync('');
+ step._setStepNumber(1);
+ await expect(step).shadowDom.to.equalSnapshot();
+ });
+
+ it('with description', async () => {
+ step = fixtureSync('');
+ step._setStepNumber(1);
+ await expect(step).shadowDom.to.equalSnapshot();
+ });
+
+ it('completed state', async () => {
+ step = fixtureSync('');
+ await expect(step).shadowDom.to.equalSnapshot();
+ });
+
+ it('error state', async () => {
+ step = fixtureSync('');
+ await expect(step).shadowDom.to.equalSnapshot();
+ });
+
+ it('disabled', async () => {
+ step = fixtureSync('');
+ step._setStepNumber(1);
+ await expect(step).shadowDom.to.equalSnapshot();
+ });
+
+ it('last step', async () => {
+ step = fixtureSync('');
+ step._setLast(true);
+ step._setStepNumber(4);
+ await expect(step).shadowDom.to.equalSnapshot();
+ });
+ });
+});
diff --git a/packages/stepper/test/stepper.test.js b/packages/stepper/test/stepper.test.js
new file mode 100644
index 00000000000..24867599bd6
--- /dev/null
+++ b/packages/stepper/test/stepper.test.js
@@ -0,0 +1,391 @@
+import { expect } from '@vaadin/chai-plugins';
+import { fixtureSync, nextFrame, nextRender } from '@vaadin/testing-helpers';
+import '../src/vaadin-stepper.js';
+import '../src/vaadin-step.js';
+
+describe('vaadin-stepper', () => {
+ let stepper;
+
+ describe('basic', () => {
+ beforeEach(() => {
+ stepper = fixtureSync(`
+
+
+
+
+
+ `);
+ });
+
+ it('should have correct tag name', () => {
+ expect(stepper.localName).to.equal('vaadin-stepper');
+ });
+
+ it('should have navigation role', () => {
+ expect(stepper.getAttribute('role')).to.equal('navigation');
+ });
+
+ it('should have aria-label', () => {
+ expect(stepper.getAttribute('aria-label')).to.equal('Progress');
+ });
+
+ it('should have vertical orientation by default', () => {
+ expect(stepper.orientation).to.equal('vertical');
+ expect(stepper.getAttribute('orientation')).to.equal('vertical');
+ });
+
+ it('should update steps array on slot change', async () => {
+ await nextFrame();
+ expect(stepper._steps).to.have.lengthOf(3);
+ expect(stepper._steps[0].label).to.equal('Step 1');
+ expect(stepper._steps[1].label).to.equal('Step 2');
+ expect(stepper._steps[2].label).to.equal('Step 3');
+ });
+
+ it('should mark last step', async () => {
+ await nextFrame();
+ expect(stepper._steps[0].hasAttribute('last')).to.be.false;
+ expect(stepper._steps[1].hasAttribute('last')).to.be.false;
+ expect(stepper._steps[2].hasAttribute('last')).to.be.true;
+ });
+
+ it('should set step numbers', async () => {
+ await nextFrame();
+ expect(stepper._steps[0]._stepNumber).to.equal(1);
+ expect(stepper._steps[1]._stepNumber).to.equal(2);
+ expect(stepper._steps[2]._stepNumber).to.equal(3);
+ });
+ });
+
+ describe('orientation', () => {
+ beforeEach(() => {
+ stepper = fixtureSync(`
+
+
+
+
+ `);
+ });
+
+ it('should set horizontal orientation', () => {
+ expect(stepper.orientation).to.equal('horizontal');
+ expect(stepper.getAttribute('orientation')).to.equal('horizontal');
+ });
+
+ it('should update step orientation', async () => {
+ await nextFrame();
+ expect(stepper._steps[0].getAttribute('orientation')).to.equal('horizontal');
+ expect(stepper._steps[1].getAttribute('orientation')).to.equal('horizontal');
+ });
+
+ it('should update orientation when changed', async () => {
+ await nextFrame();
+ stepper.orientation = 'vertical';
+ await nextFrame();
+ expect(stepper._steps[0].getAttribute('orientation')).to.equal('vertical');
+ expect(stepper._steps[1].getAttribute('orientation')).to.equal('vertical');
+ });
+ });
+
+ describe('theme', () => {
+ beforeEach(() => {
+ stepper = fixtureSync(`
+
+
+
+
+ `);
+ });
+
+ it('should apply small theme to steps', async () => {
+ await nextFrame();
+ expect(stepper._steps[0].hasAttribute('small')).to.be.true;
+ expect(stepper._steps[1].hasAttribute('small')).to.be.true;
+ });
+ });
+
+ describe('step states', () => {
+ beforeEach(() => {
+ stepper = fixtureSync(`
+
+
+
+
+
+ `);
+ });
+
+ it('should set step state', async () => {
+ await nextFrame();
+ stepper.setStepState('active', 0);
+ await nextRender();
+ expect(stepper._steps[0].state).to.equal('active');
+ expect(stepper._steps[0].hasAttribute('active')).to.be.true;
+ });
+
+ it('should complete steps until index', async () => {
+ await nextFrame();
+ stepper.completeStepsUntil(2);
+ expect(stepper._steps[0].state).to.equal('completed');
+ expect(stepper._steps[1].state).to.equal('completed');
+ expect(stepper._steps[2].state).to.equal('inactive');
+ });
+
+ it('should get active step', async () => {
+ await nextFrame();
+ stepper.setStepState('active', 1);
+ await nextRender();
+ const activeStep = stepper.getActiveStep();
+ expect(activeStep).to.equal(stepper._steps[1]);
+ });
+
+ it('should return null when no active step', async () => {
+ await nextFrame();
+ const activeStep = stepper.getActiveStep();
+ expect(activeStep).to.be.null;
+ });
+
+ it('should reset all steps', async () => {
+ await nextFrame();
+ stepper.setStepState('active', 0);
+ stepper.setStepState('completed', 1);
+ stepper.setStepState('error', 2);
+
+ stepper.reset();
+
+ expect(stepper._steps[0].state).to.equal('inactive');
+ expect(stepper._steps[1].state).to.equal('inactive');
+ expect(stepper._steps[2].state).to.equal('inactive');
+ });
+ });
+});
+
+describe('vaadin-step', () => {
+ let step;
+
+ describe('basic', () => {
+ beforeEach(() => {
+ step = fixtureSync(``);
+ });
+
+ it('should have correct tag name', () => {
+ expect(step.localName).to.equal('vaadin-step');
+ });
+
+ it('should have listitem role', () => {
+ expect(step.getAttribute('role')).to.equal('listitem');
+ });
+
+ it('should render label', () => {
+ expect(step.label).to.equal('Test Step');
+ });
+
+ it('should render description', () => {
+ expect(step.description).to.equal('Test description');
+ });
+
+ it('should have inactive state by default', () => {
+ expect(step.state).to.equal('inactive');
+ });
+ });
+
+ describe('with href', () => {
+ beforeEach(() => {
+ step = fixtureSync(``);
+ });
+
+ it('should have href', () => {
+ expect(step.href).to.equal('/test');
+ });
+
+ it('should render link element', () => {
+ const link = step.shadowRoot.querySelector('a');
+ expect(link).to.exist;
+ expect(link.getAttribute('href')).to.equal('/test');
+ });
+
+ it('should support target attribute', async () => {
+ step.target = '_blank';
+ await nextFrame();
+ const link = step.shadowRoot.querySelector('a');
+ expect(link.getAttribute('target')).to.equal('_blank');
+ });
+
+ it('should support router-ignore', async () => {
+ step.routerIgnore = true;
+ await nextFrame();
+ const link = step.shadowRoot.querySelector('a');
+ expect(link.hasAttribute('router-ignore')).to.be.true;
+ });
+ });
+
+ describe('without href', () => {
+ beforeEach(() => {
+ step = fixtureSync(``);
+ });
+
+ it('should render div element', () => {
+ const div = step.shadowRoot.querySelector('div');
+ expect(div).to.exist;
+ const link = step.shadowRoot.querySelector('a');
+ expect(link).to.not.exist;
+ });
+ });
+
+ describe('states', () => {
+ beforeEach(() => {
+ step = fixtureSync(``);
+ });
+
+ it('should set active state', async () => {
+ step.state = 'active';
+ await nextRender();
+ expect(step.hasAttribute('active')).to.be.true;
+ expect(step.hasAttribute('completed')).to.be.false;
+ expect(step.hasAttribute('error')).to.be.false;
+ });
+
+ it('should set completed state', async () => {
+ step.state = 'completed';
+ await nextRender();
+ expect(step.hasAttribute('completed')).to.be.true;
+ expect(step.hasAttribute('active')).to.be.false;
+ expect(step.hasAttribute('error')).to.be.false;
+ });
+
+ it('should set error state', async () => {
+ step.state = 'error';
+ await nextRender();
+ expect(step.hasAttribute('error')).to.be.true;
+ expect(step.hasAttribute('active')).to.be.false;
+ expect(step.hasAttribute('completed')).to.be.false;
+ });
+
+ it('should show checkmark for completed state', async () => {
+ step.state = 'completed';
+ await nextFrame();
+ const indicator = step.shadowRoot.querySelector('[part="indicator"]');
+ expect(indicator.querySelector('.checkmark')).to.exist;
+ });
+
+ it('should show error icon for error state', async () => {
+ step.state = 'error';
+ await nextFrame();
+ const indicator = step.shadowRoot.querySelector('[part="indicator"]');
+ expect(indicator.querySelector('.error-icon')).to.exist;
+ });
+
+ it('should show step number for other states', async () => {
+ step._setStepNumber(3);
+ await nextFrame();
+ const indicator = step.shadowRoot.querySelector('[part="indicator"]');
+ expect(indicator.querySelector('.step-number').textContent).to.equal('3');
+ });
+ });
+
+ describe('disabled', () => {
+ beforeEach(() => {
+ step = fixtureSync(``);
+ });
+
+ it('should be disabled', () => {
+ expect(step.disabled).to.be.true;
+ });
+
+ it('should have disabled attribute', () => {
+ expect(step.hasAttribute('disabled')).to.be.true;
+ });
+
+ it('should render as div when disabled', () => {
+ const div = step.shadowRoot.querySelector('div');
+ expect(div).to.exist;
+ const link = step.shadowRoot.querySelector('a');
+ expect(link).to.not.exist;
+ });
+ });
+
+ describe('current page detection', () => {
+ let originalLocation;
+
+ beforeEach(() => {
+ originalLocation = window.location.href;
+ window.history.pushState({}, '', '/test-page');
+ step = fixtureSync(``);
+ });
+
+ afterEach(() => {
+ window.history.pushState({}, '', originalLocation);
+ });
+
+ it('should detect current page', () => {
+ expect(step.current).to.be.true;
+ expect(step.active).to.be.true;
+ });
+
+ it('should set aria-current for current page', async () => {
+ await nextRender();
+ const link = step.shadowRoot.querySelector('a');
+ expect(link.getAttribute('aria-current')).to.equal('step');
+ });
+
+ it('should update on navigation', async () => {
+ window.history.pushState({}, '', '/other-page');
+ window.dispatchEvent(new PopStateEvent('popstate'));
+ await nextFrame();
+ expect(step.current).to.be.false;
+ });
+ });
+
+ describe('orientation', () => {
+ beforeEach(() => {
+ step = fixtureSync(
+ ``,
+ );
+ });
+
+ it('should have vertical orientation by default', () => {
+ expect(step.getAttribute('orientation')).to.equal('vertical');
+ });
+
+ it('should update orientation', async () => {
+ step._setOrientation('horizontal');
+ await nextRender();
+ expect(step.getAttribute('orientation')).to.equal('horizontal');
+ });
+ });
+
+ describe('size', () => {
+ beforeEach(() => {
+ step = fixtureSync(``);
+ });
+
+ it('should not be small by default', () => {
+ expect(step.hasAttribute('small')).to.be.false;
+ });
+
+ it('should set small size', async () => {
+ step._setSmall(true);
+ await nextRender();
+
+ expect(step.hasAttribute('small')).to.be.true;
+ });
+ });
+
+ describe('connector', () => {
+ beforeEach(() => {
+ step = fixtureSync(``);
+ });
+
+ it('should show connector by default', () => {
+ const connector = step.shadowRoot.querySelector('[part="connector"]');
+ expect(connector).to.exist;
+ });
+
+ it('should hide connector for last step', async () => {
+ step._setLast(true);
+ await nextFrame();
+ const connector = step.shadowRoot.querySelector('[part="connector"]');
+ expect(connector).to.not.exist;
+ });
+ });
+});
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/all-states.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/all-states.png
new file mode 100644
index 00000000000..d71c6a96e84
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/all-states.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/disabled.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/disabled.png
new file mode 100644
index 00000000000..f2617b3129b
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/disabled.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/focus-ring.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/focus-ring.png
new file mode 100644
index 00000000000..dd7e4818b66
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/focus-ring.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-default.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-default.png
new file mode 100644
index 00000000000..071b9597367
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-default.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-small.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-small.png
new file mode 100644
index 00000000000..3086249a2e8
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-small.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-with-descriptions.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-with-descriptions.png
new file mode 100644
index 00000000000..4c4c9d835f9
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/horizontal-with-descriptions.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/hover.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/hover.png
new file mode 100644
index 00000000000..dd7e4818b66
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/hover.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/no-href.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/no-href.png
new file mode 100644
index 00000000000..51b488f99fe
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/no-href.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-horizontal.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-horizontal.png
new file mode 100644
index 00000000000..04dbfc6f1f9
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-horizontal.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-vertical.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-vertical.png
new file mode 100644
index 00000000000..83b8d7bfe61
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/rtl-vertical.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-default.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-default.png
new file mode 100644
index 00000000000..395b70d1634
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-default.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-no-descriptions.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-no-descriptions.png
new file mode 100644
index 00000000000..43e3c40261e
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-no-descriptions.png differ
diff --git a/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-small.png b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-small.png
new file mode 100644
index 00000000000..de3374fa7b7
Binary files /dev/null and b/packages/stepper/test/visual/base/screenshots/stepper/baseline/vertical-small.png differ
diff --git a/packages/stepper/test/visual/base/stepper.test.js b/packages/stepper/test/visual/base/stepper.test.js
new file mode 100644
index 00000000000..3c1379fea4c
--- /dev/null
+++ b/packages/stepper/test/visual/base/stepper.test.js
@@ -0,0 +1,245 @@
+import { resetMouse, sendKeys, sendMouseToElement } from '@vaadin/test-runner-commands';
+import { fixtureSync } from '@vaadin/testing-helpers';
+import { visualDiff } from '@web/test-runner-visual-regression';
+import '../../../src/vaadin-stepper.js';
+import '../../../src/vaadin-step.js';
+
+describe('stepper', () => {
+ let div, element;
+
+ afterEach(async () => {
+ await resetMouse();
+ });
+
+ describe('vertical', () => {
+ it('default', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'vertical-default');
+ });
+
+ it('small', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'vertical-small');
+ });
+
+ it('without-descriptions', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'vertical-no-descriptions');
+ });
+ });
+
+ describe('horizontal', () => {
+ it('default', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '800px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'horizontal-default');
+ });
+
+ it('small', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '800px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'horizontal-small');
+ });
+
+ it('with-descriptions', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '1000px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'horizontal-with-descriptions');
+ });
+ });
+
+ describe('states', () => {
+ it('all-states', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'all-states');
+ });
+
+ it('hover', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+ `,
+ div,
+ );
+ const step = element.querySelector('vaadin-step[href="/step3"]');
+ await sendMouseToElement({ type: 'move', element: step });
+ await visualDiff(div, 'hover');
+ });
+
+ it('focus-ring', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+ `,
+ div,
+ );
+ await sendKeys({ press: 'Tab' });
+ await visualDiff(div, 'focus-ring');
+ });
+
+ it('disabled', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'disabled');
+ });
+ });
+
+ describe('without-links', () => {
+ it('no-href', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'no-href');
+ });
+ });
+
+ describe('RTL', () => {
+ it('rtl-vertical', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ div.setAttribute('dir', 'rtl');
+ element = fixtureSync(
+ `
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'rtl-vertical');
+ });
+
+ it('rtl-horizontal', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '800px';
+ div.setAttribute('dir', 'rtl');
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'rtl-horizontal');
+ });
+ });
+});
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/all-states.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/all-states.png
new file mode 100644
index 00000000000..57ff5e2ab72
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/all-states.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/disabled.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/disabled.png
new file mode 100644
index 00000000000..3a124fb4092
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/disabled.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/focus-ring.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/focus-ring.png
new file mode 100644
index 00000000000..53741cb86a7
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/focus-ring.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-default.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-default.png
new file mode 100644
index 00000000000..4ca0710f006
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-default.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-small.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-small.png
new file mode 100644
index 00000000000..a9d56c4cfce
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-small.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-with-descriptions.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-with-descriptions.png
new file mode 100644
index 00000000000..0d1a3346e02
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/horizontal-with-descriptions.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/hover.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/hover.png
new file mode 100644
index 00000000000..1637ed23dbc
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/hover.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/no-href.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/no-href.png
new file mode 100644
index 00000000000..d2e59c197d8
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/no-href.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-horizontal.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-horizontal.png
new file mode 100644
index 00000000000..c1f77d32bda
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-horizontal.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-vertical.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-vertical.png
new file mode 100644
index 00000000000..4f084c7e3c1
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/rtl-vertical.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-default.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-default.png
new file mode 100644
index 00000000000..b238f3d883c
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-default.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-no-descriptions.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-no-descriptions.png
new file mode 100644
index 00000000000..0121004a547
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-no-descriptions.png differ
diff --git a/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-small.png b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-small.png
new file mode 100644
index 00000000000..2a3abb6a324
Binary files /dev/null and b/packages/stepper/test/visual/lumo/screenshots/stepper/baseline/vertical-small.png differ
diff --git a/packages/stepper/test/visual/lumo/stepper.test.js b/packages/stepper/test/visual/lumo/stepper.test.js
new file mode 100644
index 00000000000..2904b2ae3b1
--- /dev/null
+++ b/packages/stepper/test/visual/lumo/stepper.test.js
@@ -0,0 +1,246 @@
+import { resetMouse, sendKeys, sendMouseToElement } from '@vaadin/test-runner-commands';
+import { fixtureSync } from '@vaadin/testing-helpers';
+import { visualDiff } from '@web/test-runner-visual-regression';
+import '@vaadin/vaadin-lumo-styles/props.css';
+import '../../../vaadin-stepper.js';
+import '../../../vaadin-step.js';
+
+describe('stepper', () => {
+ let div, element;
+
+ afterEach(async () => {
+ await resetMouse();
+ });
+
+ describe('vertical', () => {
+ it('default', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'vertical-default');
+ });
+
+ it('small', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'vertical-small');
+ });
+
+ it('without-descriptions', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'vertical-no-descriptions');
+ });
+ });
+
+ describe('horizontal', () => {
+ it('default', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '800px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'horizontal-default');
+ });
+
+ it('small', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '800px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'horizontal-small');
+ });
+
+ it('with-descriptions', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '1000px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'horizontal-with-descriptions');
+ });
+ });
+
+ describe('states', () => {
+ it('all-states', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'all-states');
+ });
+
+ it('hover', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+ `,
+ div,
+ );
+ const step = element.querySelector('vaadin-step[href="/step3"]');
+ await sendMouseToElement({ type: 'move', element: step });
+ await visualDiff(div, 'hover');
+ });
+
+ it('focus-ring', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+ `,
+ div,
+ );
+ await sendKeys({ press: 'Tab' });
+ await visualDiff(div, 'focus-ring');
+ });
+
+ it('disabled', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'disabled');
+ });
+ });
+
+ describe('without-links', () => {
+ it('no-href', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'no-href');
+ });
+ });
+
+ describe('RTL', () => {
+ it('rtl-vertical', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '400px';
+ div.setAttribute('dir', 'rtl');
+ element = fixtureSync(
+ `
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'rtl-vertical');
+ });
+
+ it('rtl-horizontal', async () => {
+ div = document.createElement('div');
+ div.style.display = 'inline-block';
+ div.style.padding = '10px';
+ div.style.width = '800px';
+ div.setAttribute('dir', 'rtl');
+ element = fixtureSync(
+ `
+
+
+
+
+ `,
+ div,
+ );
+ await visualDiff(div, 'rtl-horizontal');
+ });
+ });
+});
diff --git a/packages/stepper/vaadin-step.d.ts b/packages/stepper/vaadin-step.d.ts
new file mode 100644
index 00000000000..2ea025ec924
--- /dev/null
+++ b/packages/stepper/vaadin-step.d.ts
@@ -0,0 +1,80 @@
+/**
+ * @license
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
+import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
+import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
+import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+export type StepState = 'active' | 'completed' | 'error' | 'inactive';
+
+/**
+ * `` is a Web Component for displaying a single step in a stepper.
+ *
+ * ```html
+ * Step 1
+ * ```
+ */
+declare class Step extends DisabledMixin(DirMixin(ElementMixin(ThemableMixin(HTMLElement)))) {
+ /**
+ * The URL to navigate to
+ */
+ href: string | null | undefined;
+
+ /**
+ * The target of the link
+ */
+ target: string | null | undefined;
+
+ /**
+ * The label text
+ */
+ label: string | null | undefined;
+
+ /**
+ * The description text
+ */
+ description: string | null | undefined;
+
+ /**
+ * The state of the step
+ * @attr {string} state
+ */
+ state: StepState;
+
+ /**
+ * Whether to exclude the item from client-side routing
+ * @attr {boolean} router-ignore
+ */
+ routerIgnore: boolean;
+
+ /**
+ * Whether the step's href matches the current page
+ */
+ readonly current: boolean;
+
+ /**
+ * Whether the step is completed
+ */
+ completed: boolean;
+
+ /**
+ * Whether the step has an error
+ */
+ error: boolean;
+
+ /**
+ * Whether the step is active
+ */
+ active: boolean;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'vaadin-step': Step;
+ }
+}
+
+export { Step };
diff --git a/packages/stepper/vaadin-step.js b/packages/stepper/vaadin-step.js
new file mode 100644
index 00000000000..1e77aed654c
--- /dev/null
+++ b/packages/stepper/vaadin-step.js
@@ -0,0 +1,6 @@
+/**
+ * @license
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+export { Step } from './src/vaadin-step.js';
diff --git a/packages/stepper/vaadin-stepper.d.ts b/packages/stepper/vaadin-stepper.d.ts
new file mode 100644
index 00000000000..381faaa4c42
--- /dev/null
+++ b/packages/stepper/vaadin-stepper.d.ts
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
+import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+export type StepperOrientation = 'horizontal' | 'vertical';
+
+/**
+ * `` is a Web Component for displaying a step-by-step process.
+ *
+ * ```html
+ *
+ * Step 1
+ * Step 2
+ * Step 3
+ *
+ * ```
+ */
+declare class Stepper extends ElementMixin(ThemableMixin(HTMLElement)) {
+ /**
+ * The orientation of the stepper
+ * @attr {string} orientation
+ */
+ orientation: StepperOrientation;
+
+ /**
+ * Sets the state of a specific step
+ */
+ setStepState(state: string, stepIndex: number): void;
+
+ /**
+ * Marks steps up to the specified index as completed
+ */
+ completeStepsUntil(untilIndex: number): void;
+
+ /**
+ * Gets the current active step
+ */
+ getActiveStep(): any | null;
+
+ /**
+ * Resets all steps to inactive state
+ */
+ reset(): void;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'vaadin-stepper': Stepper;
+ }
+}
+
+export { Stepper };
diff --git a/packages/stepper/vaadin-stepper.js b/packages/stepper/vaadin-stepper.js
new file mode 100644
index 00000000000..37baeaba5a3
--- /dev/null
+++ b/packages/stepper/vaadin-stepper.js
@@ -0,0 +1,6 @@
+/**
+ * @license
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+export { Stepper } from './src/vaadin-stepper.js';