A search bar component that can be used to search for nodes in the flow.

It uses the Command component from shadcn ui.

By default, it will check for lowercase string inclusion in the node’s label, and select the node and fit the view to the node when it is selected. You can override this behavior by passing a custom onSearch function. You can also override the default onSelectNode function to customize the behavior when a node is selected.

UI Component: node-search

index.tsx
import { useCallback, useState } from "react";
 
import {
  BuiltInEdge,
  useReactFlow,
  type Node,
  type PanelProps,
} from "@xyflow/react";
 
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
 
export interface NodeSearchProps extends Omit<PanelProps, "children"> {
  // The function to search for nodes, should return an array of nodes that match the search string
  // By default, it will check for lowercase string inclusion.
  onSearch?: (searchString: string) => Node[];
  // The function to select a node, should set the node as selected and fit the view to the node
  // By default, it will set the node as selected and fit the view to the node.
  onSelectNode?: (node: Node) => void;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
}
 
export function NodeSearchInternal({
  onSearch,
  onSelectNode,
  open,
  onOpenChange,
}: NodeSearchProps) {
  const [searchResults, setSearchResults] = useState<Node[]>([]);
  const [searchString, setSearchString] = useState<string>("");
  const { getNodes, fitView, setNodes } = useReactFlow<Node, BuiltInEdge>();
 
  const defaultOnSearch = useCallback(
    (searchString: string) => {
      const nodes = getNodes();
      return nodes.filter((node) =>
        (node.data.label as string)
          .toLowerCase()
          .includes(searchString.toLowerCase()),
      );
    },
    [getNodes],
  );
 
  const onChange = useCallback(
    (searchString: string) => {
      setSearchString(searchString);
      if (searchString.length > 0) {
        onOpenChange?.(true);
        const results = (onSearch || defaultOnSearch)(searchString);
        setSearchResults(results);
      }
    },
    [defaultOnSearch, onOpenChange, onSearch],
  );
 
  const defaultOnSelectNode = useCallback(
    (node: Node) => {
      setNodes((nodes) =>
        nodes.map((n) => (n.id === node.id ? { ...n, selected: true } : n)),
      );
      fitView({ nodes: [node], duration: 500 });
    },
    [fitView, setNodes],
  );
 
  const onSelect = useCallback(
    (node: Node) => {
      (onSelectNode || defaultOnSelectNode)?.(node);
      setSearchString("");
      onOpenChange?.(false);
    },
    [onSelectNode, defaultOnSelectNode, onOpenChange],
  );
 
  return (
    <>
      <CommandInput
        placeholder="Search nodes..."
        onValueChange={onChange}
        value={searchString}
        onFocus={() => onOpenChange?.(true)}
      />
 
      {open && (
        <CommandList>
          {searchResults.length === 0 ? (
            <CommandEmpty>No results found. {searchString}</CommandEmpty>
          ) : (
            <CommandGroup heading="Nodes">
              {searchResults.map((node) => {
                return (
                  <CommandItem key={node.id} onSelect={() => onSelect(node)}>
                    <span>{node.data.label as string}</span>
                  </CommandItem>
                );
              })}
            </CommandGroup>
          )}
        </CommandList>
      )}
    </>
  );
}
 
export function NodeSearch({
  className,
  onSearch,
  onSelectNode,
  ...props
}: NodeSearchProps) {
  const [open, setOpen] = useState(false);
  return (
    <Command
      shouldFilter={false}
      className="rounded-lg border shadow-md md:min-w-[450px]"
    >
      <NodeSearchInternal
        className={className}
        onSearch={onSearch}
        onSelectNode={onSelectNode}
        open={open}
        onOpenChange={setOpen}
        {...props}
      />
    </Command>
  );
}
 
export interface NodeSearchDialogProps extends NodeSearchProps {
  title?: string;
}
 
export function NodeSearchDialog({
  className,
  onSearch,
  onSelectNode,
  open,
  onOpenChange,
  ...props
}: NodeSearchDialogProps) {
  return (
    <CommandDialog open={open} onOpenChange={onOpenChange}>
      <NodeSearchInternal
        className={className}
        onSearch={onSearch}
        onSelectNode={onSelectNode}
        open={open}
        onOpenChange={onOpenChange}
        {...props}
      />
    </CommandDialog>
  );
}