diff --git a/vaadin-stepper-flow-parent/pom.xml b/vaadin-stepper-flow-parent/pom.xml
new file mode 100644
index 00000000000..cd578db3f83
--- /dev/null
+++ b/vaadin-stepper-flow-parent/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+
+ com.vaadin
+ vaadin-flow-components
+ 25.0-SNAPSHOT
+
+ vaadin-stepper-flow-parent
+ pom
+ Vaadin Stepper Parent
+ Vaadin Stepper Parent
+
+ vaadin-stepper-flow
+ vaadin-stepper-testbench
+
+
+
+
+ default
+
+
+ !release
+
+
+
+ vaadin-stepper-flow-integration-tests
+
+
+
+
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow-integration-tests/pom.xml b/vaadin-stepper-flow-parent/vaadin-stepper-flow-integration-tests/pom.xml
new file mode 100644
index 00000000000..2be1f1f210f
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow-integration-tests/pom.xml
@@ -0,0 +1,142 @@
+
+
+ 4.0.0
+
+ com.vaadin
+ vaadin-stepper-flow-parent
+ 25.0-SNAPSHOT
+
+ vaadin-stepper-integration-tests
+ war
+ Vaadin Stepper Integration Tests
+ Vaadin Stepper Integration Tests
+
+
+ com.vaadin
+ flow-client
+ ${flow.version}
+
+
+ com.vaadin
+ flow-html-components
+ ${flow.version}
+
+
+ com.vaadin
+ flow-lit-template
+
+
+ com.vaadin
+ flow-polymer-template
+
+
+ com.vaadin
+ flow-test-generic
+ test
+
+
+ com.vaadin
+ flow-test-util
+ test
+
+
+ com.vaadin
+ vaadin-dev-server
+
+
+ com.vaadin
+ vaadin-flow-components-test-util
+ ${project.version}
+ test
+
+
+ com.vaadin
+ vaadin-lumo-theme
+ ${project.version}
+
+
+ com.vaadin
+ vaadin-ordered-layout-flow
+ ${project.version}
+
+
+ com.vaadin
+ vaadin-stepper-flow
+ ${project.version}
+
+
+ com.vaadin
+ vaadin-stepper-testbench
+ ${project.version}
+
+
+ com.vaadin
+ vaadin-testbench-core
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.17
+
+
+
+
+
+ maven-clean-plugin
+
+
+
+ ${project.basedir}
+
+ package*.json
+ pnpm*
+ vite.generated.ts
+ types.d.ts
+ tsconfig.json
+ frontend/routes.tsx
+ frontend/App.tsx
+
+
+
+ ${project.basedir}/node_modules
+
+
+ ${project.basedir}/frontend
+
+ generated/vaadin.ts
+ generated/vite-devmode.ts
+ generated/jar-resources/
+ generated/theme/
+
+
+
+
+
+
+ com.vaadin
+ flow-maven-plugin
+ ${flow.version}
+
+
+
+ prepare-frontend
+ build-frontend
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.version}
+
+ 0
+
+ 5
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/pom.xml b/vaadin-stepper-flow-parent/vaadin-stepper-flow/pom.xml
new file mode 100644
index 00000000000..7de7e01da32
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/pom.xml
@@ -0,0 +1,97 @@
+
+
+ 4.0.0
+
+ com.vaadin
+ vaadin-stepper-flow-parent
+ 25.0-SNAPSHOT
+
+ vaadin-stepper-flow
+ jar
+ Vaadin Stepper
+ Vaadin Stepper
+
+
+ com.vaadin
+ flow-server
+ provided
+
+
+ com.vaadin
+ flow-test-generic
+ test
+
+
+ com.vaadin
+ flow-test-util
+ test
+
+
+ com.vaadin
+ vaadin-flow-components-base
+ ${project.version}
+
+
+ jakarta.platform
+ jakarta.jakartaee-web-api
+ test
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ ${project.build.outputDirectory}/META-INF/MANIFEST.MF
+
+
+
+
+
+
+
+ attach-docs
+
+
+ with-docs
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Step.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Step.java
new file mode 100644
index 00000000000..9bcb5d68be1
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Step.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.stepper;
+
+import java.util.Optional;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasComponents;
+import com.vaadin.flow.component.HasEnabled;
+import com.vaadin.flow.component.HasStyle;
+import com.vaadin.flow.component.HasText;
+import com.vaadin.flow.component.PropertyDescriptor;
+import com.vaadin.flow.component.PropertyDescriptors;
+import com.vaadin.flow.component.Synchronize;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.component.dependency.JsModule;
+import com.vaadin.flow.component.dependency.NpmPackage;
+import com.vaadin.flow.component.shared.HasTooltip;
+import com.vaadin.flow.router.QueryParameters;
+import com.vaadin.flow.router.RouteConfiguration;
+import com.vaadin.flow.router.RouteParameters;
+import com.vaadin.flow.router.Router;
+import com.vaadin.flow.server.VaadinService;
+
+/**
+ * A single step in a {@link Stepper} component.
+ *
+ * Step can contain text and/or components. It supports navigation through href
+ * property or by using Router navigation targets. Steps can have different
+ * states to indicate progress through the stepper process.
+ *
+ * @author Vaadin Ltd.
+ */
+@Tag("vaadin-step")
+@JsModule("@vaadin/stepper/src/vaadin-step.js")
+@NpmPackage(value = "@vaadin/stepper", version = "25.0.0-dev")
+public class Step extends Component
+ implements HasComponents, HasText, HasStyle, HasEnabled, HasTooltip {
+
+ private static final PropertyDescriptor hrefDescriptor = PropertyDescriptors
+ .propertyWithDefault("href", "");
+
+ private static final PropertyDescriptor targetDescriptor = PropertyDescriptors
+ .propertyWithDefault("target", "");
+
+ private static final PropertyDescriptor labelDescriptor = PropertyDescriptors
+ .propertyWithDefault("label", "");
+
+ private static final PropertyDescriptor descriptionDescriptor = PropertyDescriptors
+ .propertyWithDefault("description", "");
+
+ private static final PropertyDescriptor stateDescriptor = PropertyDescriptors
+ .propertyWithDefault("state", State.INACTIVE.getValue());
+
+ private static final PropertyDescriptor routerIgnoreDescriptor = PropertyDescriptors
+ .propertyWithDefault("routerIgnore", false);
+
+ /**
+ * Enumeration of possible step states.
+ */
+ public enum State {
+ ACTIVE("active"), COMPLETED("completed"), ERROR("error"), INACTIVE(
+ "inactive");
+
+ private final String value;
+
+ State(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public static State fromValue(String value) {
+ for (State state : values()) {
+ if (state.getValue().equals(value)) {
+ return state;
+ }
+ }
+ return INACTIVE;
+ }
+ }
+
+ /**
+ * Constructs an empty step.
+ */
+ public Step() {
+ super();
+ }
+
+ /**
+ * Constructs a step with the given label.
+ *
+ * @param label
+ * the label text
+ */
+ public Step(String label) {
+ this();
+ setLabel(label);
+ }
+
+ /**
+ * Constructs a step with the given label and description.
+ *
+ * @param label
+ * the label text
+ * @param description
+ * the description text
+ */
+ public Step(String label, String description) {
+ this(label);
+ setDescription(description);
+ }
+
+ /**
+ * Constructs a step with the given component.
+ *
+ * @param component
+ * the component to add
+ */
+ public Step(Component component) {
+ this();
+ add(component);
+ }
+
+ /**
+ * Creates a step with the given label and href.
+ *
+ * @param label
+ * the label text
+ * @param href
+ * the href to navigate to
+ * @return a new step with the specified label and href
+ */
+ public static Step withHref(String label, String href) {
+ Step step = new Step(label);
+ step.setHref(href);
+ return step;
+ }
+
+ /**
+ * Creates a step with the given component and href.
+ *
+ * @param component
+ * the component to add
+ * @param href
+ * the href to navigate to
+ * @return a new step with the specified component and href
+ */
+ public static Step withHref(Component component, String href) {
+ Step step = new Step(component);
+ step.setHref(href);
+ return step;
+ }
+
+ /**
+ * Constructs a step with the given label and navigation target.
+ *
+ * @param label
+ * the label text
+ * @param navigationTarget
+ * the navigation target class
+ */
+ public Step(String label, Class extends Component> navigationTarget) {
+ this(label);
+ setRoute(navigationTarget);
+ }
+
+ /**
+ * Constructs a step with the given label, navigation target, and route
+ * parameters.
+ *
+ * @param label
+ * the label text
+ * @param navigationTarget
+ * the navigation target class
+ * @param routeParameters
+ * the route parameters
+ */
+ public Step(String label, Class extends Component> navigationTarget,
+ RouteParameters routeParameters) {
+ this(label);
+ setRoute(navigationTarget, routeParameters);
+ }
+
+ /**
+ * Gets the href of this step.
+ *
+ * @return the href, or an empty string if no href is set
+ */
+ public String getHref() {
+ return get(hrefDescriptor);
+ }
+
+ /**
+ * Sets the href of this step.
+ *
+ * The href is the URL that the step links to. Set to an empty string to
+ * remove the href.
+ *
+ * @param href
+ * the href to set, or an empty string to remove
+ */
+ public void setHref(String href) {
+ set(hrefDescriptor, href == null ? "" : href);
+ }
+
+ /**
+ * Gets the target of this step's link.
+ *
+ * @return the target, or an empty string if no target is set
+ */
+ public String getTarget() {
+ return get(targetDescriptor);
+ }
+
+ /**
+ * Sets the target of this step's link.
+ *
+ * The target attribute specifies where to display the linked URL. Common
+ * values are "_blank" to open in a new tab, "_self" to open in the same
+ * frame, "_parent" to open in the parent frame, or "_top" to open in the
+ * full window.
+ *
+ * @param target
+ * the target to set, or an empty string to remove
+ */
+ public void setTarget(String target) {
+ set(targetDescriptor, target == null ? "" : target);
+ }
+
+ /**
+ * Gets the label of this step.
+ *
+ * @return the label text
+ */
+ public String getLabel() {
+ return get(labelDescriptor);
+ }
+
+ /**
+ * Sets the label of this step.
+ *
+ * @param label
+ * the label text to set
+ */
+ public void setLabel(String label) {
+ set(labelDescriptor, label == null ? "" : label);
+ }
+
+ /**
+ * Gets the description of this step.
+ *
+ * @return the description text
+ */
+ public String getDescription() {
+ return get(descriptionDescriptor);
+ }
+
+ /**
+ * Sets the description of this step.
+ *
+ * @param description
+ * the description text to set
+ */
+ public void setDescription(String description) {
+ set(descriptionDescriptor, description == null ? "" : description);
+ }
+
+ /**
+ * Gets the state of this step.
+ *
+ * @return the current state
+ */
+ @Synchronize(property = "state", value = "state-changed")
+ public State getState() {
+ return State.fromValue(get(stateDescriptor));
+ }
+
+ /**
+ * Sets the state of this step.
+ *
+ * @param state
+ * the state to set
+ */
+ public void setState(State state) {
+ set(stateDescriptor, state == null ? State.INACTIVE.getValue()
+ : state.getValue());
+ }
+
+ /**
+ * Gets whether this step should be ignored by client-side routers.
+ *
+ * @return {@code true} if router should ignore this step, {@code false}
+ * otherwise
+ */
+ public boolean isRouterIgnore() {
+ return get(routerIgnoreDescriptor);
+ }
+
+ /**
+ * Sets whether this step should be ignored by client-side routers.
+ *
+ * When set to {@code true}, clicking this step will cause a full page
+ * reload instead of client-side navigation.
+ *
+ * @param routerIgnore
+ * {@code true} to ignore client-side routing, {@code false}
+ * otherwise
+ */
+ public void setRouterIgnore(boolean routerIgnore) {
+ set(routerIgnoreDescriptor, routerIgnore);
+ }
+
+ /**
+ * Gets whether this step represents the current page.
+ *
+ * This property is automatically updated based on the current URL.
+ *
+ * @return {@code true} if this step represents the current page,
+ * {@code false} otherwise
+ */
+ @Synchronize(property = "current", value = "current-changed")
+ public boolean isCurrent() {
+ return getElement().getProperty("current", false);
+ }
+
+ /**
+ * Gets whether this step is completed.
+ *
+ * @return {@code true} if the step is completed, {@code false} otherwise
+ */
+ public boolean isCompleted() {
+ return getState() == State.COMPLETED;
+ }
+
+ /**
+ * Sets this step as completed.
+ */
+ public void setCompleted() {
+ setState(State.COMPLETED);
+ }
+
+ /**
+ * Gets whether this step is active.
+ *
+ * @return {@code true} if the step is active, {@code false} otherwise
+ */
+ public boolean isActive() {
+ return getState() == State.ACTIVE;
+ }
+
+ /**
+ * Sets this step as active.
+ */
+ public void setActive() {
+ setState(State.ACTIVE);
+ }
+
+ /**
+ * Gets whether this step has an error.
+ *
+ * @return {@code true} if the step has an error, {@code false} otherwise
+ */
+ public boolean isError() {
+ return getState() == State.ERROR;
+ }
+
+ /**
+ * Sets this step as having an error.
+ */
+ public void setError() {
+ setState(State.ERROR);
+ }
+
+ /**
+ * Gets whether this step is inactive.
+ *
+ * @return {@code true} if the step is inactive, {@code false} otherwise
+ */
+ public boolean isInactive() {
+ return getState() == State.INACTIVE;
+ }
+
+ /**
+ * Sets this step as inactive.
+ */
+ public void setInactive() {
+ setState(State.INACTIVE);
+ }
+
+ /**
+ * Sets the navigation target for this step using a router class.
+ *
+ * @param navigationTarget
+ * the navigation target class
+ */
+ public void setRoute(Class extends Component> navigationTarget) {
+ setRoute(navigationTarget, RouteParameters.empty());
+ }
+
+ /**
+ * Sets the navigation target for this step using a router class and route
+ * parameters.
+ *
+ * @param navigationTarget
+ * the navigation target class
+ * @param routeParameters
+ * the route parameters
+ */
+ public void setRoute(Class extends Component> navigationTarget,
+ RouteParameters routeParameters) {
+ setRoute(getRouter(), navigationTarget, routeParameters,
+ QueryParameters.empty());
+ }
+
+ /**
+ * Sets the navigation target for this step using a router class, route
+ * parameters, and query parameters.
+ *
+ * @param navigationTarget
+ * the navigation target class
+ * @param routeParameters
+ * the route parameters
+ * @param queryParameters
+ * the query parameters
+ */
+ public void setRoute(Class extends Component> navigationTarget,
+ RouteParameters routeParameters,
+ QueryParameters queryParameters) {
+ setRoute(getRouter(), navigationTarget, routeParameters,
+ queryParameters);
+ }
+
+ /**
+ * Sets the navigation target for this step using a specific router.
+ *
+ * @param router
+ * the router to use, or {@code null} to use the default
+ * @param navigationTarget
+ * the navigation target class
+ * @param routeParameters
+ * the route parameters
+ * @param queryParameters
+ * the query parameters
+ */
+ public void setRoute(Router router,
+ Class extends Component> navigationTarget,
+ RouteParameters routeParameters,
+ QueryParameters queryParameters) {
+ if (router == null) {
+ router = getRouter();
+ }
+
+ if (navigationTarget != null) {
+ String url = RouteConfiguration.forRegistry(router.getRegistry())
+ .getUrl(navigationTarget, routeParameters);
+
+ if (!queryParameters.getParameters().isEmpty()) {
+ url = url + "?" + queryParameters.getQueryString();
+ }
+
+ setHref(url);
+ } else {
+ setHref("");
+ }
+ }
+
+ private Router getRouter() {
+ Router router = null;
+ if (getElement().getNode().isAttached()) {
+ router = getUI().map(ui -> ui.getInternals().getRouter())
+ .orElse(null);
+ }
+ if (router == null) {
+ router = VaadinService.getCurrent().getRouter();
+ }
+ if (router == null) {
+ throw new IllegalStateException(
+ "Cannot find a router to use for navigation");
+ }
+ return router;
+ }
+}
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Stepper.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Stepper.java
new file mode 100644
index 00000000000..6b6b6517e9d
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Stepper.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.stepper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasSize;
+import com.vaadin.flow.component.HasStyle;
+import com.vaadin.flow.component.PropertyDescriptor;
+import com.vaadin.flow.component.PropertyDescriptors;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.component.dependency.JsModule;
+import com.vaadin.flow.component.dependency.NpmPackage;
+import com.vaadin.flow.component.shared.HasThemeVariant;
+import com.vaadin.flow.dom.Element;
+
+/**
+ * Stepper is a navigation component that displays progress through a sequence
+ * of logical steps. It guides users through a step-by-step process and allows
+ * navigation between steps.
+ *
+ * Use Stepper when you want to break a complex process into smaller, manageable
+ * steps and show the user their progress through the process.
+ *
+ * {@link Step} components can be added to this component with the
+ * {@link #add(Step...)} method or the {@link #Stepper(Step...)} constructor.
+ *
+ * @author Vaadin Ltd.
+ */
+@Tag("vaadin-stepper")
+@JsModule("@vaadin/stepper/src/vaadin-stepper.js")
+@NpmPackage(value = "@vaadin/stepper", version = "25.0.0-dev")
+public class Stepper extends Component
+ implements HasSize, HasStyle, HasThemeVariant {
+
+ private static final PropertyDescriptor orientationDescriptor = PropertyDescriptors
+ .propertyWithDefault("orientation", Orientation.VERTICAL.getValue());
+
+ /**
+ * The valid orientations for {@link Stepper} instances.
+ */
+ public enum Orientation {
+ HORIZONTAL("horizontal"), VERTICAL("vertical");
+
+ private final String value;
+
+ Orientation(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+ }
+
+ /**
+ * Constructs an empty stepper component with vertical orientation.
+ */
+ public Stepper() {
+ super();
+ }
+
+ /**
+ * Constructs a stepper component with the given steps.
+ *
+ * @param steps
+ * the steps to add
+ */
+ public Stepper(Step... steps) {
+ this();
+ add(steps);
+ }
+
+ /**
+ * Gets the orientation of the stepper.
+ *
+ * @return the orientation
+ */
+ public Orientation getOrientation() {
+ String value = get(orientationDescriptor);
+ for (Orientation orientation : Orientation.values()) {
+ if (orientation.getValue().equals(value)) {
+ return orientation;
+ }
+ }
+ return Orientation.VERTICAL;
+ }
+
+ /**
+ * Sets the orientation of the stepper.
+ *
+ * @param orientation
+ * the orientation to set
+ */
+ public void setOrientation(Orientation orientation) {
+ Objects.requireNonNull(orientation, "Orientation cannot be null");
+ set(orientationDescriptor, orientation.getValue());
+ }
+
+ /**
+ * Adds the given steps to the component.
+ *
+ * @param steps
+ * the steps to add, not {@code null}
+ */
+ public void add(Step... steps) {
+ Objects.requireNonNull(steps, "Steps to add cannot be null");
+ Arrays.stream(steps).map(step -> Objects.requireNonNull(step,
+ "Individual step to add cannot be null"))
+ .map(Step::getElement)
+ .forEach(getElement()::appendChild);
+ }
+
+ /**
+ * Removes the given steps from the component.
+ *
+ * @param steps
+ * the steps to remove, not {@code null}
+ */
+ public void remove(Step... steps) {
+ Objects.requireNonNull(steps, "Steps to remove cannot be null");
+ Arrays.stream(steps).map(step -> Objects.requireNonNull(step,
+ "Individual step to remove cannot be null"))
+ .map(Step::getElement)
+ .forEach(getElement()::removeChild);
+ }
+
+ /**
+ * Removes all steps from the component.
+ */
+ public void removeAll() {
+ getElement().removeAllChildren();
+ }
+
+ /**
+ * Gets the step at the given index.
+ *
+ * @param index
+ * the index of the step to get
+ * @return the step at the given index
+ * @throws IndexOutOfBoundsException
+ * if the index is out of range
+ */
+ public Step getStepAt(int index) {
+ if (index < 0 || index >= getStepCount()) {
+ throw new IndexOutOfBoundsException("Index: " + index
+ + ", Size: " + getStepCount());
+ }
+ Iterator iterator = getSteps().iterator();
+ for (int i = 0; i < index; i++) {
+ iterator.next();
+ }
+ return iterator.next();
+ }
+
+ /**
+ * Gets the number of steps in the component.
+ *
+ * @return the number of steps
+ */
+ public int getStepCount() {
+ return (int) getSteps().count();
+ }
+
+ /**
+ * Gets the index of the given step.
+ *
+ * @param step
+ * the step to get the index of
+ * @return the index of the step, or -1 if not found
+ */
+ public int indexOf(Step step) {
+ if (step == null) {
+ return -1;
+ }
+ Iterator iterator = getSteps().iterator();
+ int index = 0;
+ while (iterator.hasNext()) {
+ if (iterator.next().equals(step)) {
+ return index;
+ }
+ index++;
+ }
+ return -1;
+ }
+
+ /**
+ * Replaces the step at the given index with a new step.
+ *
+ * @param index
+ * the index of the step to replace
+ * @param newStep
+ * the new step to set, not {@code null}
+ * @throws IndexOutOfBoundsException
+ * if the index is out of range
+ */
+ public void replace(int index, Step newStep) {
+ Objects.requireNonNull(newStep, "New step cannot be null");
+ if (index < 0 || index >= getStepCount()) {
+ throw new IndexOutOfBoundsException("Index: " + index
+ + ", Size: " + getStepCount());
+ }
+
+ Step oldStep = getStepAt(index);
+ Element parentElement = getElement();
+ List children = new ArrayList<>();
+ parentElement.getChildren().forEach(children::add);
+
+ int elementIndex = children.indexOf(oldStep.getElement());
+ if (elementIndex >= 0) {
+ parentElement.insertChild(elementIndex, newStep.getElement());
+ parentElement.removeChild(oldStep.getElement());
+ }
+ }
+
+ /**
+ * Gets all steps in the component as a stream.
+ *
+ * @return a stream of all steps
+ */
+ public Stream getSteps() {
+ return getElement().getChildren()
+ .filter(element -> element.getComponent()
+ .filter(Step.class::isInstance).isPresent())
+ .map(element -> element.getComponent().map(Step.class::cast)
+ .get());
+ }
+
+ /**
+ * Gets the currently active step.
+ *
+ * @return the active step, or {@code null} if no step is active
+ */
+ public Step getActiveStep() {
+ return getSteps().filter(Step::isActive).findFirst().orElse(null);
+ }
+
+ /**
+ * Sets the state of a specific step by index.
+ *
+ * @param state
+ * the state to set
+ * @param stepIndex
+ * the index of the step
+ */
+ public void setStepState(Step.State state, int stepIndex) {
+ Objects.requireNonNull(state, "State cannot be null");
+ if (stepIndex >= 0 && stepIndex < getStepCount()) {
+ getStepAt(stepIndex).setState(state);
+ }
+ }
+
+ /**
+ * Marks steps as completed up to the specified index (inclusive).
+ *
+ * @param untilIndex
+ * the index up to which steps should be marked as completed
+ */
+ public void completeStepsUntil(int untilIndex) {
+ if (untilIndex < 0) {
+ return;
+ }
+ int count = getStepCount();
+ for (int i = 0; i <= untilIndex && i < count; i++) {
+ getStepAt(i).setState(Step.State.COMPLETED);
+ }
+ }
+
+ /**
+ * Resets all steps to inactive state.
+ */
+ public void reset() {
+ getSteps().forEach(step -> step.setState(Step.State.INACTIVE));
+ }
+}
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/StepperVariant.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/StepperVariant.java
new file mode 100644
index 00000000000..450ae749ddf
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/StepperVariant.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.stepper;
+
+import com.vaadin.flow.component.shared.ThemeVariant;
+
+/**
+ * Theme variants for the {@link Stepper} component.
+ *
+ * @author Vaadin Ltd.
+ */
+public enum StepperVariant implements ThemeVariant {
+
+ /**
+ * Small size variant for compact display.
+ */
+ LUMO_SMALL("small");
+
+ private final String variant;
+
+ StepperVariant(String variant) {
+ this.variant = variant;
+ }
+
+ /**
+ * Gets the variant name.
+ *
+ * @return variant name
+ */
+ @Override
+ public String getVariantName() {
+ return variant;
+ }
+}
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepTest.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepTest.java
new file mode 100644
index 00000000000..8b022b45fcb
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.stepper;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.RouteParameters;
+
+/**
+ * Tests for {@link Step}.
+ */
+public class StepTest {
+
+ @Test
+ public void createEmptyStep() {
+ Step step = new Step();
+
+ Assert.assertEquals("Tag name is invalid", "vaadin-step",
+ step.getElement().getTag());
+ Assert.assertEquals("Label should be empty", "", step.getLabel());
+ Assert.assertEquals("Description should be empty", "",
+ step.getDescription());
+ Assert.assertEquals("Href should be empty", "", step.getHref());
+ Assert.assertEquals("Default state should be inactive",
+ Step.State.INACTIVE, step.getState());
+ }
+
+ @Test
+ public void createStepWithLabel() {
+ Step step = new Step("Personal Info");
+
+ Assert.assertEquals("Label is invalid", "Personal Info",
+ step.getLabel());
+ Assert.assertEquals("Href should be empty", "", step.getHref());
+ }
+
+ @Test
+ public void createStepWithLabelAndDescription() {
+ Step step = new Step("Personal Info", "Enter your details");
+
+ Assert.assertEquals("Label is invalid", "Personal Info",
+ step.getLabel());
+ Assert.assertEquals("Description is invalid", "Enter your details",
+ step.getDescription());
+ }
+
+ @Test
+ public void createStepWithLabelAndHref() {
+ Step step = Step.withHref("Personal Info", "/personal");
+
+ Assert.assertEquals("Label is invalid", "Personal Info",
+ step.getLabel());
+ Assert.assertEquals("Href is invalid", "/personal", step.getHref());
+ }
+
+ @Test
+ public void createStepWithComponent() {
+ Div div = new Div("Content");
+ Step step = new Step(div);
+
+ Assert.assertEquals("Child count is invalid", 1,
+ step.getElement().getChildCount());
+ Assert.assertEquals("Child component is invalid", div.getElement(),
+ step.getElement().getChild(0));
+ }
+
+ @Test
+ public void createStepWithComponentAndHref() {
+ Div div = new Div("Content");
+ Step step = Step.withHref(div, "/step");
+
+ Assert.assertEquals("Child count is invalid", 1,
+ step.getElement().getChildCount());
+ Assert.assertEquals("Href is invalid", "/step", step.getHref());
+ }
+
+ @Test
+ public void setHref() {
+ Step step = new Step();
+
+ step.setHref("/products");
+ Assert.assertEquals("Href is invalid", "/products", step.getHref());
+
+ step.setHref(null);
+ Assert.assertEquals("Href should be empty", "", step.getHref());
+
+ step.setHref("");
+ Assert.assertEquals("Href should be empty", "", step.getHref());
+ }
+
+ @Test
+ public void setTarget() {
+ Step step = new Step();
+
+ step.setTarget("_blank");
+ Assert.assertEquals("Target is invalid", "_blank", step.getTarget());
+
+ step.setTarget(null);
+ Assert.assertEquals("Target should be empty", "", step.getTarget());
+
+ step.setTarget("");
+ Assert.assertEquals("Target should be empty", "", step.getTarget());
+ }
+
+ @Test
+ public void setLabel() {
+ Step step = new Step();
+
+ step.setLabel("Payment");
+ Assert.assertEquals("Label is invalid", "Payment", step.getLabel());
+
+ step.setLabel(null);
+ Assert.assertEquals("Label should be empty", "", step.getLabel());
+ }
+
+ @Test
+ public void setDescription() {
+ Step step = new Step();
+
+ step.setDescription("Choose payment method");
+ Assert.assertEquals("Description is invalid", "Choose payment method",
+ step.getDescription());
+
+ step.setDescription(null);
+ Assert.assertEquals("Description should be empty", "",
+ step.getDescription());
+ }
+
+ @Test
+ public void setState() {
+ Step step = new Step();
+
+ Assert.assertEquals("Default state should be inactive",
+ Step.State.INACTIVE, step.getState());
+
+ step.setState(Step.State.ACTIVE);
+ Assert.assertEquals("State should be active", Step.State.ACTIVE,
+ step.getState());
+
+ step.setState(Step.State.COMPLETED);
+ Assert.assertEquals("State should be completed", Step.State.COMPLETED,
+ step.getState());
+
+ step.setState(Step.State.ERROR);
+ Assert.assertEquals("State should be error", Step.State.ERROR,
+ step.getState());
+
+ step.setState(null);
+ Assert.assertEquals("State should be inactive", Step.State.INACTIVE,
+ step.getState());
+ }
+
+ @Test
+ public void stateConvenienceMethods() {
+ Step step = new Step();
+
+ // Test isInactive and setInactive
+ Assert.assertTrue("Step should be inactive by default",
+ step.isInactive());
+ step.setInactive();
+ Assert.assertTrue("Step should be inactive", step.isInactive());
+ Assert.assertFalse("Step should not be active", step.isActive());
+ Assert.assertFalse("Step should not be completed", step.isCompleted());
+ Assert.assertFalse("Step should not be error", step.isError());
+
+ // Test isActive and setActive
+ step.setActive();
+ Assert.assertTrue("Step should be active", step.isActive());
+ Assert.assertFalse("Step should not be inactive", step.isInactive());
+ Assert.assertFalse("Step should not be completed", step.isCompleted());
+ Assert.assertFalse("Step should not be error", step.isError());
+
+ // Test isCompleted and setCompleted
+ step.setCompleted();
+ Assert.assertTrue("Step should be completed", step.isCompleted());
+ Assert.assertFalse("Step should not be inactive", step.isInactive());
+ Assert.assertFalse("Step should not be active", step.isActive());
+ Assert.assertFalse("Step should not be error", step.isError());
+
+ // Test isError and setError
+ step.setError();
+ Assert.assertTrue("Step should be error", step.isError());
+ Assert.assertFalse("Step should not be inactive", step.isInactive());
+ Assert.assertFalse("Step should not be active", step.isActive());
+ Assert.assertFalse("Step should not be completed", step.isCompleted());
+ }
+
+ @Test
+ public void setRouterIgnore() {
+ Step step = new Step();
+
+ Assert.assertFalse("Default routerIgnore should be false",
+ step.isRouterIgnore());
+
+ step.setRouterIgnore(true);
+ Assert.assertTrue("RouterIgnore should be true", step.isRouterIgnore());
+
+ step.setRouterIgnore(false);
+ Assert.assertFalse("RouterIgnore should be false",
+ step.isRouterIgnore());
+ }
+
+ @Test
+ public void setText() {
+ Step step = new Step();
+
+ step.setText("Step Content");
+ Assert.assertEquals("Text is invalid", "Step Content", step.getText());
+
+ step.setText(null);
+ Assert.assertEquals("Text should be empty", "", step.getText());
+ }
+
+ @Test
+ public void addComponent() {
+ Step step = new Step();
+ Div div1 = new Div("First");
+ Div div2 = new Div("Second");
+
+ step.add(div1, div2);
+
+ Assert.assertEquals("Child count is invalid", 2,
+ step.getElement().getChildCount());
+ Assert.assertEquals("First child is invalid", div1.getElement(),
+ step.getElement().getChild(0));
+ Assert.assertEquals("Second child is invalid", div2.getElement(),
+ step.getElement().getChild(1));
+ }
+
+ @Test
+ public void removeComponent() {
+ Step step = new Step();
+ Div div1 = new Div("First");
+ Div div2 = new Div("Second");
+
+ step.add(div1, div2);
+ step.remove(div1);
+
+ Assert.assertEquals("Child count is invalid", 1,
+ step.getElement().getChildCount());
+ Assert.assertEquals("Remaining child is invalid", div2.getElement(),
+ step.getElement().getChild(0));
+ }
+
+ @Test
+ public void removeAllComponents() {
+ Step step = new Step();
+ Div div1 = new Div("First");
+ Div div2 = new Div("Second");
+
+ step.add(div1, div2);
+ step.removeAll();
+
+ Assert.assertEquals("Child count should be 0", 0,
+ step.getElement().getChildCount());
+ }
+
+ @Test
+ public void setEnabled() {
+ Step step = new Step();
+
+ Assert.assertTrue("Default enabled should be true", step.isEnabled());
+
+ step.setEnabled(false);
+ Assert.assertFalse("Enabled should be false", step.isEnabled());
+
+ step.setEnabled(true);
+ Assert.assertTrue("Enabled should be true", step.isEnabled());
+ }
+
+ @Test
+ public void setTooltip() {
+ Step step = new Step();
+
+ // HasTooltip creates a tooltip immediately, so we check if text is
+ // null/empty
+ Assert.assertTrue("Default tooltip text should be null or empty",
+ step.getTooltip() == null || step.getTooltip().getText() == null
+ || step.getTooltip().getText().isEmpty());
+
+ step.setTooltipText("This is a tooltip");
+ Assert.assertNotNull("Tooltip should not be null", step.getTooltip());
+ Assert.assertEquals("Tooltip text is invalid", "This is a tooltip",
+ step.getTooltip().getText());
+ }
+
+ @Test
+ public void stateFromValue() {
+ Assert.assertEquals("Active state conversion", Step.State.ACTIVE,
+ Step.State.fromValue("active"));
+ Assert.assertEquals("Completed state conversion", Step.State.COMPLETED,
+ Step.State.fromValue("completed"));
+ Assert.assertEquals("Error state conversion", Step.State.ERROR,
+ Step.State.fromValue("error"));
+ Assert.assertEquals("Inactive state conversion", Step.State.INACTIVE,
+ Step.State.fromValue("inactive"));
+ Assert.assertEquals("Unknown state conversion", Step.State.INACTIVE,
+ Step.State.fromValue("unknown"));
+ Assert.assertEquals("Null state conversion", Step.State.INACTIVE,
+ Step.State.fromValue(null));
+ }
+
+ @Test
+ public void constructorsWithNavigationTarget() {
+ // Test constructor with navigation target - will throw without router
+ try {
+ Step step1 = new Step("Test", TestView.class);
+ Assert.fail("Should throw exception without router");
+ } catch (IllegalStateException | NullPointerException e) {
+ // Expected if router configuration is not available
+ // Can be either IllegalStateException or NullPointerException
+ // depending on the state
+ Assert.assertTrue("Expected router exception",
+ e.getMessage() != null
+ && (e.getMessage().contains("Cannot find a router")
+ || e.getMessage().contains(
+ "VaadinService.getCurrent()")));
+ }
+
+ // Test constructor with navigation target and route parameters - will
+ // throw without router
+ try {
+ RouteParameters params = RouteParameters.empty();
+ Step step2 = new Step("Test", TestView.class, params);
+ Assert.fail("Should throw exception without router");
+ } catch (IllegalStateException | NullPointerException e) {
+ // Expected if router configuration is not available
+ // Can be either IllegalStateException or NullPointerException
+ // depending on the state
+ Assert.assertTrue("Expected router exception",
+ e.getMessage() != null
+ && (e.getMessage().contains("Cannot find a router")
+ || e.getMessage().contains(
+ "VaadinService.getCurrent()")));
+ }
+ }
+
+ // Test view class for navigation tests
+ private static class TestView extends Div {
+ }
+}
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepperTest.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepperTest.java
new file mode 100644
index 00000000000..8ecafc007c1
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepperTest.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.stepper;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+/**
+ * Tests for {@link Stepper}.
+ */
+public class StepperTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void createStepperInDefaultState() {
+ Stepper stepper = new Stepper();
+
+ Assert.assertEquals("Initial step count is invalid", 0,
+ stepper.getStepCount());
+ Assert.assertEquals("Tag name is invalid", "vaadin-stepper",
+ stepper.getElement().getTag());
+ Assert.assertEquals("Default orientation is invalid",
+ Stepper.Orientation.VERTICAL, stepper.getOrientation());
+ }
+
+ @Test
+ public void createStepperWithSteps() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step step3 = new Step("Step 3");
+ Stepper stepper = new Stepper(step1, step2, step3);
+
+ Assert.assertEquals("Initial step count is invalid", 3,
+ stepper.getStepCount());
+ Assert.assertEquals("First step is invalid", step1,
+ stepper.getStepAt(0));
+ Assert.assertEquals("Second step is invalid", step2,
+ stepper.getStepAt(1));
+ Assert.assertEquals("Third step is invalid", step3,
+ stepper.getStepAt(2));
+ }
+
+ @Test
+ public void setOrientation() {
+ Stepper stepper = new Stepper();
+
+ stepper.setOrientation(Stepper.Orientation.HORIZONTAL);
+ Assert.assertEquals("Orientation is invalid",
+ Stepper.Orientation.HORIZONTAL, stepper.getOrientation());
+
+ stepper.setOrientation(Stepper.Orientation.VERTICAL);
+ Assert.assertEquals("Orientation is invalid",
+ Stepper.Orientation.VERTICAL, stepper.getOrientation());
+ }
+
+ @Test
+ public void setOrientationNull_throwsException() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("Orientation cannot be null");
+
+ Stepper stepper = new Stepper();
+ stepper.setOrientation(null);
+ }
+
+ @Test
+ public void addSteps() {
+ Stepper stepper = new Stepper();
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+
+ stepper.add(step1, step2);
+
+ Assert.assertEquals("Step count after add is invalid", 2,
+ stepper.getStepCount());
+ Assert.assertEquals("First step is invalid", step1,
+ stepper.getStepAt(0));
+ Assert.assertEquals("Second step is invalid", step2,
+ stepper.getStepAt(1));
+ }
+
+ @Test
+ public void addNullSteps_throwsException() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("Steps to add cannot be null");
+
+ Stepper stepper = new Stepper();
+ stepper.add((Step[]) null);
+ }
+
+ @Test
+ public void addStepWithNull_throwsException() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("Individual step to add cannot be null");
+
+ Stepper stepper = new Stepper();
+ stepper.add(new Step("Step 1"), null);
+ }
+
+ @Test
+ public void removeSteps() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step step3 = new Step("Step 3");
+ Stepper stepper = new Stepper(step1, step2, step3);
+
+ stepper.remove(step2);
+
+ Assert.assertEquals("Step count after remove is invalid", 2,
+ stepper.getStepCount());
+ Assert.assertEquals("First step is invalid", step1,
+ stepper.getStepAt(0));
+ Assert.assertEquals("Second step is invalid", step3,
+ stepper.getStepAt(1));
+ }
+
+ @Test
+ public void removeAll() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Stepper stepper = new Stepper(step1, step2);
+
+ stepper.removeAll();
+
+ Assert.assertEquals("Step count after removeAll is invalid", 0,
+ stepper.getStepCount());
+ }
+
+ @Test
+ public void getStepAt_invalidIndex_throwsException() {
+ thrown.expect(IndexOutOfBoundsException.class);
+ thrown.expectMessage("Index: 0, Size: 0");
+
+ Stepper stepper = new Stepper();
+ stepper.getStepAt(0);
+ }
+
+ @Test
+ public void getStepAt_negativeIndex_throwsException() {
+ thrown.expect(IndexOutOfBoundsException.class);
+ thrown.expectMessage("Index: -1, Size: 0");
+
+ Stepper stepper = new Stepper();
+ stepper.getStepAt(-1);
+ }
+
+ @Test
+ public void indexOf_returnsCorrectIndex() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step step3 = new Step("Step 3");
+ Stepper stepper = new Stepper(step1, step2, step3);
+
+ Assert.assertEquals("Index of step1 is invalid", 0,
+ stepper.indexOf(step1));
+ Assert.assertEquals("Index of step2 is invalid", 1,
+ stepper.indexOf(step2));
+ Assert.assertEquals("Index of step3 is invalid", 2,
+ stepper.indexOf(step3));
+ }
+
+ @Test
+ public void indexOf_stepNotFound_returnsNegativeOne() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step stepNotAdded = new Step("Step 3");
+ Stepper stepper = new Stepper(step1, step2);
+
+ Assert.assertEquals("Index of not added step should be -1", -1,
+ stepper.indexOf(stepNotAdded));
+ }
+
+ @Test
+ public void indexOf_nullStep_returnsNegativeOne() {
+ Step step1 = new Step("Step 1");
+ Stepper stepper = new Stepper(step1);
+
+ Assert.assertEquals("Index of null should be -1", -1,
+ stepper.indexOf(null));
+ }
+
+ @Test
+ public void replace_validIndex() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step step3 = new Step("Step 3");
+ Step newStep = new Step("New Step");
+ Stepper stepper = new Stepper(step1, step2, step3);
+
+ stepper.replace(1, newStep);
+
+ Assert.assertEquals("Step count after replace is invalid", 3,
+ stepper.getStepCount());
+ Assert.assertEquals("First step is invalid", step1,
+ stepper.getStepAt(0));
+ Assert.assertEquals("Replaced step is invalid", newStep,
+ stepper.getStepAt(1));
+ Assert.assertEquals("Third step is invalid", step3,
+ stepper.getStepAt(2));
+ }
+
+ @Test
+ public void replace_invalidIndex_throwsException() {
+ thrown.expect(IndexOutOfBoundsException.class);
+ thrown.expectMessage("Index: 3, Size: 2");
+
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step newStep = new Step("New Step");
+ Stepper stepper = new Stepper(step1, step2);
+
+ stepper.replace(3, newStep);
+ }
+
+ @Test
+ public void replace_nullStep_throwsException() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("New step cannot be null");
+
+ Step step1 = new Step("Step 1");
+ Stepper stepper = new Stepper(step1);
+
+ stepper.replace(0, null);
+ }
+
+ @Test
+ public void getSteps_returnsAllSteps() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step step3 = new Step("Step 3");
+ Stepper stepper = new Stepper(step1, step2, step3);
+
+ Step[] steps = stepper.getSteps().toArray(Step[]::new);
+
+ Assert.assertEquals("Step count is invalid", 3, steps.length);
+ Assert.assertEquals("First step is invalid", step1, steps[0]);
+ Assert.assertEquals("Second step is invalid", step2, steps[1]);
+ Assert.assertEquals("Third step is invalid", step3, steps[2]);
+ }
+
+ @Test
+ public void getActiveStep() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step step3 = new Step("Step 3");
+ Stepper stepper = new Stepper(step1, step2, step3);
+
+ Assert.assertNull("Initially no step should be active",
+ stepper.getActiveStep());
+
+ step2.setState(Step.State.ACTIVE);
+ Assert.assertEquals("Active step should be step2", step2,
+ stepper.getActiveStep());
+ }
+
+ @Test
+ public void setStepState() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Stepper stepper = new Stepper(step1, step2);
+
+ stepper.setStepState(Step.State.COMPLETED, 0);
+ Assert.assertEquals("Step state should be completed",
+ Step.State.COMPLETED, step1.getState());
+
+ stepper.setStepState(Step.State.ACTIVE, 1);
+ Assert.assertEquals("Step state should be active", Step.State.ACTIVE,
+ step2.getState());
+ }
+
+ @Test
+ public void setStepState_nullState_throwsException() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("State cannot be null");
+
+ Step step1 = new Step("Step 1");
+ Stepper stepper = new Stepper(step1);
+
+ stepper.setStepState(null, 0);
+ }
+
+ @Test
+ public void setStepState_invalidIndex_ignored() {
+ Step step1 = new Step("Step 1");
+ Stepper stepper = new Stepper(step1);
+
+ // Should not throw exception, just ignore
+ stepper.setStepState(Step.State.COMPLETED, 5);
+ stepper.setStepState(Step.State.COMPLETED, -1);
+
+ Assert.assertEquals("Step state should remain inactive",
+ Step.State.INACTIVE, step1.getState());
+ }
+
+ @Test
+ public void completeStepsUntil() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step step3 = new Step("Step 3");
+ Stepper stepper = new Stepper(step1, step2, step3);
+
+ stepper.completeStepsUntil(1);
+
+ Assert.assertEquals("Step 1 should be completed", Step.State.COMPLETED,
+ step1.getState());
+ Assert.assertEquals("Step 2 should be completed", Step.State.COMPLETED,
+ step2.getState());
+ Assert.assertEquals("Step 3 should remain inactive",
+ Step.State.INACTIVE, step3.getState());
+ }
+
+ @Test
+ public void completeStepsUntil_negativeIndex_ignored() {
+ Step step1 = new Step("Step 1");
+ Stepper stepper = new Stepper(step1);
+
+ stepper.completeStepsUntil(-1);
+
+ Assert.assertEquals("Step should remain inactive", Step.State.INACTIVE,
+ step1.getState());
+ }
+
+ @Test
+ public void completeStepsUntil_indexBeyondRange() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Stepper stepper = new Stepper(step1, step2);
+
+ stepper.completeStepsUntil(5);
+
+ Assert.assertEquals("Step 1 should be completed", Step.State.COMPLETED,
+ step1.getState());
+ Assert.assertEquals("Step 2 should be completed", Step.State.COMPLETED,
+ step2.getState());
+ }
+
+ @Test
+ public void reset() {
+ Step step1 = new Step("Step 1");
+ Step step2 = new Step("Step 2");
+ Step step3 = new Step("Step 3");
+ Stepper stepper = new Stepper(step1, step2, step3);
+
+ step1.setState(Step.State.COMPLETED);
+ step2.setState(Step.State.ACTIVE);
+ step3.setState(Step.State.ERROR);
+
+ stepper.reset();
+
+ Assert.assertEquals("Step 1 should be inactive", Step.State.INACTIVE,
+ step1.getState());
+ Assert.assertEquals("Step 2 should be inactive", Step.State.INACTIVE,
+ step2.getState());
+ Assert.assertEquals("Step 3 should be inactive", Step.State.INACTIVE,
+ step3.getState());
+ }
+
+ @Test
+ public void themeVariants() {
+ Stepper stepper = new Stepper();
+
+ stepper.addThemeVariants(StepperVariant.LUMO_SMALL);
+ Assert.assertTrue("Should have small variant",
+ stepper.getThemeNames().contains("small"));
+
+ stepper.removeThemeVariants(StepperVariant.LUMO_SMALL);
+ Assert.assertFalse("Should not have small variant",
+ stepper.getThemeNames().contains("small"));
+ }
+}
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-testbench/pom.xml b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/pom.xml
new file mode 100644
index 00000000000..5cdbfcd8a6e
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+ com.vaadin
+ vaadin-stepper-flow-parent
+ 25.0-SNAPSHOT
+
+ vaadin-stepper-testbench
+ jar
+ Vaadin Stepper Testbench API
+ Vaadin Stepper Testbench API
+
+
+ com.vaadin
+ vaadin-testbench-shared
+ provided
+
+
+
+
+
+
+
+ attach-docs
+
+
+ with-docs
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepElement.java b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepElement.java
new file mode 100644
index 00000000000..1e0237ed792
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepElement.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.stepper.testbench;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elementsbase.Element;
+
+/**
+ * TestBench element for the {@code } component.
+ *
+ * @author Vaadin Ltd.
+ */
+@Element("vaadin-step")
+public class StepElement extends TestBenchElement {
+
+ /**
+ * Gets the href of this step.
+ *
+ * @return the href, or {@code null} if not set
+ */
+ public String getHref() {
+ return getPropertyString("href");
+ }
+
+ /**
+ * Gets the target of this step.
+ *
+ * @return the target, or {@code null} if not set
+ */
+ public String getTarget() {
+ return getPropertyString("target");
+ }
+
+ /**
+ * Gets the label of this step.
+ *
+ * @return the label text
+ */
+ public String getLabel() {
+ return getPropertyString("label");
+ }
+
+ /**
+ * Gets the description of this step.
+ *
+ * @return the description text
+ */
+ public String getDescription() {
+ return getPropertyString("description");
+ }
+
+ /**
+ * Gets the state of this step.
+ *
+ * @return the state ("active", "completed", "error", or "inactive")
+ */
+ public String getState() {
+ return getPropertyString("state");
+ }
+
+ /**
+ * Checks if this step is marked as the current page.
+ *
+ * @return {@code true} if this is the current page, {@code false}
+ * otherwise
+ */
+ public boolean isCurrent() {
+ return hasAttribute("current");
+ }
+
+ /**
+ * Checks if this step is disabled.
+ *
+ * @return {@code true} if disabled, {@code false} otherwise
+ */
+ public boolean isDisabled() {
+ return hasAttribute("disabled");
+ }
+
+ /**
+ * Checks if this step should be ignored by client-side routers.
+ *
+ * @return {@code true} if router should ignore this step, {@code false}
+ * otherwise
+ */
+ public boolean isRouterIgnore() {
+ return getPropertyBoolean("routerIgnore");
+ }
+
+ /**
+ * Checks if this step is in active state.
+ *
+ * @return {@code true} if the step is active, {@code false} otherwise
+ */
+ public boolean isActive() {
+ return "active".equals(getState());
+ }
+
+ /**
+ * Checks if this step is in completed state.
+ *
+ * @return {@code true} if the step is completed, {@code false} otherwise
+ */
+ public boolean isCompleted() {
+ return "completed".equals(getState());
+ }
+
+ /**
+ * Checks if this step is in error state.
+ *
+ * @return {@code true} if the step is in error state, {@code false}
+ * otherwise
+ */
+ public boolean isError() {
+ return "error".equals(getState());
+ }
+
+ /**
+ * Checks if this step is in inactive state.
+ *
+ * @return {@code true} if the step is inactive, {@code false} otherwise
+ */
+ public boolean isInactive() {
+ return "inactive".equals(getState());
+ }
+
+ /**
+ * Gets the tooltip text of this step.
+ *
+ * @return the tooltip text, or {@code null} if not set
+ */
+ public String getTooltipText() {
+ return getAttribute("title");
+ }
+
+ /**
+ * Clicks on the indicator part of this step. This will navigate to the
+ * href if one is set and the step is not disabled.
+ */
+ public void clickIndicator() {
+ // Click on the indicator part in the shadow DOM
+ getCommandExecutor().executeScript(
+ "arguments[0].shadowRoot.querySelector('[part=\"indicator\"]').click();",
+ this);
+ }
+
+ /**
+ * Checks if this step has a connector line to the next step.
+ *
+ * @return {@code true} if a connector is visible, {@code false} otherwise
+ */
+ public boolean hasConnector() {
+ return (Boolean) getCommandExecutor().executeScript(
+ "return !!arguments[0].shadowRoot.querySelector('[part=\"connector\"]');",
+ this);
+ }
+
+ /**
+ * Gets the text content of the step indicator (usually a number or icon).
+ *
+ * @return the indicator text content
+ */
+ public String getIndicatorText() {
+ return (String) getCommandExecutor().executeScript(
+ "const indicator = arguments[0].shadowRoot.querySelector('[part=\"indicator\"]');"
+ + "return indicator ? indicator.textContent.trim() : '';",
+ this);
+ }
+
+ /**
+ * Checks if the step indicator shows a checkmark (completed state).
+ *
+ * @return {@code true} if checkmark is visible, {@code false} otherwise
+ */
+ public boolean hasCheckmark() {
+ return (Boolean) getCommandExecutor().executeScript(
+ "const indicator = arguments[0].shadowRoot.querySelector('[part=\"indicator\"]');"
+ + "return indicator && indicator.textContent.includes('✓');",
+ this);
+ }
+
+ /**
+ * Checks if the step indicator shows an error mark (error state).
+ *
+ * @return {@code true} if error mark is visible, {@code false} otherwise
+ */
+ public boolean hasErrorMark() {
+ return (Boolean) getCommandExecutor().executeScript(
+ "const indicator = arguments[0].shadowRoot.querySelector('[part=\"indicator\"]');"
+ + "return indicator && indicator.textContent.includes('!');",
+ this);
+ }
+}
\ No newline at end of file
diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepperElement.java b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepperElement.java
new file mode 100644
index 00000000000..ed8db794e25
--- /dev/null
+++ b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepperElement.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.stepper.testbench;
+
+import java.util.List;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elementsbase.Element;
+
+/**
+ * TestBench element for the {@code } component.
+ *
+ * @author Vaadin Ltd.
+ */
+@Element("vaadin-stepper")
+public class StepperElement extends TestBenchElement {
+
+ /**
+ * Gets all steps in this stepper.
+ *
+ * @return a list of all steps
+ */
+ public List getSteps() {
+ return $(StepElement.class).all();
+ }
+
+ /**
+ * Gets the step at the given index.
+ *
+ * @param index
+ * the index of the step to get
+ * @return the step at the given index
+ */
+ public StepElement getStep(int index) {
+ return $(StepElement.class).get(index);
+ }
+
+ /**
+ * Gets the number of steps.
+ *
+ * @return the number of steps
+ */
+ public int getStepCount() {
+ return getSteps().size();
+ }
+
+ /**
+ * Clicks on the step at the given index.
+ *
+ * @param index
+ * the index of the step to click
+ */
+ public void clickStep(int index) {
+ getStep(index).click();
+ }
+
+ /**
+ * Gets the label of the step at the given index.
+ *
+ * @param index
+ * the index of the step
+ * @return the label of the step
+ */
+ public String getStepLabel(int index) {
+ return getStep(index).getLabel();
+ }
+
+ /**
+ * Gets the description of the step at the given index.
+ *
+ * @param index
+ * the index of the step
+ * @return the description of the step
+ */
+ public String getStepDescription(int index) {
+ return getStep(index).getDescription();
+ }
+
+ /**
+ * Gets the state of the step at the given index.
+ *
+ * @param index
+ * the index of the step
+ * @return the state of the step
+ */
+ public String getStepState(int index) {
+ return getStep(index).getState();
+ }
+
+ /**
+ * Gets the orientation of the stepper.
+ *
+ * @return the orientation ("horizontal" or "vertical")
+ */
+ public String getOrientation() {
+ return getPropertyString("orientation");
+ }
+
+ /**
+ * Checks if the stepper has the given theme variant.
+ *
+ * @param variant
+ * the theme variant to check
+ * @return {@code true} if the stepper has the variant, {@code false}
+ * otherwise
+ */
+ public boolean hasThemeVariant(String variant) {
+ String theme = getAttribute("theme");
+ return theme != null && theme.contains(variant);
+ }
+
+ /**
+ * Gets the currently active step.
+ *
+ * @return the active step, or {@code null} if no step is active
+ */
+ public StepElement getActiveStep() {
+ return getSteps().stream().filter(StepElement::isActive).findFirst()
+ .orElse(null);
+ }
+
+ /**
+ * Gets all completed steps.
+ *
+ * @return a list of completed steps
+ */
+ public List getCompletedSteps() {
+ return getSteps().stream().filter(StepElement::isCompleted).toList();
+ }
+
+ /**
+ * Gets all steps in error state.
+ *
+ * @return a list of steps in error state
+ */
+ public List getErrorSteps() {
+ return getSteps().stream().filter(StepElement::isError).toList();
+ }
+}
\ No newline at end of file