Skip to content
Native Animation
Sankey Flow Focus animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add sankey-flow-focus

Sankey Flow Focus

A flow diagram with proportional ribbons and node or link focus, with configurable nodes, links, palette, and fonts.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

A flow diagram with proportional ribbons and node or link focus, with configurable nodes, links, palette, and fonts.

How it works

  1. Validate IDs, finite nonnegative amounts, increasing columns, and internal conservation within 0.01 units. Invalid datasets expose their rows without drawing a misleading diagram.
  2. Compute one pixels-per-unit scale for every node and ribbon, with 8-point nodes and 16-point vertical gaps. Zero links are omitted; small links never receive a fabricated minimum thickness.
  3. Precompute cubic ribbon paths and use the same paths for geometric tap testing. Selection changes opacity rather than geometry.
  4. Derive ribbon opacity from a shared focus progress. A separate 420 ms pulse trims the selected centerline while the single total rolls into place. Node totals use the larger of inflow and outflow, rather than adding both.
  5. Keep all text outside the Canvas. Expand the equivalent flow list to reach every link and node using a keyboard, screen reader, or a 48-point button.

Install

Copy this folder, excluding demo/, into your project.

npx expo install @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-worklets expo-blur expo-linear-gradient

Developed against Expo SDK 58, React Native 0.88.0, React 19.2.3, Skia 2.14.0, Gesture Handler 3.3, Reanimated 4.7.1, expo-blur 58.0.5, and expo-linear-gradient 58.0.3. Configure Reanimated and wrap your application in GestureHandlerRootView. On web, load CanvasKit before importing the component, following Skia's web setup. No chart library, provider, external assets, or network request is required by the component.

Usage

import SankeyFlowFocus from './components/sankey-flow-focus';

const nodes = [
  { id: 'income', label: 'Income', column: 0 },
  { id: 'spending', label: 'Spending', column: 1 },
] as const;
const links = [
  { id: 'income-spending', source: 'income', target: 'spending', value: 1200 },
];

// The parent supplies a bounded height and any safe-area padding.
<SankeyFlowFocus nodes={nodes} links={links} maxColumns={2} style={{ height: 400, flex: 0 }} />;

Controlled selection and personality knobs:

const chart = useRef<Handle>(null);
const [selected, setSelected] = useState<Selection>(null);

<SankeyFlowFocus
  ref={chart}
  nodes={nodes}
  links={links}
  maxColumns={2}
  selected={selected}
  onSelect={setSelected}
  colorScheme="dark"
  palette={{ accent: '#C2ADF5', focus: '#C2ADF5' }}
  flowColors={{ 'income-spending': '#AA95D6' }}
  motion={{ durationScale: 0.8, glowOpacity: 0.2 }}
  formatValue={(value) => `$${value.toLocaleString('en-US')}`}
/>;

// Methods request a controlled change through onSelect once.
chart.current?.focusLink('income-spending');

Import Handle and Selection from the same folder. Supply all localized labels, node names, and formatters through props. The demo's Aster branding and fonts are separate from the component.

Props

PropDefaultBehavior
nodes: readonly FlowNode[]RequiredUnique IDs and labels, columns 0–2; maximum 12 nodes.
links: readonly FlowLink[]RequiredUnique IDs, source, target, and finite amount; maximum 20 links.
maxColumns: 2 | 33Two-column data uses columns 0 and 1.
selected: SelectionUncontrolled{kind: 'node' | 'link', id} or null.
defaultSelected: SelectionnullInitial uncontrolled focus.
onSelect(selection)NoneOne request per user or imperative selection action.
formatValue(value)US number formatFormats totals, rows, and announcements.
labels: Partial<Labels>Neutral Englishtotal, from, to, amount, clearSelection, empty, invalid, showFlows, hideFlows, and flow(source, target, formattedValue).
flowColorsThree violet palette tokensOptional color per link ID.
colorScheme'auto''light', 'dark', or system.
palette: Partial<Palette>Paper/NightAll semantic colors plus ribbonSecondary, ribbonTertiary, and highlight.
fontFamily, titleFontFamily, monoFontFamilySystemHost fonts; no catalog dependency.
motion{durationScale: 1, glowOpacity: .25}Finite values clamp to 0.5–2 and 0–0.3.
disabledfalseBlocks selection and replay, including handle calls; reset remains available.
styleNoneApplied last to the flex root.
testID'sankey-flow-focus'Stable root, -diagram, -node-ID, -flow-ID, -toggle-list, -flow-list, -clear.
accessibilityLabelNoneOptional host description.

The handle exposes focusNode(id), focusLink(id), clear(), replay(), and reset(). Replay reruns the entrance and preserves focus. Reset requests no selection, closes details, and replays. Unknown IDs are ignored. A focus whose ID remains in a replacement dataset is retained; removed IDs fall back to no focus. Controlled props always win. Treat input arrays as immutable.

Empty arrays or all-zero links show the empty label. Duplicate IDs, backward or same-column links, orphan links, negative/nonfinite amounts, exceeded budgets, and unbalanced internal nodes show the invalid label and raw rows. Duplicate rows receive distinct React keys but retain their supplied IDs in test selectors. Formatters must accept finite numbers; nonfinite row values use the invalid label.

Compatibility

This is an Expo/RN component, with iOS, Android, and web implementations supplied by the installed libraries.

The total card blurs its own mineral gradient through a BlurTargetView. Android uses dimezisBlurViewSdk31Plus; older Android versions retain a legible solid base. Both the material and its clipping layers have their own radius. The diagram uses one Canvas; the demo adds one small brand Canvas. Raster assets and image-density requirements: N/A.

Accessibility

All labels are React Native text outside the Canvas. The component owns vertical scrolling so the expanded equivalent list remains reachable in a 300×400 container or with large type. The host owns safe areas. The demo keeps theme and replay controls in the lower flow. No essential text is truncated and font scaling stays enabled.

Explore flows exposes node buttons and every nonzero From/To/Amount row with selected and disabled states. These controls have a minimum height of 48. Narrow ribbons use exact geometry rather than overlapping enlarged targets. Tab and Enter activate buttons on web; Escape clears selection. RTL mirrors chart columns and keeps list content in logical order. A confirmed focused selection generates one screen reader announcement, never one per frame.

useReducedMotion() supplies the initial preference, with a live reduceMotionChanged subscription. Reduced motion reveals the graph immediately, removes the traveling pulse and total translation, and changes focus with a 200 ms fade. No decorative loop runs inside the component. The demo's optional five-second autoplay uses public handle methods, stops on real input/background, and clears timers on unmount.

Why this one

A many-to-many diagram explains where resources move while preserving amount through each intermediate node. Focus never inflates a small ribbon, shifts a node, or counts both sides of an internal transfer. The equivalent list keeps the financial story available when geometry alone is too small to touch.

The reference informed only the general Sankey pattern. Geometry, code, data, copy, palette, and interaction are original. The demo uses the kit's exact Aster name and ISC-licensed Lucide sparkle path. Demo fonts are provided by the host under their respective OFL licenses. No reference artwork is shipped. Distribution is subject to the enclosing Native Animation license.