diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalData.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalData.java
new file mode 100644
index 00000000000..f8b21cbc2bc
--- /dev/null
+++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalData.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2000-2026 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.data.provider.hierarchy;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Represents hierarchical data.
+ *
+ * Typically used as a backing data source for
+ * {@link InMemoryHierarchicalDataProvider}.
+ *
+ * @author Vaadin Ltd
+ * @since 25.2
+ *
+ * @param
+ * data type
+ */
+public interface HierarchicalData extends Serializable {
+
+ /**
+ * Get the immediate child items for the given item.
+ *
+ * @param item
+ * the item for which to retrieve child items for, null to
+ * retrieve all root items
+ * @return an unmodifiable list of child items for the given item
+ *
+ * @throws IllegalArgumentException
+ * if the item does not exist in this structure
+ */
+ List getChildren(T item);
+
+ /**
+ * Get the parent item for the given item.
+ *
+ * @param item
+ * the item for which to retrieve the parent item for
+ * @return parent item for the given item or {@code null} if the item is a
+ * root item.
+ * @throws IllegalArgumentException
+ * if the item does not exist in this structure
+ */
+ T getParent(T item);
+
+ /**
+ * Check whether the given item is in this hierarchy.
+ *
+ * @param item
+ * the item to check
+ * @return {@code true} if the item is in this hierarchy, {@code false} if
+ * not
+ */
+ boolean contains(T item);
+}
diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/InMemoryHierarchicalDataProvider.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/InMemoryHierarchicalDataProvider.java
new file mode 100644
index 00000000000..a9d71b51640
--- /dev/null
+++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/InMemoryHierarchicalDataProvider.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2000-2026 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.data.provider.hierarchy;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import com.vaadin.flow.data.provider.InMemoryDataProvider;
+import com.vaadin.flow.function.SerializableComparator;
+import com.vaadin.flow.function.SerializablePredicate;
+
+/**
+ * An in-memory data provider for listing components that display hierarchical
+ * data. Uses an instance of {@link HierarchicalData} as its source of data.
+ *
+ * @author Vaadin Ltd
+ * @since 25.2
+ *
+ * @param
+ * data type
+ * @param
+ * concrete type of the {@link HierarchicalData} used in this
+ * provider.
+ */
+public class InMemoryHierarchicalDataProvider>
+ extends AbstractHierarchicalDataProvider>
+ implements InMemoryDataProvider {
+
+ private final U hierarchicalData;
+
+ private SerializablePredicate filter = null;
+
+ private SerializableComparator sortOrder = null;
+
+ private HierarchyFormat hierarchyFormat = HierarchyFormat.NESTED;
+
+ /**
+ * Constructs a new InMemoryHierarchicalDataProvider.
+ *
+ * The data provider should be refreshed after making changes to the
+ * underlying {@link HierarchicalData} instance.
+ *
+ * @param hierarchicalData
+ * the backing {@link HierarchicalData} for this provider, not
+ * {@code null}
+ */
+ public InMemoryHierarchicalDataProvider(U hierarchicalData) {
+ this.hierarchicalData = Objects.requireNonNull(hierarchicalData,
+ "hierarchicalData cannot be null");
+ }
+
+ /**
+ * Creates a new InMemoryHierarchicalDataProvider and configures it to
+ * return the hierarchical data in the specified format:
+ * {@link HierarchyFormat#NESTED} or {@link HierarchyFormat#FLATTENED}.
+ *
+ * The data provider should be refreshed after making changes to the
+ * underlying {@link HierarchicalData} instance.
+ *
+ * @param hierarchicalData
+ * the backing {@link HierarchicalData} for this provider, not
+ * {@code null}
+ * @param hierarchyFormat
+ * the hierarchy format to return data in, not {@code null}
+ */
+ public InMemoryHierarchicalDataProvider(U hierarchicalData,
+ HierarchyFormat hierarchyFormat) {
+ this(hierarchicalData);
+ this.hierarchyFormat = Objects.requireNonNull(hierarchyFormat,
+ "hierarchyFormat cannot be null");
+ }
+
+ @Override
+ public HierarchyFormat getHierarchyFormat() {
+ return hierarchyFormat;
+ }
+
+ /**
+ * Return the underlying {@link HierarchicalData} of this provider.
+ *
+ * @return the underlying data of this provider
+ */
+ public U getHierarchicalData() {
+ return hierarchicalData;
+ }
+
+ @Override
+ public boolean hasChildren(T item) {
+ if (!hierarchicalData.contains(item)) {
+ // The item might be dropped from the tree already
+ return false;
+ }
+ return !hierarchicalData.getChildren(item).isEmpty();
+ }
+
+ @Override
+ public T getParent(T item) {
+ Objects.requireNonNull(item, "Item cannot be null.");
+ try {
+ return hierarchicalData.getParent(item);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public int getDepth(T item) {
+ int depth = 0;
+ while ((item = hierarchicalData.getParent(item)) != null) {
+ depth++;
+ }
+ return depth;
+ }
+
+ @Override
+ public int getChildCount(
+ HierarchicalQuery> query) {
+ Optional> combinedFilter = getCombinedFilter(
+ query.getFilter());
+
+ return (int) flatten(query.getParent(), query.getExpandedItemIds(),
+ combinedFilter, Optional.empty()).stream()
+ .skip(query.getOffset()).limit(query.getLimit()).count();
+ }
+
+ @Override
+ public Stream fetchChildren(
+ HierarchicalQuery> query) {
+ if (!hierarchicalData.contains(query.getParent())) {
+ throw new IllegalArgumentException("The queried item "
+ + query.getParent()
+ + " could not be found in the backing HierarchicalData. "
+ + "Did you forget to refresh this data provider after item removal?");
+ }
+
+ Optional> combinedFilter = getCombinedFilter(
+ query.getFilter());
+
+ Optional> comparator = Stream
+ .of(query.getInMemorySorting(), sortOrder)
+ .filter(Objects::nonNull).reduce(Comparator::thenComparing);
+
+ return flatten(query.getParent(), query.getExpandedItemIds(),
+ combinedFilter, comparator).stream().skip(query.getOffset())
+ .limit(query.getLimit());
+ }
+
+ @Override
+ public SerializablePredicate getFilter() {
+ return filter;
+ }
+
+ @Override
+ public void setFilter(SerializablePredicate filter) {
+ this.filter = filter;
+ refreshAll();
+ }
+
+ @Override
+ public SerializableComparator getSortComparator() {
+ return sortOrder;
+ }
+
+ @Override
+ public void setSortComparator(SerializableComparator comparator) {
+ sortOrder = comparator;
+ refreshAll();
+ }
+
+ private Optional> getCombinedFilter(
+ Optional> queryFilter) {
+ return filter != null
+ ? Optional.of(queryFilter.map(filter::and).orElse(filter))
+ : queryFilter;
+ }
+
+ private List flatten(T parent, Set