About this animation
A dependency graph with light beams following job states, with configurable nodes, connections, palette, and fonts.
How it works
- Validate a bounded DAG (12 nodes, 16 edges). Omit orphan and cycle-closing edges in input order.
- Measure the component's own host with
onLayout. Cache cubic geometry on layout/data changes, independently of animation frames. - Drive every active beam from one UI-thread clock, offset by a stable hash of its edge ID. Skia path trimming follows curve length;
progressByIdcaps the active extent. A beam occupies at most 14% of its connection, with a fading trail. - Keep job states in the host. Retry and node presses request actions; the component never changes those states.
- When all required edges become complete, converge the beams and compress one radial output halo in 420 ms. Emit one completion callback per valid cycle. Reset and replay affect visuals only.
- Stop the clock in background, while hidden/disabled, without active edges, or with reduced motion. The demo owns its simulated data and timers.
Install
Copy this directory, excluding demo/, into your app. Install SDK-compatible packages:
npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets expo-blur expo-linear-gradientUse your app's Reanimated/worklets setup. On web, initialize CanvasKit before importing this module using the Skia web setup. The demo uses the catalog host's deferred Skia loader; that loader is not part of this component.
Usage
The host supplies a nonzero height and owns safe areas. States are required, controlled inputs.
import { ConnectionFlowBeams, type FlowNode } from './connection-flow-beams';
const nodes: FlowNode[] = [
{ id: 'source', label: 'Source', icon: 'file-text', position: { x: 0.2, y: 0.5 } },
{ id: 'output', label: 'Output', icon: 'cloud', position: { x: 0.8, y: 0.5 } },
];
<ConnectionFlowBeams
style={{ height: 440, flex: 0 }}
nodes={nodes}
edges={[{ id: 'job', from: 'source', to: 'output' }]}
statusById={{ job: 'active' }}
/>With controlled retry and motion knobs:
const [status, setStatus] = useState<FlowStatus>('error');
const flow = useRef<ConnectionFlowBeamsHandle>(null);
<ConnectionFlowBeams
ref={flow}
nodes={nodes}
edges={[{ id: 'job', from: 'source', to: 'output', required: true }]}
statusById={{ job: status }}
progressById={{ job: 0.8 }}
colorScheme="dark"
pulseDuration={1600}
motion={{ durationScale: 0.9, glowOpacity: 0.22 }}
palette={{ accent: '#B6A4FF', onAccent: '#151720' }}
labels={{ outputReady: 'Output ready', retry: 'Try again' }}
onRetry={() => setStatus('active')}
onAllComplete={() => saveCompletedJob()}
/>
// Update status to 'complete' when the real job finishes.
// flow.current?.replay() repeats visuals without firing onAllComplete.The second example assumes React hooks, the exported FlowStatus and ConnectionFlowBeamsHandle types, and a host saveCompletedJob function.
Props
| Prop | Default | Purpose |
|---|---|---|
nodes | required | Readonly node data: id, label, icon: KitIconName, normalized center position |
edges | required | Readonly edges: id, from, to, required?: boolean (default true) |
statusById | required | pending / active / complete / error for each edge ID |
progressById | missing = 1 | Active visible extent; finite values clamp to 0–1, nonfinite values use 1 |
onNodePress | omitted | Requests a node action by ID |
onRetry | omitted | Requests an edge retry by ID; host supplies new state |
onAllComplete | omitted | Once per transition into all required edges complete |
labels | neutral English | pending, active, complete, error, retry, empty, outputReady |
pulseDuration | 1400 | Positive pulse period in ms; invalid values use 1400 |
colorScheme | auto | light/dark/auto |
palette | Paper/Night | Partial override of every semantic token, plus highlight and shadow |
fontFamily | system | Body and labels |
titleFontFamily | body | Output heading |
monoFontFamily | body | Status labels |
motion | 1 / 0.25 | durationScale clamps 0.5–2; glowOpacity clamps 0–0.3 |
disabled | false | Disables actions and visual playback; reset remains available |
isVisible | true | Set false when a mounted host loses focus |
accessibilityLabel | omitted | Optional localized root label |
testID | connection-flow-beams | Root; -node-<id> and -retry-<edgeId> suffixes |
style | flex:1 | Root override applied last; no safe areas |
The ref exposes replay() and reset(). Replay is a no-op unless the graph is complete, visible, enabled, and foregrounded. Neither method mutates jobs or emits completion. An error or another non-complete required state re-arms the completion cycle; a changed edge topology starts a new cycle. An empty graph or a graph with no required edges never emits completion. Optional errors do not prevent required-edge completion.
Validation keeps the first unique nonempty node/edge IDs, at most 12/16. Unknown states become pending; unknown record keys are ignored. Invalid positions default to center, finite positions clamp to bounds. Unknown runtime icon names render a blank tile with the node's accessible name. Colliding coordinates use topologically ordered vertical layout, with a scrollable stage. The status list remains available in compact layouts, at 200% text, and for dense graphs; the graph and list scroll independently only when their allocated space is insufficient. The demo has a single outer scroll for short screens/landscape.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, React 19.2.3, Reanimated 4.7.1, worklets 0.13.0, and Skia 2.14.0. Uses Expo Go's installed modules, without new native dependencies.
Glass samples a real gradient via one BlurTargetView shared by the node tiles. Each wrapper and BlurView has its own radius and clipping. Android uses dimezisBlurViewSdk31Plus; older Android versions retain the solid surface fallback. The host can set isVisible on navigation changes. No timers or decorative clock remain after unmount.
Accessibility
The graph is paired with a native text list naming each source, destination, and status. Error uses explicit text and an exclamation mark; success uses a check mark and text. Retry has a 48-point minimum target. Node targets are 48–56 points. Tab/Enter activate native Pressables on web, with visible focus. RTL mirrors node placement and connection direction. Text scales naturally and can scroll without truncation.
Reduced motion is read initially and subscribed to live: rails and statuses remain, beams and pressure motion stop, and completion becomes immediate. Native status announcements occur on discrete data changes; web has polite live status content.
Why this one
Light carries data: an error cuts only its own connection, progress limits its beam, and required-edge completion governs the output. Nimbus's demo turns three trip-planning documents into one brief, with a recoverable failure before the final convergence. The 5-second simulation reaches success at 2500 ms and resets at 4200 ms. Touch cancels simulation; Replay starts one cycle.
Assets and licenses
Raster assets: N/A. Icons: Lucide (ISC), exact paths copied from the project kit. The Nimbus cloud glyph and name are from the internal brand kit; no reference brand or artwork is shipped. The demo alone imports the catalog's OFL Geist/Instrument Serif fonts. The reusable component defaults to system fonts. Package licenses remain those of their respective copyright holders.




