From d59707f4f208d2ea18f3a383b1f254fccb418143 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 13 Nov 2025 10:28:56 +0100 Subject: [PATCH 1/9] feat(nodes-edges): add zIndexMode --- .../components/ReactFlowProvider/index.tsx | 5 +- .../src/components/StoreUpdater/index.tsx | 1 + .../react/src/container/ReactFlow/Wrapper.tsx | 5 +- .../react/src/container/ReactFlow/index.tsx | 3 ++ packages/react/src/store/index.ts | 28 ++++++++--- packages/react/src/store/initialState.ts | 4 ++ packages/react/src/types/component-props.ts | 2 + packages/react/src/types/store.ts | 3 ++ packages/system/src/types/general.ts | 2 + packages/system/src/utils/store.ts | 50 +++++++++++++------ 10 files changed, 80 insertions(+), 23 deletions(-) diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index 11a9c6b8..51d344f0 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -4,7 +4,7 @@ import { Provider } from '../../contexts/StoreContext'; import { createStore } from '../../store'; import { BatchProvider } from '../BatchProvider'; import type { Node, Edge, FitViewOptions } from '../../types'; -import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; +import { CoordinateExtent, NodeOrigin, ZIndexMode } from '@xyflow/system'; export type ReactFlowProviderProps = { /** These nodes are used to initialize the flow. They are not dynamic. */ @@ -48,6 +48,7 @@ export type ReactFlowProviderProps = { */ nodeExtent?: CoordinateExtent; children: ReactNode; + zIndexMode?: ZIndexMode; }; /** @@ -97,6 +98,7 @@ export function ReactFlowProvider({ fitView, nodeOrigin, nodeExtent, + zIndexMode, children, }: ReactFlowProviderProps) { const [store] = useState(() => @@ -113,6 +115,7 @@ export function ReactFlowProvider({ fitViewOptions, nodeOrigin, nodeExtent, + zIndexMode, }) ); diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 7f375d0a..223e62bc 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -70,6 +70,7 @@ const reactFlowFieldsToTrack = [ 'debug', 'autoPanSpeed', 'ariaLabelConfig', + 'zIndexMode', ] as const; type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number]; diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index 9ae0aa62..0591aba3 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -3,7 +3,7 @@ import { useContext, type ReactNode } from 'react'; import StoreContext from '../../contexts/StoreContext'; import { ReactFlowProvider } from '../../components/ReactFlowProvider'; import type { Node, Edge, FitViewOptions } from '../../types'; -import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; +import { CoordinateExtent, NodeOrigin, ZIndexMode } from '@xyflow/system'; export function Wrapper({ children, @@ -19,6 +19,7 @@ export function Wrapper({ maxZoom, nodeOrigin, nodeExtent, + zIndexMode, }: { children: ReactNode; nodes?: Node[]; @@ -33,6 +34,7 @@ export function Wrapper({ maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; + zIndexMode?: ZIndexMode; }) { const isWrapped = useContext(StoreContext); @@ -58,6 +60,7 @@ export function Wrapper({ initialMaxZoom={maxZoom} nodeOrigin={nodeOrigin} nodeExtent={nodeExtent} + zIndexMode={zIndexMode} > {children} diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index bd20242f..aeebb2c4 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -148,6 +148,7 @@ function ReactFlow( debug, onScroll, ariaLabelConfig, + zIndexMode, ...rest }: ReactFlowProps, ref: ForwardedRef @@ -186,6 +187,7 @@ function ReactFlow( maxZoom={maxZoom} nodeOrigin={nodeOrigin} nodeExtent={nodeExtent} + zIndexMode={zIndexMode} > onInit={onInit} @@ -311,6 +313,7 @@ function ReactFlow( onBeforeDelete={onBeforeDelete} debug={debug} ariaLabelConfig={ariaLabelConfig} + zIndexMode={zIndexMode} /> onSelectionChange={onSelectionChange} /> {children} diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 3254bdfa..23ce6913 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -14,6 +14,7 @@ import { NodeOrigin, CoordinateExtent, fitViewport, + ZIndexMode, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -33,6 +34,7 @@ const createStore = ({ maxZoom, nodeOrigin, nodeExtent, + zIndexMode, }: { nodes?: Node[]; edges?: Edge[]; @@ -46,6 +48,7 @@ const createStore = ({ maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; + zIndexMode?: ZIndexMode; }) => createWithEqualityFn((set, get) => { async function resolveFitView() { @@ -89,9 +92,10 @@ const createStore = ({ nodeExtent, defaultNodes, defaultEdges, + zIndexMode, }), setNodes: (nodes: Node[]) => { - const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get(); + const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued, zIndexMode } = get(); /* * setNodes() is called exclusively in response to user actions: * - either when the `` prop is updated in the controlled ReactFlow setup, @@ -106,6 +110,7 @@ const createStore = ({ nodeExtent, elevateNodesOnSelect, checkEquality: true, + zIndexMode, }); if (fitViewQueued && nodesInitialized) { @@ -140,8 +145,17 @@ const createStore = ({ * new dimensions and update the nodes. */ updateNodeInternals: (updates) => { - const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug, fitViewQueued } = - get(); + const { + triggerNodeChanges, + nodeLookup, + parentLookup, + domNode, + nodeOrigin, + nodeExtent, + debug, + fitViewQueued, + zIndexMode, + } = get(); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -149,14 +163,15 @@ const createStore = ({ parentLookup, domNode, nodeOrigin, - nodeExtent + nodeExtent, + zIndexMode ); if (!updatedInternals) { return; } - updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); + updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent, zIndexMode }); if (fitViewQueued) { resolveFitView(); @@ -332,7 +347,7 @@ const createStore = ({ triggerEdgeChanges(edgeChanges); }, setNodeExtent: (nextNodeExtent) => { - const { nodes, nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, nodeExtent } = get(); + const { nodes, nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, nodeExtent, zIndexMode } = get(); if ( nextNodeExtent[0][0] === nodeExtent[0][0] && @@ -348,6 +363,7 @@ const createStore = ({ nodeExtent: nextNodeExtent, elevateNodesOnSelect, checkEquality: false, + zIndexMode, }); set({ nodeExtent: nextNodeExtent }); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 3e561ce9..da1234c0 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -11,6 +11,7 @@ import { initialConnection, CoordinateExtent, defaultAriaLabelConfig, + ZIndexMode, } from '@xyflow/system'; import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types'; @@ -28,6 +29,7 @@ const getInitialState = ({ maxZoom = 2, nodeOrigin, nodeExtent, + zIndexMode = 'auto', }: { nodes?: Node[]; edges?: Edge[]; @@ -41,6 +43,7 @@ const getInitialState = ({ maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; + zIndexMode?: ZIndexMode; } = {}): ReactFlowStore => { const nodeLookup = new Map(); const parentLookup = new Map(); @@ -146,6 +149,7 @@ const getInitialState = ({ lib: 'react', debug: false, ariaLabelConfig: defaultAriaLabelConfig, + zIndexMode, }; }; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 3e534aaa..25c6750a 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -23,6 +23,7 @@ import type { OnReconnect, AriaLabelConfig, FinalConnectionState, + ZIndexMode, } from '@xyflow/system'; import type { @@ -691,4 +692,5 @@ export interface ReactFlowProps; + zIndexMode?: ZIndexMode; } diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 59a05127..1a723405 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -30,6 +30,7 @@ import { type ParentLookup, type AriaLabelConfig, SetCenter, + ZIndexMode, } from '@xyflow/system'; import type { @@ -152,6 +153,8 @@ export type ReactFlowStore = { diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index d43c50ee..a7b0ae49 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -367,3 +367,5 @@ export type OnBeforeDeleteBase Promise; + +export type ZIndexMode = 'auto' | 'manual'; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 88f4bb79..c8207b91 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,4 +1,4 @@ -import { Handle, HandleConnection, infiniteExtent, NodeHandle, NodeHandleBounds } from '..'; +import { Handle, HandleConnection, infiniteExtent, NodeHandleBounds, ZIndexMode } from '..'; import { NodeBase, CoordinateExtent, @@ -37,6 +37,7 @@ const defaultOptions = { nodeOrigin: [0, 0] as NodeOrigin, nodeExtent: infiniteExtent, elevateNodesOnSelect: true, + zIndexMode: 'auto' as ZIndexMode, defaults: {}, }; @@ -112,21 +113,27 @@ type UpdateNodesOptions = { nodeExtent?: CoordinateExtent; elevateNodesOnSelect?: boolean; defaults?: Partial; + zIndexMode?: ZIndexMode; checkEquality?: boolean; }; +function isManualZIndexMode(zIndexMode?: ZIndexMode): boolean { + return zIndexMode === 'manual'; +} + export function adoptUserNodes( nodes: NodeType[], nodeLookup: NodeLookup>, parentLookup: ParentLookup>, - options?: UpdateNodesOptions + options: UpdateNodesOptions = {} ): boolean { const _options = mergeObjects(adoptUserNodesDefaultOptions, options); let rootParentIndex = { i: -1 }; let nodesInitialized = nodes.length > 0; const tmpLookup = new Map(nodeLookup); - const selectedNodeZ: number = _options?.elevateNodesOnSelect ? SELECTED_NODE_Z : 0; + const selectedNodeZ: number = + _options?.elevateNodesOnSelect && !isManualZIndexMode(_options.zIndexMode) ? SELECTED_NODE_Z : 0; nodeLookup.clear(); parentLookup.clear(); @@ -152,7 +159,7 @@ export function adoptUserNodes( positionAbsolute: clampedPosition, // if user re-initializes the node or removes `measured` for whatever reason, we reset the handleBounds so that the node gets re-measured handleBounds: parseHandles(userNode, internalNode), - z: calculateZ(userNode, selectedNodeZ), + z: calculateZ(userNode, selectedNodeZ, _options.zIndexMode), userNode, }, }; @@ -201,10 +208,10 @@ function updateChildNode( node: InternalNodeBase, nodeLookup: NodeLookup>, parentLookup: ParentLookup>, - options?: UpdateNodesOptions, + options: UpdateNodesOptions, rootParentIndex?: { i: number } ) { - const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options); + const { elevateNodesOnSelect, nodeOrigin, nodeExtent, zIndexMode } = mergeObjects(defaultOptions, options); const parentId = node.parentId!; const parentNode = nodeLookup.get(parentId); @@ -218,7 +225,12 @@ function updateChildNode( updateParentLookup(node, parentLookup); // We just want to set the rootParentIndex for the first child - if (rootParentIndex && !parentNode.parentId && parentNode.internals.rootParentIndex === undefined) { + if ( + rootParentIndex && + !parentNode.parentId && + parentNode.internals.rootParentIndex === undefined && + !isManualZIndexMode(zIndexMode) + ) { parentNode.internals.rootParentIndex = ++rootParentIndex.i; parentNode.internals.z = parentNode.internals.z + rootParentIndex.i * ROOT_PARENT_Z_INCREMENT; } @@ -228,8 +240,8 @@ function updateChildNode( rootParentIndex.i = parentNode.internals.rootParentIndex; } - const selectedNodeZ = elevateNodesOnSelect ? SELECTED_NODE_Z : 0; - const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ); + const selectedNodeZ = elevateNodesOnSelect && !isManualZIndexMode(zIndexMode) ? SELECTED_NODE_Z : 0; + const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ, zIndexMode); const { positionAbsolute } = node.internals; const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y; @@ -246,8 +258,14 @@ function updateChildNode( } } -function calculateZ(node: NodeBase, selectedNodeZ: number) { - return (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0); +function calculateZ(node: NodeBase, selectedNodeZ: number, zIndexMode: ZIndexMode): number { + const zIndex = isNumeric(node.zIndex) ? node.zIndex : 0; + + if (isManualZIndexMode(zIndexMode)) { + return zIndex; + } + + return zIndex + (node.selected ? selectedNodeZ : 0); } function calculateChildXYZ( @@ -255,7 +273,8 @@ function calculateChildXYZ( parentNode: InternalNodeBase, nodeOrigin: NodeOrigin, nodeExtent: CoordinateExtent, - selectedNodeZ: number + selectedNodeZ: number, + zIndexMode: ZIndexMode ) { const { x: parentX, y: parentY } = parentNode.internals.positionAbsolute; const childDimensions = getNodeDimensions(childNode); @@ -274,7 +293,7 @@ function calculateChildXYZ( absolutePosition = clampPositionToParent(absolutePosition, childDimensions, parentNode); } - const childZ = calculateZ(childNode, selectedNodeZ); + const childZ = calculateZ(childNode, selectedNodeZ, zIndexMode); const parentZ = parentNode.internals.z ?? 0; return { @@ -378,7 +397,8 @@ export function updateNodeInternals( parentLookup: ParentLookup, domNode: HTMLElement | null, nodeOrigin?: NodeOrigin, - nodeExtent?: CoordinateExtent + nodeExtent?: CoordinateExtent, + zIndexMode?: ZIndexMode ): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } { const viewportNode = domNode?.querySelector('.xyflow__viewport'); let updatedInternals = false; @@ -446,7 +466,7 @@ export function updateNodeInternals( nodeLookup.set(node.id, newNode); if (node.parentId) { - updateChildNode(newNode, nodeLookup, parentLookup, { nodeOrigin }); + updateChildNode(newNode, nodeLookup, parentLookup, { nodeOrigin, zIndexMode }); } updatedInternals = true; From fe3bb90c5412e8df28a180161750f40a0dcb83c2 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 1 Dec 2025 11:04:57 +0100 Subject: [PATCH 2/9] feat(zIndexMode): use it for edges too --- .../src/components/EdgeWrapper/index.tsx | 1 + packages/react/src/store/initialState.ts | 2 +- packages/system/src/types/general.ts | 11 +++++++++- packages/system/src/utils/edges/general.ts | 21 +++++++++++++++++-- packages/system/src/utils/store.ts | 6 +++--- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 7b76cdfa..700ef11a 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -89,6 +89,7 @@ function EdgeWrapper({ sourceNode, targetNode, elevateOnSelect: store.elevateEdgesOnSelect, + zIndexMode: store.zIndexMode, }); return { diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index da1234c0..e516b533 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -122,7 +122,7 @@ const getInitialState = ({ edgesReconnectable: true, elementsSelectable: true, elevateNodesOnSelect: true, - elevateEdgesOnSelect: false, + elevateEdgesOnSelect: true, selectNodesOnDrag: true, multiSelectionActive: false, diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index a7b0ae49..7af07063 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -368,4 +368,13 @@ export type OnBeforeDeleteBase Promise; -export type ZIndexMode = 'auto' | 'manual'; +/** + * The `ZIndexMode` type is used to define how z-indexing is calculated for nodes and edges. + * `auto` mode will automatically manage z-indexing for selections and sub flows. + * `basic` mode will only manage z-indexing for selections. + * `manual` mode does not apply any automatic z-indexing. + * + * @public + * @default 'auto' + */ +export type ZIndexMode = 'auto' | 'basic' | 'manual'; diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index 58139394..65b2410d 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -1,4 +1,13 @@ -import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..'; +import { + Connection, + InternalNodeBase, + Transform, + errorMessages, + isEdgeBase, + EdgeBase, + ZIndexMode, + isManualZIndexMode, +} from '../..'; import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general'; // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB) @@ -28,6 +37,7 @@ export type GetEdgeZIndexParams = { selected?: boolean; zIndex?: number; elevateOnSelect?: boolean; + zIndexMode?: ZIndexMode; }; /** @@ -41,12 +51,19 @@ export function getElevatedEdgeZIndex({ selected = false, zIndex, elevateOnSelect = false, + zIndexMode = 'auto', }: GetEdgeZIndexParams): number { + const manualZIndexMode = isManualZIndexMode(zIndexMode); + if (zIndex !== undefined) { return zIndex; } - const edgeZ = elevateOnSelect && selected ? 1000 : 0; + if (manualZIndexMode) { + return 0; + } + + const edgeZ = elevateOnSelect && selected ? 1000 : 0; const nodeZ = Math.max( sourceNode.parentId || (elevateOnSelect && sourceNode.selected) ? sourceNode.internals.z : 0, targetNode.parentId || (elevateOnSelect && targetNode.selected) ? targetNode.internals.z : 0 diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index c8207b91..b9049bc0 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -117,7 +117,7 @@ type UpdateNodesOptions = { checkEquality?: boolean; }; -function isManualZIndexMode(zIndexMode?: ZIndexMode): boolean { +export function isManualZIndexMode(zIndexMode?: ZIndexMode): boolean { return zIndexMode === 'manual'; } @@ -129,7 +129,7 @@ export function adoptUserNodes( ): boolean { const _options = mergeObjects(adoptUserNodesDefaultOptions, options); - let rootParentIndex = { i: -1 }; + const rootParentIndex = { i: -1 }; let nodesInitialized = nodes.length > 0; const tmpLookup = new Map(nodeLookup); const selectedNodeZ: number = @@ -229,7 +229,7 @@ function updateChildNode( rootParentIndex && !parentNode.parentId && parentNode.internals.rootParentIndex === undefined && - !isManualZIndexMode(zIndexMode) + zIndexMode === 'auto' ) { parentNode.internals.rootParentIndex = ++rootParentIndex.i; parentNode.internals.z = parentNode.internals.z + rootParentIndex.i * ROOT_PARENT_Z_INCREMENT; From 4eb42952f01b947c9d36c25e6b30b7bd98224632 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 1 Dec 2025 12:11:19 +0100 Subject: [PATCH 3/9] feat(svelte): add zIndexMode --- packages/react/src/types/component-props.ts | 6 ++++++ .../src/lib/container/SvelteFlow/Wrapper.svelte | 1 + packages/svelte/src/lib/container/SvelteFlow/types.ts | 10 +++++++++- packages/svelte/src/lib/store/index.ts | 7 +++++-- packages/svelte/src/lib/store/initial-store.svelte.ts | 11 ++++++++--- packages/svelte/src/lib/store/visibleElements.ts | 11 ++++++++--- 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 25c6750a..aad916a1 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -692,5 +692,11 @@ export interface ReactFlowProps; + /** + * Used to define how z-indexing is calculated for nodes and edges. + * 'auto' is for selections and sub flows, 'basic' for selections only, and 'manual' for no auto z-indexing. + * + * @default 'auto' + */ zIndexMode?: ZIndexMode; } diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index 88ba9f1a..b9314774 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -96,6 +96,7 @@ ariaLabelConfig, autoPanSpeed, panOnScrollSpeed, + zIndexMode, ...divAttributes } = $derived(rest); /* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 0fbd4b18..bcba8961 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -21,7 +21,8 @@ import type { OnReconnect, OnReconnectStart, OnReconnectEnd, - AriaLabelConfig + AriaLabelConfig, + ZIndexMode } from '@xyflow/system'; import type { @@ -500,4 +501,11 @@ export type SvelteFlowProps< * Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings. */ ariaLabelConfig?: Partial; + /** + * Used to define how z-indexing is calculated for nodes and edges. + * 'auto' is for selections and sub flows, 'basic' for selections only, and 'manual' for no auto z-indexing. + * + * @default 'auto' + */ + zIndexMode?: ZIndexMode; }; diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 798e87d8..7b108e0d 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -62,7 +62,9 @@ export function createStore(signals.width ?? 0); height = $state.raw(signals.height ?? 0); + zIndexMode = $state.raw(signals.props.zIndexMode ?? 'auto'); nodesInitialized: boolean = $derived.by(() => { const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, { nodeExtent: this.nodeExtent, nodeOrigin: this.nodeOrigin, elevateNodesOnSelect: signals.props.elevateNodesOnSelect ?? true, - checkEquality: true + checkEquality: true, + zIndexMode: this.zIndexMode }); if (this.fitViewQueued && nodesInitialized) { @@ -226,7 +229,8 @@ export function getInitialStore>; @@ -239,6 +243,7 @@ export function getInitialStore( @@ -42,6 +43,7 @@ export interface EdgeLayoutAllOptions = @@ -70,7 +73,8 @@ export function getLayoutedEdges>(); for (const edge of edges) { @@ -134,7 +138,8 @@ export function getLayoutedEdges Date: Tue, 2 Dec 2025 13:43:12 +0100 Subject: [PATCH 4/9] chore(examples): add zindexmode --- examples/react/src/App/routes.ts | 6 ++ .../react/src/examples/ColorMode/index.tsx | 2 - .../react/src/examples/ColorMode/style.css | 0 .../react/src/examples/ZIndexMode/index.tsx | 88 +++++++++++++++++++ packages/react/src/index.ts | 1 + packages/svelte/src/lib/index.ts | 3 +- 6 files changed, 97 insertions(+), 3 deletions(-) delete mode 100644 examples/react/src/examples/ColorMode/style.css create mode 100644 examples/react/src/examples/ZIndexMode/index.tsx diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 886eecbd..812f2e9f 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -60,6 +60,7 @@ import DevTools from '../examples/DevTools'; import Redux from '../examples/Redux'; import MovingHandles from '../examples/MovingHandles'; import DetachedHandle from '../examples/DetachedHandle'; +import ZIndexMode from '../examples/ZIndexMode'; export interface IRoute { name: string; @@ -378,6 +379,11 @@ const routes: IRoute[] = [ path: 'use-key-press', component: UseKeyPress, }, + { + name: 'zIndexMode', + path: 'z-index-mode', + component: ZIndexMode, + }, ]; export default routes; diff --git a/examples/react/src/examples/ColorMode/index.tsx b/examples/react/src/examples/ColorMode/index.tsx index eae8ad86..f867c650 100644 --- a/examples/react/src/examples/ColorMode/index.tsx +++ b/examples/react/src/examples/ColorMode/index.tsx @@ -15,8 +15,6 @@ import { Position, } from '@xyflow/react'; -import './style.css'; - const nodeDefaults = { sourcePosition: Position.Right, targetPosition: Position.Left, diff --git a/examples/react/src/examples/ColorMode/style.css b/examples/react/src/examples/ColorMode/style.css deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/react/src/examples/ZIndexMode/index.tsx b/examples/react/src/examples/ZIndexMode/index.tsx new file mode 100644 index 00000000..5b5477d9 --- /dev/null +++ b/examples/react/src/examples/ZIndexMode/index.tsx @@ -0,0 +1,88 @@ +import { ChangeEventHandler, useCallback, useState } from 'react'; +import { + ReactFlow, + addEdge, + Node, + useNodesState, + useEdgesState, + OnConnect, + Edge, + MiniMap, + Background, + Controls, + Position, + ZIndexMode, + Panel, +} from '@xyflow/react'; + +const nodeDefaults = { + sourcePosition: Position.Right, + targetPosition: Position.Left, +}; + +const initialNodes: Node[] = [ + { id: 'A', type: 'input', position: { x: 0, y: 150 }, data: { label: 'A' }, ...nodeDefaults }, + { id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults }, + { id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults }, + + // group 1 + { id: 'D', position: { x: 0, y: 300 }, width: 200, height: 200, data: { label: 'D' }, ...nodeDefaults }, + { id: 'E', parentId: 'D', position: { x: 10, y: 10 }, data: { label: 'E' }, ...nodeDefaults }, + + // group 2 + { id: 'F', position: { x: 250, y: 300 }, width: 200, height: 200, data: { label: 'F' }, ...nodeDefaults }, + { id: 'G', parentId: 'F', position: { x: 10, y: 10 }, data: { label: 'G' }, ...nodeDefaults }, + + // group 3 + { id: 'H', position: { x: 500, y: 300 }, width: 200, height: 200, data: { label: 'H' }, ...nodeDefaults }, + { id: 'I', parentId: 'H', position: { x: 10, y: 10 }, data: { label: 'I' }, ...nodeDefaults }, +]; + +const initialEdges: Edge[] = [ + { + id: 'A-B', + source: 'A', + target: 'B', + }, + { + id: 'A-C', + source: 'A', + target: 'C', + }, +]; + +function ZIndexModeFlow() { + const [zIndexMode, setZIndexMode] = useState('auto'); + const [nodes, , onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const onConnect: OnConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + + const onChange: ChangeEventHandler = (evt) => setZIndexMode(evt.target.value as ZIndexMode); + + return ( + + + + + + + + + + ); +} + +export default ZIndexModeFlow; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index b4a67f2e..03fe4c6c 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -113,6 +113,7 @@ export { type SetViewport, type FitBounds, type HandleConnection, + type ZIndexMode, } from '@xyflow/system'; // we need this workaround to prevent a duplicate identifier error diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 815803f7..9665f211 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -122,7 +122,8 @@ export { type SetCenter, type SetViewport, type FitBounds, - type HandleConnection + type HandleConnection, + type ZIndexMode } from '@xyflow/system'; // system utils From 0c7261a6dc94f1aa58333a6aebcaca8ced9b5ad2 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 2 Dec 2025 17:11:20 +0100 Subject: [PATCH 5/9] chore(changesets): add --- .changeset/breezy-gorillas-accept.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/breezy-gorillas-accept.md diff --git a/.changeset/breezy-gorillas-accept.md b/.changeset/breezy-gorillas-accept.md new file mode 100644 index 00000000..2179e425 --- /dev/null +++ b/.changeset/breezy-gorillas-accept.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': minor +'@xyflow/system': minor +--- + +Add `zIndexMode` to control how z-index is calculated for nodes and edges From 7ef3ffadf6ef1a587c76a7a2cbd95fe3df8e9d03 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 2 Dec 2025 17:15:46 +0100 Subject: [PATCH 6/9] chore(rootIndex): start with 1 --- packages/system/src/utils/store.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index b9049bc0..de75804f 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -128,12 +128,11 @@ export function adoptUserNodes( options: UpdateNodesOptions = {} ): boolean { const _options = mergeObjects(adoptUserNodesDefaultOptions, options); - - const rootParentIndex = { i: -1 }; - let nodesInitialized = nodes.length > 0; + const rootParentIndex = { i: 0 }; const tmpLookup = new Map(nodeLookup); const selectedNodeZ: number = _options?.elevateNodesOnSelect && !isManualZIndexMode(_options.zIndexMode) ? SELECTED_NODE_Z : 0; + let nodesInitialized = nodes.length > 0; nodeLookup.clear(); parentLookup.clear(); From 14fd41b1f153406f1a21a61bf98ea07ad8275ec6 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 3 Dec 2025 13:20:14 +0100 Subject: [PATCH 7/9] change default back to elevateEdgesOnSelect=false and zIndexMode=basic --- packages/react/src/container/ReactFlow/index.tsx | 6 +++--- packages/react/src/store/initialState.ts | 2 +- .../svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index abb0023f..b04f2955 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -127,8 +127,8 @@ function ReactFlow( attributionPosition, proOptions, defaultEdgeOptions, - elevateNodesOnSelect, - elevateEdgesOnSelect, + elevateNodesOnSelect = true, + elevateEdgesOnSelect = false, disableKeyboardA11y = false, autoPanOnConnect, autoPanOnNodeDrag, @@ -148,7 +148,7 @@ function ReactFlow( debug, onScroll, ariaLabelConfig, - zIndexMode, + zIndexMode = 'basic', ...rest }: ReactFlowProps, ref: ForwardedRef diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index d90aa8b4..597b1147 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -59,7 +59,7 @@ const getInitialState = ({ const nodesInitialized = adoptUserNodes(storeNodes, nodeLookup, parentLookup, { nodeOrigin: storeNodeOrigin, nodeExtent: storeNodeExtent, - elevateNodesOnSelect: false, + zIndexMode, }); let transform: Transform = [0, 0, 1]; diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index ab9378b8..a2283be7 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -75,7 +75,7 @@ }: SvelteFlowProps & Omit, 'onselectionchange'> = $props(); - // svelte-ignore non_reactive_update + // svelte-ignore non_reactive_update, state_referenced_locally let store = createStore({ props, width, From 83a312b2e1691a44223536653689f8f99f0d5b24 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 3 Dec 2025 14:04:15 +0100 Subject: [PATCH 8/9] chore(zIndexMode): use basic as default --- packages/react/src/store/initialState.ts | 2 +- packages/react/src/types/component-props.ts | 2 +- packages/svelte/src/lib/container/SvelteFlow/types.ts | 2 +- packages/svelte/src/lib/store/initial-store.svelte.ts | 2 +- packages/system/src/types/general.ts | 1 - packages/system/src/utils/edges/general.ts | 2 +- packages/system/src/utils/store.ts | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 597b1147..26a5c045 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -29,7 +29,7 @@ const getInitialState = ({ maxZoom = 2, nodeOrigin, nodeExtent, - zIndexMode = 'auto', + zIndexMode = 'basic', }: { nodes?: Node[]; edges?: Edge[]; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index aad916a1..effc7ca6 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -696,7 +696,7 @@ export interface ReactFlowProps(signals.width ?? 0); height = $state.raw(signals.height ?? 0); - zIndexMode = $state.raw(signals.props.zIndexMode ?? 'auto'); + zIndexMode = $state.raw(signals.props.zIndexMode ?? 'basic'); nodesInitialized: boolean = $derived.by(() => { const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, { diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 7af07063..d2939f82 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -375,6 +375,5 @@ export type OnBeforeDeleteBase Date: Wed, 3 Dec 2025 14:30:44 +0100 Subject: [PATCH 9/9] Update breezy-gorillas-accept.md --- .changeset/breezy-gorillas-accept.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/breezy-gorillas-accept.md b/.changeset/breezy-gorillas-accept.md index 2179e425..920cfac7 100644 --- a/.changeset/breezy-gorillas-accept.md +++ b/.changeset/breezy-gorillas-accept.md @@ -1,7 +1,7 @@ --- '@xyflow/react': minor '@xyflow/svelte': minor -'@xyflow/system': minor +'@xyflow/system': patch --- Add `zIndexMode` to control how z-index is calculated for nodes and edges