Skip to content
Native Animation
Spec Compare Duel animation preview

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

npx nativeanimation add spec-compare-duel
SkiaGlass

Spec Compare Duel

A product comparison with a draggable space-sharing divider, with configurable products, specifications, background color, 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 product comparison with a draggable space-sharing divider, with configurable products, specifications, background color, and fonts.

Why this one

Facing bars redistribute with the divider while preserving each specification's true winner. Expanding the winning side grows its camera to roughly 55% of screen width and centers an accent light behind it; crossing 70% locks the comparison and reveals a contextual choice. This turns a static two-column table into a physical decision without hiding the underlying data.

How it works

  1. Measure the container with onLayout; allocate the image stage after reserving 34-point specification rows and the choice area. The demo starts the stage at 14% of screen height and extends it to about 50%.
  2. A single shared progress value drives both clipped product regions, camera scale, light, divider, and facing bars.
  3. Pan and tap gestures share the same interruptible spring. Dragging cancels the current animation and preserves its position; release transfers velocity into the settling spring.
  4. Specification values roll into clipped, measured windows. The winner check brightens with its side's emphasis.
  5. Crossing 70% exposes a selection summary and choose button. Discrete transitions alone reach React through scheduleOnRN.
  6. The demo schedules Mini → balanced → One → hold, looping every 4.8 seconds. Touch cancels its timers.

Install

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

Copy this folder, excluding demo/. Wrap the host in GestureHandlerRootView. Configure Reanimated/worklets for your host Expo version. On web, load CanvasKit with Skia's WithSkiaWeb boundary before importing the component. Keep product images local and provide their sources through props. No demo image is imported by the component.

Usage

import { SpecCompareDuel } from './spec-compare-duel';

<SpecCompareDuel
  products={[
    { name: 'Camera One', price: '$1,299', image: require('./camera.png'), accent: '#F5C26B', bestFor: 'low light, 6K' },
    { name: 'Camera Mini', price: '$849', image: require('./camera.png'), accent: '#7DD3FC', bestFor: 'travel', imageScale: 0.78, coolTint: true },
  ]}
  specifications={[
    { label: 'Battery', values: ['410 shots', '520 shots'], scores: [0.79, 1] },
  ]}
/>

Personalize motion and typography, and connect the selection to your checkout:

<SpecCompareDuel
  ref={comparisonRef}
  products={products}
  specifications={specifications}
  fontFamily="YourInterfaceFont"
  monoFontFamily="YourNumericFont"
  springDamping={32}
  heroFraction={0.4}
  onChoose={(side) => openCheckout(side)}
/>
// In an event handler: comparisonRef.current?.focus('left');

The imperative ref exposes focus('left' | 'right' | null) and replay() (specification entrance).

Props

PropTypeDefaultDescription
productsreadonly [DuelProduct, DuelProduct]RequiredName, formatted price, image, accent, summary; optional imageScale and coolTint.
specificationsreadonly DuelSpecification[]RequiredUnique label, two values, two normalized scores. Higher is better; invert scores for weight.
fontFamilystringSystemInterface font.
monoFontFamilystringSystemNumeric font.
backgroundColorstring#0C0C0DBase surface.
springDampingnumber28Damping, clamped to at least 28 for restrained settling.
heroFractionnumber0.42Relative hero height, clamped to 0.3–0.48.
accessibilityLabelstringProduct comparison dividerAdjustable control label.
onSelectionChange(side or null) => void—Reports locked or balanced state.
onChoose(side) => void—Reports confirmed choice.
onInteraction() => void—Stops external playback on interaction.
styleStyleProp<ViewStyle>—Container style.
testIDstring—Root identifier.

The image stage has no fixed maximum height. At balance, the full-size camera spans about 40% of screen width; imageScale preserves the smaller model’s relative size. Focusing either side grows its camera to the same hero size while the other contracts to half its balanced size. Rows retain one label/value line and facing bars, without extra divider rules.

Colors should be six-digit hex strings for the radial light's alpha stops. Scores should be in [0, 1]. The shipped layout targets two products and five concise specification rows; use a sufficiently tall container for larger datasets.

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. Uses SDK 58 BlurTargetView for Android blur. Compatible with this project's Expo Go modules; other hosts need compatible native modules or a development build.

Accessibility

Product-name buttons provide a tap alternative to dragging, with selected states. The divider supports adjustable increment/decrement actions. Every row announces both values and the actual winner. Selection and confirmation are announced; the footer is a polite live region. Interactive targets are at least 48 points.

Reduced motion keeps camera scale fixed, disables blur and numeric travel, and replaces settling springs with 200 ms changes. No particles, trails, or flights are used. The upper-right area is reserved for the catalog viewer's close button.

Icons: Lucide (ISC). Demo identity: Orrin, from the repository's brand kit. The public component has no brand defaults and no dependency on shared theme files.