diff --git a/docs/src/pages/components/TreeView.svx b/docs/src/pages/components/TreeView.svx
index 189cf78124..1a214360fe 100644
--- a/docs/src/pages/components/TreeView.svx
+++ b/docs/src/pages/components/TreeView.svx
@@ -113,3 +113,28 @@ Use `TreeView.showNode` to expand, select, and focus a specific node.
Convert flat data to a hierarchical structure using the `toHierarchy` utility.
+
+## Filter by text with search input
+
+Combine a `Search` input with `filterTreeByText` to create an interactive searchable tree. Type to filter nodes by name (case-insensitive substring matching).
+
+
+
+## Filter utilities
+
+Three filtering functions are available:
+
+
+ filterTreeByText(tree, text, options): Case-insensitive substring search on node text
+ filterTreeById(tree, id, options): Filter by single ID or array of IDs
+ filterTreeNodes(tree, predicate, options): Custom filtering with predicate function
+
+
+## Filter options
+
+All filter functions accept an optional `options` object:
+
+
+ includeAncestors (default: true): Include parent chain of matched nodes
+ includeChildren (default: false): Include all descendants of matched nodes
+
diff --git a/docs/src/pages/framed/TreeView/TreeViewFilter.svelte b/docs/src/pages/framed/TreeView/TreeViewFilter.svelte
new file mode 100644
index 0000000000..9009b6115d
--- /dev/null
+++ b/docs/src/pages/framed/TreeView/TreeViewFilter.svelte
@@ -0,0 +1,127 @@
+
+
+
+
+{#if filteredNodes.length > 0}
+
+{:else}
+
+ No matching nodes found for "{searchValue}"
+
+{/if}
diff --git a/src/index.js b/src/index.js
index e28120a430..e7dad790e0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -153,3 +153,8 @@ export {
} from "./UIShell";
export { UnorderedList } from "./UnorderedList";
export { toHierarchy } from "./utils/toHierarchy";
+export {
+ filterTreeNodes,
+ filterTreeById,
+ filterTreeByText,
+} from "./utils/filterTreeNodes";
diff --git a/src/utils/filterTreeNodes.d.ts b/src/utils/filterTreeNodes.d.ts
new file mode 100644
index 0000000000..71925daf0f
--- /dev/null
+++ b/src/utils/filterTreeNodes.d.ts
@@ -0,0 +1,64 @@
+type NodeLike = {
+ id: string | number;
+ text?: string;
+ nodes?: NodeLike[];
+ [key: string]: any;
+};
+
+type FilterOptions = {
+ /**
+ * Include all descendants of matching nodes
+ * @default false
+ */
+ includeChildren?: boolean;
+ /**
+ * Include all ancestors of matching nodes
+ * @default true
+ */
+ includeAncestors?: boolean;
+};
+
+/**
+ * Filter tree nodes by a predicate function.
+ * Returns a new tree containing only matching nodes and their ancestors.
+ *
+ * @example
+ * const tree = [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child" }] }];
+ * const filtered = filterTreeNodes(tree, (node) => node.id === 2);
+ * // Result: [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child" }] }]
+ */
+export function filterTreeNodes(
+ tree: T[],
+ predicate: (node: T) => boolean,
+ options?: FilterOptions,
+): T[];
+
+/**
+ * Filter tree nodes by node ID
+ *
+ * @example
+ * const tree = [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child" }] }];
+ * const filtered = filterTreeById(tree, 2);
+ * // Result: [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child" }] }]
+ */
+export function filterTreeById(
+ tree: T[],
+ id: string | number | (string | number)[],
+ options?: FilterOptions,
+): T[];
+
+/**
+ * Filter tree nodes by text/name (case-insensitive substring match)
+ *
+ * @example
+ * const tree = [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child Node" }] }];
+ * const filtered = filterTreeByText(tree, "child");
+ * // Result: [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child Node" }] }]
+ */
+export function filterTreeByText(
+ tree: T[],
+ text: string,
+ options?: FilterOptions,
+): T[];
+
+export default filterTreeNodes;
diff --git a/src/utils/filterTreeNodes.js b/src/utils/filterTreeNodes.js
new file mode 100644
index 0000000000..0becf1932a
--- /dev/null
+++ b/src/utils/filterTreeNodes.js
@@ -0,0 +1,133 @@
+// @ts-check
+/**
+ * Filter tree nodes by a predicate function.
+ * Returns a new tree containing only matching nodes and their ancestors.
+ *
+ * @typedef {Object} TreeNode
+ * @property {string | number} id - Unique identifier for the node
+ * @property {string} [text] - Optional text/name for the node
+ * @property {TreeNode[]} [nodes] - Optional array of child nodes
+ * @property {Record} [additionalProperties] - Any additional properties
+ *
+ * @typedef {Object} FilterOptions
+ * @property {boolean} [includeChildren=false] - Include all descendants of matching nodes
+ * @property {boolean} [includeAncestors=true] - Include all ancestors of matching nodes
+ *
+ * @param {TreeNode[]} tree - Hierarchical tree structure to filter
+ * @param {function(TreeNode): boolean} predicate - Function to test each node
+ * @param {FilterOptions} [options] - Filtering options
+ * @returns {TreeNode[]} Filtered tree structure
+ */
+export function filterTreeNodes(
+ tree,
+ predicate,
+ options = { includeChildren: false, includeAncestors: true },
+) {
+ const { includeChildren = false, includeAncestors = true } = options;
+
+ /**
+ * Deep clone a node and all its children
+ * @param {TreeNode} node
+ * @returns {TreeNode}
+ */
+ function cloneNode(node) {
+ const cloned = { ...node };
+ if (Array.isArray(node.nodes)) {
+ cloned.nodes = node.nodes.map(cloneNode);
+ }
+ return cloned;
+ }
+
+ /**
+ * Recursively filter tree nodes
+ * @param {TreeNode} node
+ * @returns {{ node: TreeNode | null, hasMatch: boolean }}
+ */
+ function filterNode(node) {
+ const matches = predicate(node);
+
+ // Process children first
+ let filteredChildren = [];
+ let childHasMatch = false;
+
+ if (Array.isArray(node.nodes)) {
+ for (const child of node.nodes) {
+ const result = filterNode(child);
+ if (result.node) {
+ filteredChildren.push(result.node);
+ childHasMatch = true;
+ }
+ }
+ }
+
+ // If this node matches and we include children, use all original children
+ if (matches && includeChildren) {
+ return { node: cloneNode(node), hasMatch: true };
+ }
+
+ // Include this node if:
+ // 1. It matches the predicate, OR
+ // 2. includeAncestors is true AND a descendant matches
+ if (matches || (includeAncestors && childHasMatch)) {
+ const newNode = { ...node };
+ if (filteredChildren.length > 0) {
+ newNode.nodes = filteredChildren;
+ } else if (matches && !includeChildren) {
+ // If node matches but has no matching children, remove nodes array
+ delete newNode.nodes;
+ } else {
+ // Remove empty nodes array when just including ancestors
+ delete newNode.nodes;
+ }
+ return { node: newNode, hasMatch: true };
+ }
+
+ return { node: null, hasMatch: false };
+ }
+
+ const result = [];
+ for (const node of tree) {
+ const filtered = filterNode(node);
+ if (filtered.node) {
+ result.push(filtered.node);
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Filter tree nodes by node ID
+ * @param {TreeNode[]} tree - Hierarchical tree structure to filter
+ * @param {string | number | (string | number)[]} id - Single ID or array of IDs to match
+ * @param {FilterOptions} [options] - Filtering options
+ * @returns {TreeNode[]} Filtered tree structure
+ */
+export function filterTreeById(tree, id, options) {
+ const ids = Array.isArray(id) ? new Set(id) : new Set([id]);
+ return filterTreeNodes(tree, (node) => ids.has(node.id), options);
+}
+
+/**
+ * Filter tree nodes by text/name (case-insensitive substring match)
+ * @param {TreeNode[]} tree - Hierarchical tree structure to filter
+ * @param {string} text - Text to search for (case-insensitive)
+ * @param {FilterOptions} [options] - Filtering options
+ * @returns {TreeNode[]} Filtered tree structure
+ */
+export function filterTreeByText(tree, text, options) {
+ const searchText = text.toLowerCase();
+ return filterTreeNodes(
+ tree,
+ (node) => {
+ const nodeText = node.text || "";
+ return (
+ typeof nodeText === "string" &&
+ nodeText.toLowerCase().includes(searchText)
+ );
+ },
+ options,
+ );
+}
+
+export default filterTreeNodes;
diff --git a/tests/TreeView/filterTreeNodes.test.ts b/tests/TreeView/filterTreeNodes.test.ts
new file mode 100644
index 0000000000..95c07d3eb6
--- /dev/null
+++ b/tests/TreeView/filterTreeNodes.test.ts
@@ -0,0 +1,361 @@
+import {
+ filterTreeNodes,
+ filterTreeById,
+ filterTreeByText,
+} from "../../src/utils/filterTreeNodes";
+
+describe("filterTreeNodes", () => {
+ const sampleTree = [
+ {
+ id: 1,
+ text: "Documents",
+ nodes: [
+ {
+ id: 2,
+ text: "Work",
+ nodes: [
+ { id: 3, text: "Report.docx" },
+ { id: 4, text: "Presentation.pptx" },
+ ],
+ },
+ {
+ id: 5,
+ text: "Personal",
+ nodes: [{ id: 6, text: "Resume.pdf" }],
+ },
+ ],
+ },
+ {
+ id: 7,
+ text: "Pictures",
+ nodes: [{ id: 8, text: "Vacation.jpg" }],
+ },
+ ];
+
+ describe("filterTreeNodes with predicate", () => {
+ test("should filter by custom predicate and include ancestors", () => {
+ const result = filterTreeNodes(sampleTree, (node) => node.id === 3);
+
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Documents",
+ nodes: [
+ {
+ id: 2,
+ text: "Work",
+ nodes: [{ id: 3, text: "Report.docx" }],
+ },
+ ],
+ },
+ ]);
+ });
+
+ test("should filter multiple nodes at different levels", () => {
+ const result = filterTreeNodes(
+ sampleTree,
+ (node) => node.id === 3 || node.id === 8,
+ );
+
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Documents",
+ nodes: [
+ {
+ id: 2,
+ text: "Work",
+ nodes: [{ id: 3, text: "Report.docx" }],
+ },
+ ],
+ },
+ {
+ id: 7,
+ text: "Pictures",
+ nodes: [{ id: 8, text: "Vacation.jpg" }],
+ },
+ ]);
+ });
+
+ test("should return empty array when no matches", () => {
+ const result = filterTreeNodes(sampleTree, (node) => node.id === 999);
+ expect(result).toEqual([]);
+ });
+
+ test("should include all children when includeChildren is true", () => {
+ const result = filterTreeNodes(sampleTree, (node) => node.id === 2, {
+ includeChildren: true,
+ });
+
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Documents",
+ nodes: [
+ {
+ id: 2,
+ text: "Work",
+ nodes: [
+ { id: 3, text: "Report.docx" },
+ { id: 4, text: "Presentation.pptx" },
+ ],
+ },
+ ],
+ },
+ ]);
+ });
+
+ test("should exclude ancestors when includeAncestors is false", () => {
+ const result = filterTreeNodes(sampleTree, (node) => node.id === 3, {
+ includeAncestors: false,
+ });
+
+ expect(result).toEqual([]);
+ });
+
+ test("should match root nodes", () => {
+ const result = filterTreeNodes(sampleTree, (node) => node.id === 1);
+
+ // When a root node matches, without includeChildren,
+ // it only includes the node itself (no children unless they also match)
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Documents",
+ },
+ ]);
+ });
+
+ test("should preserve additional properties", () => {
+ const tree = [
+ {
+ id: 1,
+ text: "Root",
+ custom: "value",
+ nodes: [{ id: 2, text: "Child", meta: { key: "data" } }],
+ },
+ ];
+
+ const result = filterTreeNodes(tree, (node) => node.id === 2);
+
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Root",
+ custom: "value",
+ nodes: [{ id: 2, text: "Child", meta: { key: "data" } }],
+ },
+ ]);
+ });
+ });
+
+ describe("filterTreeById", () => {
+ test("should filter by single ID", () => {
+ const result = filterTreeById(sampleTree, 6);
+
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Documents",
+ nodes: [
+ {
+ id: 5,
+ text: "Personal",
+ nodes: [{ id: 6, text: "Resume.pdf" }],
+ },
+ ],
+ },
+ ]);
+ });
+
+ test("should filter by array of IDs", () => {
+ const result = filterTreeById(sampleTree, [3, 4]);
+
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Documents",
+ nodes: [
+ {
+ id: 2,
+ text: "Work",
+ nodes: [
+ { id: 3, text: "Report.docx" },
+ { id: 4, text: "Presentation.pptx" },
+ ],
+ },
+ ],
+ },
+ ]);
+ });
+
+ test("should work with includeChildren option", () => {
+ const result = filterTreeById(sampleTree, 1, { includeChildren: true });
+
+ // Only the first tree (id=1) matches, so we get just that tree with all children
+ expect(result).toEqual([sampleTree[0]]);
+ });
+
+ test("should handle string IDs", () => {
+ const tree = [
+ {
+ id: "root",
+ text: "Root",
+ nodes: [{ id: "child", text: "Child" }],
+ },
+ ];
+
+ const result = filterTreeById(tree, "child");
+
+ expect(result).toEqual([
+ {
+ id: "root",
+ text: "Root",
+ nodes: [{ id: "child", text: "Child" }],
+ },
+ ]);
+ });
+ });
+
+ describe("filterTreeByText", () => {
+ test("should filter by text (case-insensitive)", () => {
+ const result = filterTreeByText(sampleTree, "work");
+
+ // Without includeChildren, only the matching node is included (and ancestors)
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Documents",
+ nodes: [
+ {
+ id: 2,
+ text: "Work",
+ },
+ ],
+ },
+ ]);
+ });
+
+ test("should perform substring matching", () => {
+ const result = filterTreeByText(sampleTree, "res");
+
+ // "res" matches: "Presentation.pptx", "Personal", "Resume.pdf", and "Pictures"
+ expect(result).toEqual([
+ {
+ id: 1,
+ text: "Documents",
+ nodes: [
+ {
+ id: 2,
+ text: "Work",
+ nodes: [{ id: 4, text: "Presentation.pptx" }],
+ },
+ {
+ id: 5,
+ text: "Personal",
+ nodes: [{ id: 6, text: "Resume.pdf" }],
+ },
+ ],
+ },
+ {
+ id: 7,
+ text: "Pictures",
+ },
+ ]);
+ });
+
+ test("should handle empty search string", () => {
+ const result = filterTreeByText(sampleTree, "");
+
+ // Empty string matches all nodes with text
+ expect(result).toEqual(sampleTree);
+ });
+
+ test("should return empty array when no text matches", () => {
+ const result = filterTreeByText(sampleTree, "nonexistent");
+
+ expect(result).toEqual([]);
+ });
+
+ test("should work with includeChildren option", () => {
+ const result = filterTreeByText(sampleTree, "Documents", {
+ includeChildren: true,
+ });
+
+ // Only "Documents" node matches, so we get just the first tree with all children
+ expect(result).toEqual([sampleTree[0]]);
+ });
+
+ test("should handle nodes without text property", () => {
+ const tree = [
+ {
+ id: 1,
+ nodes: [{ id: 2, text: "Has Text" }],
+ },
+ ];
+
+ const result = filterTreeByText(tree, "Has");
+
+ expect(result).toEqual([
+ {
+ id: 1,
+ nodes: [{ id: 2, text: "Has Text" }],
+ },
+ ]);
+ });
+ });
+
+ describe("edge cases", () => {
+ test("should handle empty tree", () => {
+ const result = filterTreeNodes([], (node) => true);
+ expect(result).toEqual([]);
+ });
+
+ test("should handle deeply nested structures", () => {
+ const deepTree = [
+ {
+ id: 1,
+ text: "L1",
+ nodes: [
+ {
+ id: 2,
+ text: "L2",
+ nodes: [
+ {
+ id: 3,
+ text: "L3",
+ nodes: [
+ {
+ id: 4,
+ text: "L4",
+ nodes: [{ id: 5, text: "L5" }],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ];
+
+ const result = filterTreeById(deepTree, 5);
+
+ expect(result).toEqual(deepTree);
+ });
+
+ test("should not mutate original tree", () => {
+ const original = [
+ {
+ id: 1,
+ text: "Root",
+ nodes: [{ id: 2, text: "Child" }],
+ },
+ ];
+ const originalCopy = JSON.parse(JSON.stringify(original));
+
+ filterTreeById(original, 2);
+
+ expect(original).toEqual(originalCopy);
+ });
+ });
+});
diff --git a/types/index.d.ts b/types/index.d.ts
index be215d974d..b4157169aa 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -167,3 +167,6 @@ export { default as HeaderGlobalAction } from "./UIShell/HeaderGlobalAction.svel
export { default as HeaderSearch } from "./UIShell/HeaderSearch.svelte";
export { default as UnorderedList } from "./UnorderedList/UnorderedList.svelte";
export { default as toHierarchy } from "./utils/toHierarchy";
+export { default as filterTreeNodes } from "./utils/filterTreeNodes";
+export { default as filterTreeById } from "./utils/filterTreeNodes";
+export { default as filterTreeByText } from "./utils/filterTreeNodes";
diff --git a/types/utils/filterTreeNodes.d.ts b/types/utils/filterTreeNodes.d.ts
new file mode 100644
index 0000000000..71925daf0f
--- /dev/null
+++ b/types/utils/filterTreeNodes.d.ts
@@ -0,0 +1,64 @@
+type NodeLike = {
+ id: string | number;
+ text?: string;
+ nodes?: NodeLike[];
+ [key: string]: any;
+};
+
+type FilterOptions = {
+ /**
+ * Include all descendants of matching nodes
+ * @default false
+ */
+ includeChildren?: boolean;
+ /**
+ * Include all ancestors of matching nodes
+ * @default true
+ */
+ includeAncestors?: boolean;
+};
+
+/**
+ * Filter tree nodes by a predicate function.
+ * Returns a new tree containing only matching nodes and their ancestors.
+ *
+ * @example
+ * const tree = [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child" }] }];
+ * const filtered = filterTreeNodes(tree, (node) => node.id === 2);
+ * // Result: [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child" }] }]
+ */
+export function filterTreeNodes(
+ tree: T[],
+ predicate: (node: T) => boolean,
+ options?: FilterOptions,
+): T[];
+
+/**
+ * Filter tree nodes by node ID
+ *
+ * @example
+ * const tree = [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child" }] }];
+ * const filtered = filterTreeById(tree, 2);
+ * // Result: [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child" }] }]
+ */
+export function filterTreeById(
+ tree: T[],
+ id: string | number | (string | number)[],
+ options?: FilterOptions,
+): T[];
+
+/**
+ * Filter tree nodes by text/name (case-insensitive substring match)
+ *
+ * @example
+ * const tree = [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child Node" }] }];
+ * const filtered = filterTreeByText(tree, "child");
+ * // Result: [{ id: 1, text: "Root", nodes: [{ id: 2, text: "Child Node" }] }]
+ */
+export function filterTreeByText(
+ tree: T[],
+ text: string,
+ options?: FilterOptions,
+): T[];
+
+export default filterTreeNodes;