Veltrix

loading

Matrix

A compact 5×5 LED matrix that gives asynchronous and AI states their own visible motion language.

AppleiOSAndroidAndroidExpoExpo Go
skiareanimated
GitHubOpen in GitHub

Installation

Install the dependencies, then copy the files for your preferred renderer.

pnpm add @shopify/react-native-skia react-native-reanimated

Behavior

Use state to select the status pattern, size for its footprint, and colors to override visual roles. Pass paused to stop motion while preserving the current state.

Install tailwind variants

pnpm add tailwind-variants

Usage

SyncStatus.tsx
import { Matrix } from "@animations/ui/components/loading/status-matrix/uniwind";const statusColors = {  error: "#b42318",  neutral: "#344054",  success: "#067647",};export function SyncStatus() {  return (    <Matrix      className="self-start"      colors={statusColors}      size={40}      state="syncing"    />  );}

Component files

5 files

status-matrix/config.ts
import {  STATUS_MATRIX_STATES,  type MatrixColors,  type MatrixState,} from "./types";export const STATUS_MATRIX_CONFIG = {  size: 36,  transitionDuration: 420,} as const;export const STATUS_MATRIX_COLORS = {  error: "#ff2e3a",  info: "#2e80ff",  neutral: "#e8ebed",  offline: "#737880",  recording: "#ff3340",  success: "#14e088",  warning: "#ff9e0a",} satisfies MatrixColors;export const STATUS_MATRIX_STATE_INDEX = Object.fromEntries(  STATUS_MATRIX_STATES.map((state, index) => [state, index]),) as Record<MatrixState, number>;
status-matrix/matrix-artwork.tsx
import {  Canvas,  Fill,  Shader,  Skia,  useCanvasSize,  useClock,} from "@shopify/react-native-skia";import { StyleSheet } from "react-native";import {  useDerivedValue,  useReducedMotion,  type SharedValue,} from "react-native-reanimated";import {  STATUS_MATRIX_COLORS,  STATUS_MATRIX_STATE_INDEX,} from "./config";import type { MatrixColors } from "./types";const matrixEffect = Skia.RuntimeEffect.Make(`    uniform float2 resolution;    uniform float time;    uniform float currentState;    uniform float previousState;    uniform float transition;    uniform float4 neutralColor;    uniform float4 recordingColor;    uniform float4 successColor;    uniform float4 errorColor;    uniform float4 warningColor;    uniform float4 infoColor;    uniform float4 offlineColor;    const float TAU = 6.28318530718;    float clamp01(float value) {      return clamp(value, 0.0, 1.0);    }    float point(float2 cell, float x, float y) {      return (1.0 - step(0.1, abs(cell.x - x))) *        (1.0 - step(0.1, abs(cell.y - y)));    }    float within(float value, float minimum, float maximum) {      return step(minimum - 0.1, value) * step(value, maximum + 0.1);    }    float noiseHash(float2 point) {      return fract(sin(dot(point, float2(127.1, 311.7))) * 43758.5453123);    }    float softNoise(float2 cell, float phase) {      float base = noiseHash(cell + floor(phase) * 17.0);      float next = noiseHash(cell + (floor(phase) + 1.0) * 17.0);      float mixAmount = smoothstep(0.0, 1.0, fract(phase));      return mix(base, next, mixAmount);    }    float cycleWave(float elapsed, float duration, float phase) {      return 0.5 + 0.5 * sin(elapsed * TAU / duration + phase);    }    float streamColumnPhase(float x) {      if (x < -1.5) {        return 1.44;      }      if (x < -0.5) {        return 1.57;      }      if (x < 0.5) {        return 0.92;      }      if (x < 1.5) {        return 1.93;      }      return 0.22;    }    float recordingCross(float2 cell) {      float vertical = point(cell, 0.0, -1.0) +        point(cell, 0.0, 0.0) +        point(cell, 0.0, 1.0);      float horizontal = point(cell, -1.0, 0.0) +        point(cell, 1.0, 0.0);      return clamp01(vertical + horizontal);    }    float check(float2 cell) {      return clamp01(        point(cell, -2.0, 1.0) +        point(cell, -1.0, 2.0) +        point(cell, 0.0, 1.0) +        point(cell, 1.0, 0.0) +        point(cell, 2.0, -1.0)      );    }    float errorX(float2 cell) {      return clamp01(        point(cell, -2.0, -2.0) + point(cell, 2.0, -2.0) +        point(cell, -1.0, -1.0) + point(cell, 1.0, -1.0) +        point(cell, 0.0, 0.0) +        point(cell, -1.0, 1.0) + point(cell, 1.0, 1.0) +        point(cell, -2.0, 2.0) + point(cell, 2.0, 2.0)      );    }    float warning(float2 cell) {      return clamp01(        point(cell, 0.0, -2.0) +        point(cell, 0.0, -1.0) +        point(cell, 0.0, 0.0) +        point(cell, 0.0, 2.0)      );    }    float info(float2 cell) {      return clamp01(        point(cell, 0.0, -2.0) +        point(cell, 0.0, 0.0) +        point(cell, 0.0, 1.0) +        point(cell, 0.0, 2.0)      );    }    float pauseBars(float2 cell) {      return within(cell.y, -1.0, 1.0) *        (point(cell, -1.0, cell.y) + point(cell, 1.0, cell.y));    }    float stopBlock(float2 cell) {      return within(cell.x, -1.0, 1.0) * within(cell.y, -1.0, 1.0);    }    float4 stateSignal(float state, float2 cell, float elapsed) {      float3 color = neutralColor.rgb;      float level = 0.15;      if (state == ${STATUS_MATRIX_STATE_INDEX.idle}) {        level = 0.145;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.loading}) {        float nearGrain = softNoise(cell, elapsed * 4.1);        float farGrain = softNoise(          cell + float2(8.7, 3.1),          elapsed * 1.7 + 0.4        );        level = 0.19 + mix(nearGrain, farGrain, 0.32) * 0.62;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.thinking}) {        float diagonal = cycleWave(          elapsed,          1.2,          (cell.x + cell.y) * 0.45 - 1.43        );        level = 0.18 + diagonal * 0.67;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.streaming}) {        float stream = cycleWave(          elapsed,          0.9,          cell.y * 0.80 + streamColumnPhase(cell.x)        );        level = 0.18 + stream * 0.70;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.searching}) {        float scan = cycleWave(elapsed, 1.1, cell.x * 0.62 - 0.17);        level = 0.16 + scan * 0.70;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.syncing}) {        float syncPhase = (cell.x + cell.y) * 0.39 - 1.04 +          0.74 * exp(-pow(length(cell) * 0.8, 2.0));        float ripple = cycleWave(elapsed, 1.3, syncPhase);        level = 0.18 + ripple * 0.66;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.connecting}) {        float radialPhase = -0.96 -          1.52 * exp(-pow(length(cell) * 0.85, 2.0));        float connectPulse = cycleWave(elapsed, 1.4, radialPhase);        level = 0.17 + connectPulse * 0.75;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.waiting}) {        float waitingPoint = -2.25 + fract(elapsed / 1.2) * 4.5;        float ping = exp(-pow(abs(cell.x - waitingPoint) * 1.18, 2.0)) *          exp(-pow(abs(cell.y) * 2.8, 2.0));        level = 0.14 + ping * 0.66;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.uploading}) {        float fill = cycleWave(elapsed, 1.0, -cell.y * 0.72 - 3.93);        level = 0.25 + fill * 0.63;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.downloading}) {        float fill = cycleWave(elapsed, 1.0, cell.y * 0.72 + 2.26);        level = 0.25 + fill * 0.63;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.listening}) {        float texture = 0.5 + 0.5 * cos(          cell.y * 0.90 + elapsed * TAU        );        float flutter = softNoise(cell, elapsed * 3.5);        level = 0.20 + texture * 0.56 + flutter * 0.10;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.speaking}) {        float syllable = 0.5 + 0.5 * cos(          cell.y * 0.95 + 2.0 - elapsed * TAU / 0.575        );        float accent = softNoise(cell, elapsed * 5.5);        level = 0.17 + syllable * 0.60 + accent * 0.12;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.recording}) {        float recordPulse = cycleWave(elapsed, 1.4, -2.58);        color = recordingColor.rgb;        level = 0.11 + recordingCross(cell) *          (0.14 + recordPulse * 0.50);      } else if (state == ${STATUS_MATRIX_STATE_INDEX.success}) {        color = successColor.rgb;        level = 0.13 + check(cell) * 0.87;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.error}) {        float errorPulse = cycleWave(elapsed, 1.1, -1.78);        color = errorColor.rgb;        level = 0.11 + errorX(cell) * (0.11 + errorPulse * 0.67);      } else if (state == ${STATUS_MATRIX_STATE_INDEX.warning}) {        float warningPulse = cycleWave(elapsed, 1.6, -0.73);        color = warningColor.rgb;        level = 0.12 + warning(cell) * warningPulse * 0.84;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.info}) {        color = infoColor.rgb;        level = 0.12 + info(cell) * 0.80;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.paused}) {        level = 0.13 + pauseBars(cell) * 0.75;      } else if (state == ${STATUS_MATRIX_STATE_INDEX.stopped}) {        level = 0.12 + stopBlock(cell) * 0.77;      } else {        color = offlineColor.rgb;        level = 0.08;      }      return float4(color, level);    }    half4 main(float2 position) {      float minimumSize = max(1.0, min(resolution.x, resolution.y));      float spacing = minimumSize * 0.175;      float radius = minimumSize * 0.061;      float antialias = max(0.55, minimumSize * 0.015);      float2 center = resolution * 0.5;      float2 normalizedCell = (position - center) / spacing;      float2 cell = float2(        floor(normalizedCell.x + 0.5),        floor(normalizedCell.y + 0.5)      );      if (abs(cell.x) > 2.0 || abs(cell.y) > 2.0) {        return half4(0.0, 0.0, 0.0, 0.0);      }      float2 dotCenter = center + cell * spacing;      float dotCoverage = 1.0 - smoothstep(        radius - antialias,        radius + antialias,        length(position - dotCenter)      );      float easedTransition = smoothstep(0.0, 1.0, transition);      float4 prior = stateSignal(previousState, cell, time);      float4 current = stateSignal(currentState, cell, time);      float4 signalValue = mix(prior, current, easedTransition);      float alpha = dotCoverage * signalValue.a;      return half4(signalValue.rgb * alpha, alpha);    }`);if (!matrixEffect) {  throw new Error("Matrix shader failed to compile.");}const matrixShader = matrixEffect;export function MatrixArtwork({  colors: colorOverrides,  currentState,  paused = false,  previousState,  transition,}: {  colors?: Partial<MatrixColors>;  currentState: SharedValue<number>;  paused?: boolean;  previousState: SharedValue<number>;  transition: SharedValue<number>;}) {  const {    ref,    size: { height, width },  } = useCanvasSize();  const clock = useClock();  const reducedMotion = useReducedMotion();  const colors = {    ...STATUS_MATRIX_COLORS,    ...colorOverrides,  };  const neutralColor = Skia.Color(colors.neutral);  const recordingColor = Skia.Color(colors.recording);  const successColor = Skia.Color(colors.success);  const errorColor = Skia.Color(colors.error);  const warningColor = Skia.Color(colors.warning);  const infoColor = Skia.Color(colors.info);  const offlineColor = Skia.Color(colors.offline);  const uniforms = useDerivedValue(() => ({    currentState: currentState.get(),    errorColor,    infoColor,    neutralColor,    offlineColor,    previousState: previousState.get(),    recordingColor,    resolution: [width, height],    successColor,    time: paused || reducedMotion ? 0 : clock.get() / 1_000,    transition: transition.get(),    warningColor,  }));  return (    <Canvas      pointerEvents="none"      ref={ref}      style={StyleSheet.absoluteFill}    >      <Fill>        <Shader source={matrixShader} uniforms={uniforms} />      </Fill>    </Canvas>  );}
status-matrix/types.ts
export const STATUS_MATRIX_STATES = [  "idle",  "loading",  "thinking",  "streaming",  "searching",  "syncing",  "connecting",  "waiting",  "uploading",  "downloading",  "listening",  "speaking",  "recording",  "success",  "error",  "warning",  "info",  "paused",  "stopped",  "offline",] as const;export type MatrixState =  (typeof STATUS_MATRIX_STATES)[number];export type MatrixColors = {  /** Color shared by neutral activity and static states. */  neutral: string;  /** Color used for the recording cross. */  recording: string;  /** Color used for the success check. */  success: string;  /** Color used for the error X. */  error: string;  /** Color used for the warning exclamation. */  warning: string;  /** Color used for the info glyph. */  info: string;  /** Color used while offline. */  offline: string;};export type MatrixBaseProps = {  /** Accessible description announced with the current state. */  accessibilityLabel?: string;  /** Color tokens resolved by the consuming app's theme. */  colors?: Partial<MatrixColors>;  /** Stops the status motion while retaining the selected state. */  paused?: boolean;  /** Height and width of the square matrix surface. */  size?: number;  /** Semantic status represented by the matrix. */  state: MatrixState;};
status-matrix/uniwind/index.tsx
import { View } from "react-native";import { STATUS_MATRIX_CONFIG } from "../config";import { MatrixArtwork } from "../matrix-artwork";import type { MatrixBaseProps } from "../types";import { useMatrix } from "../use-status-matrix";import { cn } from "#shared/utils/cn";export type MatrixProps = MatrixBaseProps & {  className?: string;};/** A compact 5×5 LED matrix for semantic async and AI states. */export function Matrix({  accessibilityLabel,  className,  colors,  paused,  size = STATUS_MATRIX_CONFIG.size,  state,}: MatrixProps) {  const { currentState, previousState, transition } =    useMatrix({      paused,      state,    });  return (    <View      accessibilityLabel={accessibilityLabel ?? `${state} status`}      accessibilityRole="image"      accessible      className={cn(        "relative items-center justify-center",        className,      )}      style={{ height: size, width: size }}    >      <MatrixArtwork        colors={colors}        currentState={currentState}        paused={paused}        previousState={previousState}        transition={transition}      />    </View>  );}
status-matrix/use-status-matrix.ts
import { useEffect } from "react";import {  cancelAnimation,  Easing,  useReducedMotion,  useSharedValue,  withTiming,} from "react-native-reanimated";import {  STATUS_MATRIX_CONFIG,  STATUS_MATRIX_STATE_INDEX,} from "./config";import type { MatrixState } from "./types";export function useMatrix({  paused = false,  state,}: {  paused?: boolean;  state: MatrixState;}) {  const reducedMotion = useReducedMotion();  const currentState = useSharedValue(    STATUS_MATRIX_STATE_INDEX[state],  );  const previousState = useSharedValue(    STATUS_MATRIX_STATE_INDEX[state],  );  const transition = useSharedValue(1);  useEffect(() => {    const nextState = STATUS_MATRIX_STATE_INDEX[state];    if (currentState.get() === nextState) {      return;    }    cancelAnimation(transition);    previousState.set(currentState.get());    currentState.set(nextState);    if (paused || reducedMotion) {      transition.set(1);      return;    }    transition.set(0);    transition.set(      withTiming(1, {        duration: STATUS_MATRIX_CONFIG.transitionDuration,        easing: Easing.linear,      }),    );  }, [currentState, paused, reducedMotion, state, transition, previousState]);  useEffect(    () => () => {      cancelAnimation(transition);    },    [transition],  );  return {    currentState,    previousState,    transition,  };}

API reference

Generated directly from the exported component props.

Matrix

A compact 5×5 LED matrix for semantic async and AI states.