Merge pull request #5419 from xyflow/arrow-heads-fallback-color

Style Arrow Heads Edge Markers with CSS Variables
This commit is contained in:
Moritz Klack
2025-08-11 21:44:55 +02:00
committed by GitHub
12 changed files with 250 additions and 57 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
'@xyflow/system': patch
---
Make arrow heads markers fallback to --xy-edge-stroke CSS variable when passing null as marker color
+110 -22
View File
@@ -70,6 +70,30 @@ const initialNodes: Node[] = [
data: { label: 'Output 10' }, data: { label: 'Output 10' },
position: { x: 50, y: 400 }, position: { x: 50, y: 400 },
}, },
{
id: '11',
type: 'output',
data: { label: 'Output 11' },
position: { x: 825, y: 400 },
},
{
id: '12',
type: 'output',
data: { label: 'Output 12' },
position: { x: 825, y: 300 },
},
{
id: '13',
type: 'output',
data: { label: 'Output 13' },
position: { x: 900, y: 200 },
},
{
id: '14',
type: 'output',
data: { label: 'Output 14' },
position: { x: 825, y: 100 },
},
]; ];
const initialEdges: Edge[] = [ const initialEdges: Edge[] = [
@@ -182,6 +206,57 @@ const initialEdges: Edge[] = [
height: 20, height: 20,
}, },
}, },
{
id: 'e4-11',
source: '4',
target: '11',
label: 'Explicit Blue Prop Color (should override CSS)',
className: 'css-variable-edge',
markerEnd: {
type: MarkerType.ArrowClosed,
color: '#0000ff',
width: 40,
height: 40,
},
},
{
id: 'e4-12',
source: '4',
target: '12',
label: 'Marker explicitly undefined Color (defaults to none)',
className: 'css-variable-edge',
markerEnd: {
type: MarkerType.ArrowClosed,
color: undefined,
width: 40,
height: 40,
},
},
{
id: 'e4-13',
source: '4',
target: '13',
label: 'Marker null Color (should use `--xy-edge-stroke` CSS variable)',
className: 'css-variable-edge',
markerEnd: {
type: MarkerType.ArrowClosed,
color: null,
width: 40,
height: 40,
},
},
{
id: 'e4-14',
source: '4',
target: '14',
label: 'Marker implicitly undefined Color (defaults to defaultMarkerColor)',
className: 'css-variable-edge',
markerEnd: {
type: MarkerType.ArrowClosed,
width: 40,
height: 40,
},
},
]; ];
const edgeTypes: EdgeTypes = { const edgeTypes: EdgeTypes = {
@@ -205,28 +280,41 @@ const EdgesFlow = () => {
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]); const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return ( return (
<ReactFlow <>
nodes={nodes} <style>
edges={edges} {`
onNodesChange={onNodesChange} /* Test CSS variables on specific edges */
onEdgesChange={onEdgesChange} .react-flow {
onNodeClick={onNodeClick} --xy-edge-stroke-width: 1;
onConnect={onConnect} --xy-edge-stroke: #00ff00;
onNodeDragStop={onNodeDragStop} }
snapToGrid={true}
edgeTypes={edgeTypes} `}
onEdgeClick={onEdgeClick} </style>
onEdgeDoubleClick={onEdgeDoubleClick} <ReactFlow
onEdgeMouseEnter={onEdgeMouseEnter} nodes={nodes}
onEdgeMouseMove={onEdgeMouseMove} edges={edges}
onEdgeMouseLeave={onEdgeMouseLeave} onNodesChange={onNodesChange}
onDelete={console.log} onEdgesChange={onEdgesChange}
defaultEdgeOptions={defaultEdgeOptions} onNodeClick={onNodeClick}
> onConnect={onConnect}
<MiniMap /> onNodeDragStop={onNodeDragStop}
<Controls /> snapToGrid={true}
<Background /> edgeTypes={edgeTypes}
</ReactFlow> onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onDelete={console.log}
defaultEdgeOptions={defaultEdgeOptions}
defaultMarkerColor={'purple'}
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
</>
); );
}; };
@@ -51,6 +51,30 @@
type: 'output', type: 'output',
data: { label: 'Output 9' }, data: { label: 'Output 9' },
position: { x: 675, y: 500 } position: { x: 675, y: 500 }
},
{
id: '10',
type: 'output',
data: { label: 'Output 10' },
position: { x: 825, y: 400 }
},
{
id: '11',
type: 'output',
data: { label: 'Output 11' },
position: { x: 825, y: 300 }
},
{
id: '12',
type: 'output',
data: { label: 'Output 12' },
position: { x: 900, y: 200 }
},
{
id: '13',
type: 'output',
data: { label: 'Output 13' },
position: { x: 825, y: 100 }
} }
]); ]);
@@ -129,6 +153,55 @@
label: 'hi', label: 'hi',
labelStyle: 'background: red; font-weight: 700; padding: 5px;', labelStyle: 'background: red; font-weight: 700; padding: 5px;',
style: 'stroke: #ffcc0' style: 'stroke: #ffcc0'
},
{
id: 'e4-10',
source: '4',
target: '10',
label: 'Explicit Prop Blue Color (should override CSS)',
markerEnd: {
type: MarkerType.ArrowClosed,
color: '#0000ff',
width: 40,
height: 40
}
},
{
id: 'e4-11',
source: '4',
target: '11',
label: 'Marker explicitly undefined Color (defaults to none)',
className: 'css-variable-edge',
markerEnd: {
type: MarkerType.ArrowClosed,
color: undefined,
width: 40,
height: 40
}
},
{
id: 'e4-12',
source: '4',
target: '12',
label: 'Marker null Color (should use `--xy-edge-stroke` CSS variable)',
markerEnd: {
type: MarkerType.ArrowClosed,
color: null,
width: 40,
height: 40
}
},
{
id: 'e4-13',
source: '4',
target: '13',
label: 'Marker implicitly undefined Color (defaults to defaultMarkerColor)',
className: 'css-variable-edge',
markerEnd: {
type: MarkerType.ArrowClosed,
width: 40,
height: 40
}
} }
]); ]);
@@ -141,6 +214,15 @@
return `edge-${connection.source}-${connection.target}}`; return `edge-${connection.source}-${connection.target}}`;
} }
const defaultEdgeOptions = {
markerEnd: {
type: MarkerType.ArrowClosed,
color: 'red',
width: 20,
height: 20
}
};
$inspect(edges); $inspect(edges);
</script> </script>
@@ -149,6 +231,7 @@
bind:edges bind:edges
{edgeTypes} {edgeTypes}
fitView fitView
{defaultEdgeOptions}
nodeDragThreshold={2} nodeDragThreshold={2}
onbeforeconnect={(connection) => { onbeforeconnect={(connection) => {
console.log('on edge create', connection); console.log('on edge create', connection);
@@ -158,8 +241,17 @@
id: getEdgeId(connection) id: getEdgeId(connection)
}; };
}} }}
defaultMarkerColor={'purple'}
> >
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
</SvelteFlow> </SvelteFlow>
<style>
/* Test CSS variables on the marker SVG container */
:global(.svelte-flow) {
--xy-edge-stroke-width: 1;
--xy-edge-stroke: #00ff00;
}
</style>
@@ -5,7 +5,7 @@ import { useStore } from '../../hooks/useStore';
import { useMarkerSymbol } from './MarkerSymbols'; import { useMarkerSymbol } from './MarkerSymbols';
type MarkerDefinitionsProps = { type MarkerDefinitionsProps = {
defaultColor: string; defaultColor: string | null;
rfId?: string; rfId?: string;
}; };
@@ -6,33 +6,21 @@ import { useStoreApi } from '../../hooks/useStore';
type SymbolProps = Omit<EdgeMarker, 'type'>; type SymbolProps = Omit<EdgeMarker, 'type'>;
const ArrowSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => { const ArrowSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => {
return ( const style = {
<polyline strokeWidth,
style={{ ...(color && { stroke: color }),
stroke: color, };
strokeWidth,
}} return <polyline style={style} strokeLinecap="round" strokeLinejoin="round" fill="none" points="-5,-4 0,0 -5,4" />;
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
points="-5,-4 0,0 -5,4"
/>
);
}; };
const ArrowClosedSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => { const ArrowClosedSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => {
return ( const style = {
<polyline strokeWidth,
style={{ ...(color && { stroke: color, fill: color }),
stroke: color, };
fill: color,
strokeWidth, return <polyline style={style} strokeLinecap="round" strokeLinejoin="round" points="-5,-4 0,0 -5,4 -5,-4" />;
}}
strokeLinecap="round"
strokeLinejoin="round"
points="-5,-4 0,0 -5,4 -5,-4"
/>
);
}; };
export const MarkerSymbols = { export const MarkerSymbols = {
+8 -2
View File
@@ -139,7 +139,12 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* This event fires when the user releases the source or target of an editable edge. It is called * This event fires when the user releases the source or target of an editable edge. It is called
* even if an edge update does not occur. * even if an edge update does not occur.
*/ */
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType, connectionState: FinalConnectionState) => void; onReconnectEnd?: (
event: MouseEvent | TouchEvent,
edge: EdgeType,
handleType: HandleType,
connectionState: FinalConnectionState
) => void;
/** /**
* Use this event handler to add interactivity to a controlled flow. * Use this event handler to add interactivity to a controlled flow.
* It is called on node drag, select, and move. * It is called on node drag, select, and move.
@@ -494,9 +499,10 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
/** /**
* Color of edge markers. * Color of edge markers.
* You can pass `null` to use the CSS variable `--xy-edge-stroke` for the marker color.
* @default '#b1b1b7' * @default '#b1b1b7'
*/ */
defaultMarkerColor?: string; defaultMarkerColor?: string | null;
/** /**
* Controls if the viewport should zoom by scrolling inside the container. * Controls if the viewport should zoom by scrolling inside the container.
* @default true * @default true
@@ -8,9 +8,12 @@
height = 12.5, height = 12.5,
markerUnits = 'strokeWidth', markerUnits = 'strokeWidth',
orient = 'auto-start-reverse', orient = 'auto-start-reverse',
color, color = 'none',
strokeWidth strokeWidth
}: MarkerProps = $props(); }: MarkerProps = $props();
// Create inline styles when explicit color is provided
const polylineStyle = color ? `stroke: ${color}; fill: ${color};` : '';
</script> </script>
<marker <marker
@@ -26,7 +29,7 @@
> >
{#if type === MarkerType.Arrow} {#if type === MarkerType.Arrow}
<polyline <polyline
stroke={color} style={polylineStyle}
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width={strokeWidth} stroke-width={strokeWidth}
@@ -35,11 +38,10 @@
/> />
{:else if type === MarkerType.ArrowClosed} {:else if type === MarkerType.ArrowClosed}
<polyline <polyline
stroke={color} style={polylineStyle}
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width={strokeWidth} stroke-width={strokeWidth}
fill={color}
points="-5,-4 0,0 -5,4 -5,-4" points="-5,-4 0,0 -5,4 -5,-4"
/> />
{/if} {/if}
@@ -230,9 +230,10 @@ export type SvelteFlowProps<
*/ */
snapGrid?: SnapGrid; snapGrid?: SnapGrid;
/** Color of edge markers /** Color of edge markers
* You can pass `null` to use the CSS variable `--xy-edge-stroke` for the marker color.
* @example "#b1b1b7" * @example "#b1b1b7"
*/ */
defaultMarkerColor?: string; defaultMarkerColor?: string | null;
/** /**
* Controls if all nodes should be draggable * Controls if all nodes should be draggable
* @default true * @default true
@@ -365,7 +365,9 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
selectNodesOnDrag: boolean = $derived(signals.props.selectNodesOnDrag ?? true); selectNodesOnDrag: boolean = $derived(signals.props.selectNodesOnDrag ?? true);
defaultMarkerColor: string = $derived(signals.props.defaultMarkerColor ?? '#b1b1b7'); defaultMarkerColor: string | null = $derived(
signals.props.defaultMarkerColor === undefined ? '#b1b1b7' : signals.props.defaultMarkerColor
);
markers: MarkerProps[] = $derived.by(() => { markers: MarkerProps[] = $derived.by(() => {
return createMarkerIds(signals.edges, { return createMarkerIds(signals.edges, {
defaultColor: this.defaultMarkerColor, defaultColor: this.defaultMarkerColor,
+7
View File
@@ -165,6 +165,13 @@
user-select: none; user-select: none;
} }
} }
/* Arrowhead marker styles - use CSS custom properties as default */
.xy-flow__arrowhead polyline {
fill: var(--xy-edge-stroke, var(--xy-edge-stroke-default));
stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));
}
.xy-flow__connection { .xy-flow__connection {
pointer-events: none; pointer-events: none;
+1 -1
View File
@@ -91,7 +91,7 @@ export enum ConnectionLineType {
*/ */
export type EdgeMarker = { export type EdgeMarker = {
type: MarkerType | `${MarkerType}`; type: MarkerType | `${MarkerType}`;
color?: string; color?: string | null;
width?: number; width?: number;
height?: number; height?: number;
markerUnits?: string; markerUnits?: string;
+1 -1
View File
@@ -26,7 +26,7 @@ export function createMarkerIds(
defaultMarkerEnd, defaultMarkerEnd,
}: { }: {
id?: string | null; id?: string | null;
defaultColor?: string; defaultColor?: string | null;
defaultMarkerStart?: EdgeMarkerType; defaultMarkerStart?: EdgeMarkerType;
defaultMarkerEnd?: EdgeMarkerType; defaultMarkerEnd?: EdgeMarkerType;
} }