Skip to content
Native Animation
Marquee Goal Pills animation preview

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

npx nativeanimation add marquee-goal-pills
Onboarding

Marquee Goal Pills

A selectable goal marquee that gathers chosen pills into a summary, with configurable goals, copy, accent 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 selectable goal marquee that gathers chosen pills into a summary, with configurable goals, copy, accent color, and fonts.

Why this one

A conventional goal picker is a static chip grid. Here, dragging a lane adds momentum, vertical scrolling energizes the wall, and every selection pauses its lane for a half-second. Confirmation gathers the actual selected pill surfaces into a small stack. No photographs, remote assets, or real sports identities are involved.

How it works

  1. Each lane owns one continuous displacement shared value. A UI-thread frame callback adds its 18–30 pt/s drift and exponentially decaying gesture velocity.
  2. Repeated copies wrap outside the visible viewport with a constant 12 pt gutter. Horizontal pan wins over a pill's tap; vertical movement remains available to the page.
  3. Selection changes React state once per action. Separate UI-thread values handle pressure, the critically damped 1.08 selection scale, rotating check, and three-point border.
  4. The selected count animates to an exact integer; neighboring numerals fade out. Once the required count is reached, the button surface fills from the left.
  5. A single confirmation progress captures each pill's launch position and gathers the nearest copy of each selected goal into a stack. Other copies fade away. A new selection or reset interrupts confirmation and cancels its callback.
  6. Short containers scroll the heading and wall while keeping the confirmation button in its own footer. The host supplies safe-area padding.

Install

npx expo install expo-image react-native-gesture-handler react-native-reanimated react-native-worklets

Validated dependency versions: Expo Image ~58.0.17, Gesture Handler ~3.3.0, Reanimated 4.7.1, Worklets 0.13.0. React and React Native come from the host project. Follow the host project's Reanimated setup and wrap its root in GestureHandlerRootView. No Skia setup is needed. The included demo additionally uses expo-linear-gradient and the catalog's loaded fonts.

Usage

import { MarqueeGoalPills } from './marquee-goal-pills';

const goals = [
  { id: 'cardio', label: 'CARDIO', color: '#B7E36B', textColor: '#4A0F14' },
  { id: 'core', label: 'CORE', color: '#F7A6CF', textColor: '#4A0F14' },
  { id: 'mobility', label: 'MOBILITY', color: '#1F5E4A', textColor: '#BFF2D9' },
];

export function GoalScreen() {
  return <MarqueeGoalPills goals={goals} onSubmit={(ids) => saveGoals(ids)} />;
}

Use a ref and palette props for a darker, quieter variant:

const picker = useRef<MarqueeGoalPillsRef>(null);

<MarqueeGoalPills
  ref={picker}
  goals={goals}
  rows={3}
  maxSelected={2}
  speed={0.7}
  title={'Find your\nnext move'}
  backgroundColor="#25151A"
  textColor="#F3E7D3"
  buttonTextColor="#25151A"
  accentColor="#8E86C9"
  fontFamily="YourInterfaceFont"
  titleFontFamily="YourDisplayFont"
  onChange={(ids) => updateDraft(ids)}
/>;

// Called from an event handler:
picker.current?.toggle('cardio');
picker.current?.submit();
picker.current?.reset();

Import useRef from React and MarqueeGoalPillsRef from this folder. Supply stable, unique goal IDs. maxSelected is the required count as well as the selection limit; it is capped to the available goals. An empty list disables confirmation. Remount the component when replacing the goal dataset or changing the selection limit.

Props

PropTypeDefaultPurpose
goalsMarqueeGoal[]RequiredStable ID, visible label, surface color, text color
rowsnumber5Alternating lanes, capped to goal count
maxSelectednumber3Required selection count and maximum
titlestringChoose\nyour goalsEditorial heading; accepts a newline
descriptionstringPick what moves you.Instructions
brandReactNode—Host-provided wordmark or brand tile
startLabelstringGET STARTEDInitial action label
submitLabelstringLET'S GOReady action label
completeLabelstringYOU’RE SETConfirmed action label
selectedLabelstringselectedCounter suffix
announce(ids, phase) => stringEnglish feedbackCustom announcements for change, limit, submit
backgroundColorstring#F3E7D3Page and footer surface
textColorstring#4A0F14Heading, borders, initial button surface
buttonTextColorstring#F3E7D3Initial button text
accentColorstring#B7E36BReady button fill
fontFamilystringSystemInterface text
titleFontFamilystringSystemHeading and pill text
monoFontFamilystringSystemRolling counter
titleSizenumber76Maximum heading size, fitted to width
speednumber1Drift multiplier; zero stops ambient drift
onChange(ids: string[]) => void—Discrete selection changes
onSubmit(ids: string[]) => void—Confirmation settled, once per submission
styleStyleProp<ViewStyle>—Root container
testIDstringmarquee-goal-pillsRoot and control identifier prefix

ref: toggle(id), submit(), and reset(). Repeated submit calls are ignored until reset or a selection change. No callback fires for a canceled submission.

Compatibility

Targets Expo SDK 58, React Native 0.88.0, and Reanimated 4.7.1. Uses modules bundled in Expo Go. Supports iOS, Android, and web. The component caps its content width at 600 pt and scrolls in small embeds and landscape. It applies no safe-area insets; full-screen hosts must provide them.

Accessibility

Pills expose checkbox labels and checked states; repeated decorative copies are hidden from assistive technology. The button announces its readiness and selection count. Important selection and confirmation changes receive announcements; announce localizes all feedback. Selection requires only a tap, and accessibility activation actions are provided. Pills are at least 64 pt high; the button is at least 58 pt high. With reduced motion enabled, ambient motion, gesture displacement, pill scaling, check rotation, and collection flights are removed; selection and confirmation use 200 ms fades.

Demo timeline and review notes

The Aurora demo uses the kit brand and Geist 800. Autoplay drifts for 2 seconds, selects CARDIO at 2.00 s, CORE at 2.38 s, FULL BODY at 2.76 s, and gathers them at 3.50 s; it resets every 5.80 s. Timers are canceled on unmount. The first visible selection response occurs within the first three seconds.

Code review: text uses at least 1.1× line height; the brand stays on the left and the top-right viewer close area is empty. At 390×844 the heading, five lanes, and footer fit in flow; at 320×694 the wall scrolls without overlapping the footer. Small embeds use the same scroll fallback. The button fill is the button's own clipped material, and the flight moves the pill surfaces themselves; no opaque block passes over text.

Icons: Lucide (ISC). Paths are copied from the repository kit into icons.ts. The demo uses Aurora from the kit; all goal copy and colors are supplied through props.