Skip to content
Native Animation
Pixel Grid Voice animation preview

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

npx nativeanimation add pixel-grid-voice
Skia

Pixel Grid Voice

An LED face that morphs into listening bars and speaking waves, with configurable state labels, shortcuts, colors, 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

An LED face that morphs into listening bars and speaking waves, with configurable state labels, shortcuts, colors, and fonts.

Why this one

Voice gets a face instead of another orb. The same rounded LEDs remain on screen throughout each state change: a spring blends their occupancy with a column stagger, preserving continuity when the interaction is interrupted. Amber light is shaded inside each LED and spills into its surrounding grid; no rectangular shine overlays the content.

How it works

  1. onLayout measures the component and panel. Column count determines square LED pitch, and the available height determines rows.
  2. One Skia runtime shader draws the entire grid, its bezels, warm spill, and brighter LED centers. No React elements are created per LED.
  3. One spring progress blends four state weights per column. An interruption snapshots the current weights before starting the next transition.
  4. A UI-thread clock drives idle gaze/blinks, simulated listening amplitude, the thinking trail, and the symmetric speaking shape. A supplied level replaces the simulated amplitude.
  5. Gesture Handler composes a short tap with a 280 ms hold. Press feedback uses its own spring. Response timers update React only when the state changes, and are canceled on interruption or unmount.
  6. Reduced motion freezes the clock and replaces the column stagger with a 200 ms crossfade between still shapes.

Install

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

For web, copy the matching CanvasKit binary into your public assets:

npx setup-skia-web

Copy PixelGridVoice.tsx, shaders.ts, icons.ts, and index.ts together. Wrap your app in GestureHandlerRootView. Configure Reanimated/worklets according to your Expo SDK. For web, make CanvasKit available using Skia's web setup and load Skia before mounting the component; the catalog already supplies this loader.

Icons: Lucide (ISC). Only the used paths are bundled in icons.ts.

Usage

import { PixelGridVoice } from './pixel-grid-voice';

// The host supplies screen safe areas, or a bounded integration container.
<View style={{ flex: 1 }}>
  <PixelGridVoice />
</View>
const assistant = useRef<PixelGridVoiceRef>(null);
const audioLevel = useSharedValue(0);

<PixelGridVoice
  ref={assistant}
  level={audioLevel}
  columns={16}
  title="Your assistant"
  meetingTime="10:30"
  colors={{ lit: '#FFB020', pill: '#E5F23C' }}
  onStateChange={(next) => handleVoiceState(next)}
  onKeyboardPress={() => openComposer()}
  onCameraPress={() => openCamera()}
  onShortcutPress={(index) => openWorkspaceSection(index)}
/>

// Drive real audio via audioLevel.set(amplitude), and the lifecycle explicitly:
assistant.current?.listen();
assistant.current?.stop();
assistant.current?.speak();
assistant.current?.reset();

state makes the state machine controlled: methods and gestures request a state through onStateChange, while the host decides when to apply it. Automatic thinking/speaking/idle timers run only when uncontrolled. No microphone permission, recording, transcription, keyboard, or camera integration is included. Connect those actions with the callbacks; the default auxiliary actions demonstrate listening or speaking.

Props

PropTypeDefaultPurpose
state'idle' | 'listening' | 'thinking' | 'speaking'uncontrolled idleHost-controlled visual state
onStateChange(state) => void—Requested state changes
levelnumber | SharedValue<number>simulatedAudio amplitude, clamped to 0–1; invalid input becomes zero
columnsnumber12Column count, clamped to 8–24
colorsPartial<PixelGridVoiceColors>dark palette belowComplete surface, text, LED, and control palette
titlestringAssistantHeading
brandIconReactNodeamber pixel cloudHost-supplied brand tile; pass null to hide it
leadingIconReactNode—Legacy tile override; used when brandIcon is undefined
glassTint'light' | 'dark'darkBlur material tint
stateLabelsPartial<Record<PixelGridVoiceState, string>>Ready when you are / Listening… / Thinking… / SpeakingState announcements and subtitle
talkLabel, listeningLabelstringTalk to me / Tap to finishTalk pill copy
meetingLabel, meetingTime, meetingDetailstringNext meeting / 09:56 / Daily check-inMeeting card content
shortcutsLabelstringYour workspaceShortcut card heading
shortcutsreadonly PixelGridVoiceShortcut[]Calendar / Tasks / Notes / FocusUp to four entries, with kit icons
onShortcutPress(index: number) => voidsimulated state actionWorkspace action
onKeyboardPress, onCameraPress() => voidlisten / speakHost integrations
keyboardLabel, cameraLabelstringUse keyboard / Use cameraAuxiliary action announcements
accessibilityLabelstringTalk: tap to toggle or hold to listenTalk action announcement
fontFamily, headingFontFamily, lightFontFamilystringsystemBody, heading, and time fonts
thinkingDuration, speakingDurationnumber1500 / 2500Simulated response delays in ms
styleStyleProp<ViewStyle>—Root container style
testIDstringpixel-grid-voiceRoot ID; controls append -talk, -keyboard, -camera, -shortcut-N

The default palette is background #050506, panel #080809, cell #141416, lit #FFB020, pill #E5F23C, text #F4F4F6, muted #A3A3AD, border rgba(255,255,255,0.12), glass rgba(255,255,255,0.045), and pillText #202406. A light theme should override the entire palette and set glassTint="light".

The demo uses the default 44×44 dark tile with a 5×4 amber pixel cloud and Geist body/light typefaces. The tile matches the unlit grid cells and their dim bezel; perimeter pixels are softer than the center. Thinking and speaking gently pulse its LED cores and halo using the grid’s shared clock and state blend, without per-frame React updates. Reduced motion leaves the cloud still. Geist 600 is loaded locally for the demo heading, without changing the host font registry.

Timing and layout

Autoplay lives entirely in demo/: idle 1.2 s → listening 2.5 s → thinking 1.5 s → speaking 2.5 s → repeat. The first recomposition starts at 1.2 s, inside the first three seconds. The spring uses explicit mass 1, damping 30, and stiffness 260; normal state changes settle in roughly half a second.

The component fills its host, caps the content width at 592 pt (420 pt in short landscape containers), and reserves the upper right for host controls. Primary actions stay visible below the scrollable content. The panel keeps at least six rows so wide containers cannot crop the LEDs. At 300×400, in landscape, or with large text, the cards scroll instead of overlapping controls. Listening and speaking return the scroll to the visualizer; reduced motion makes this immediate. The host applies safe-area insets. Interactive targets are at least 48 pt; text scaling remains enabled.

Compatibility

Built for Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, Gesture Handler 3.3, and Skia 2.14.0. Dependencies are SDK-compatible and available in the project's Expo Go setup. Android glass uses a shared BlurTargetView with dimezisBlurViewSdk31Plus; older Android versions receive the material fallback.

Accessibility

The state subtitle is a polite live region. Talk has an accessible activation action as an alternative to holding. Auxiliary controls and shortcuts have localizable button labels. Decorative LEDs are hidden from the accessibility tree. Reduced motion disables blinking, gaze, sweeping, spectrum motion, and moving grain, and uses a 200 ms crossfade between static state shapes. Listening and speaking still accept state requests immediately.