Skip to content
Native Animation
Circular Text Scrubber animation preview

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

npx nativeanimation add circular-text-scrubber

Circular Text Scrubber

A draggable circular text ribbon with a stationary center and configurable copy, radius, palette, 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 draggable circular text ribbon with a stationary center and configurable copy, radius, palette, and fonts.

How it works

Actual proportional glyph widths determine the arc layout. Accessible 30-degree controls offer the same interaction without dragging. A single pink separator crosses twelve o'clock and reveals a 24-degree edge light once per release. Local measurement, a complete Paper/Night palette, and a readable flow layout make the ribbon usable beyond a full-screen demo. No reference artwork or font is included.

  1. Invisible native Text nodes measure each distinct glyph with onLayout, including on web. Font size is 17 pt, tracking is 0.04 em.
  2. The local slot limits the radius using its smaller dimension. Whole phrase repetitions are removed before tracking is tightened; an overlong phrase becomes ordinary text instead of shrinking the type.
  3. Glyph centers follow cumulative advance widths divided by radius. The first separator begins 65 degrees before twelve; all remaining glyphs use the regular ink color.
  4. One rotation shared value controls the ribbon. Gesture angles unwrap at the seam; tangential release velocity drives withDecay with deceleration 0.995.
  5. Crossing twelve after release reveals an actual curved edge stroke for 360 ms. Pressure and highlight clocks do not alter rotation. Buttons use an interruptible spring with explicit mass; the Paper separator mixes the accent with theme ink for contrast.
  6. The demo alone calls the ref: rest 600 ms, rotate 80 degrees over 700 ms, settle, reset at 3900 ms over 300 ms, repeat at 5000 ms. Direct input cancels every timer.

Install

npx expo install @shopify/react-native-skia@2.14.0 react-native-gesture-handler@~3.3.0 react-native-reanimated@4.7.1 react-native-worklets@0.13.0

The host must provide GestureHandlerRootView. For web, initialize CanvasKit before mounting this component, following the host project's Skia setup. The optional demo also uses expo-linear-gradient and the catalog's preloaded Geist, Instrument Serif, and Geist Mono fonts. The component has no theme or catalog imports.

Usage

import { useRef } from 'react';
import { View } from 'react-native';
import {
  CircularTextScrubber,
  type CircularTextScrubberRef,
} from './circular-text-scrubber';

const labels = {
  instruction: 'Trace the circle, or use the buttons.',
  empty: 'Add a reminder to begin.',
  rotationEnd: 'Your reminder is at rest.',
  clockwiseValue: '+30°',
  counterclockwiseValue: '−30°',
};

export function Reminder() {
  const ribbon = useRef<CircularTextScrubberRef>(null);
  return (
    <View style={{ width: 300, height: 400 }}>
      <CircularTextScrubber
        ref={ribbon}
        text="Take a breath • Leave some room •"
        separator="•"
        centerTitle="A moment for you"
        centerBody="Start here."
        accessibilityLabel="Rotating reminder"
        clockwiseLabel="Turn clockwise 30 degrees"
        counterclockwiseLabel="Turn counterclockwise 30 degrees"
        labels={labels}
        colorScheme="dark"
        radius={112}
        onRotationEnd={(angle) => saveAngle(angle)}
      />
    </View>
  );
}

saveAngle represents your application callback. To customize, pass palette={circularTextPalettes.light}, fontFamily="YourMediumFont", titleFontFamily="YourSerif", and monoFontFamily="YourMono". Load those font families before mounting. ribbon.current?.rotateBy(80), .stop(), and .reset() are event-handler calls, never render-time reads.

Props

PropTypeDefaultDescription
textstringRequiredFull semantic phrase. Empty text displays labels.empty.
separatorstringRequiredDelimiter; appended when missing. Empty disables highlighting. Prefer one glyph.
centerTitle, centerBodystringRequiredStationary center copy.
radiusnumberLocal fitPreferred radius, clamped to the local slot. Nonfinite/nonpositive values use local fit.
onRotationEnd(angle: number) => void—Once per completed motion or active stop, normalized to [0, 360). Interrupted motion emits no completion.
clockwiseLabel, counterclockwiseLabelstringRequiredAccessible button names.
labelsobjectRequiredinstruction, empty, rotationEnd (announcement), clockwiseValue, counterclockwiseValue (visible button text).
accessibilityLabelstringRequiredRoot description.
colorScheme'light' | 'dark' | 'auto''auto'Complete system-aware theme.
paletteCircularTextPaletteTheme paletteOverrides background, surface, text, muted, border, accent, success, error.
fontFamily, titleFontFamily, monoFontFamilystringSystemRing/body, center heading, controls.
onInteractionStart() => void—Cancel host automation before drag/button input.
styleStyleProp<ViewStyle>—Root override; root has flex 1. Host owns safe areas.
testIDstring'circular-text-scrubber'Stable -ring, -phrase, -clockwise, -counterclockwise suffixes.

No controlled angle prop is exposed: the ref requests relative motion and callbacks report completion without duplicate events. rotateBy(NaN) and infinite deltas are ignored. stop() is idempotent while idle. reset() returns from the current pose over 300 ms. There are no exercise/session APIs, IDs to resolve, network requests, or internal autoplay.

Compatibility

Targets 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. Supports iOS, Android, and web with Expo Go-compatible native dependencies; web needs the CanvasKit setup above.

No errors are reported in this piece.

Implementation notes

The ordinary phrase preserves reading order; the center switches to an ordinary flow layout when measured copy exceeds its circle; the component never creates an internal scroll region. The demo reserves the upper-right corner and places appearance controls below the piece. The accent separator starts 65 degrees before twelve, so the demo's 80-degree turn schedules the 360 ms light arc within its first three seconds. Light is a curved stroke and radial halo, with no rectangular overlay over type.

Icons in the optional demo: Lucide (ISC), copied from the local kit. Aurora uses the kit's sparkles glyph on a palette-matched tile. No generated assets.

Accessibility

Buttons have roles, explicit labels, and at least 48-point targets. The phrase is repeated in normal reading order below the decorative ring, which is hidden from screen readers. Completion announcements use supplied copy. Text scaling stays enabled.

Reduced motion, font scaling above 130%, slots smaller than 240 points, center copy that exceeds its measured circle, and phrases too long for one circumference use a non-scrolling flow layout with the center and phrase preserved. Rotation requests in this mode update the semantic angle with a 200 ms opacity pulse, without rotation or traveling light. The demo uses the available height instead of a fixed stage: at 320×694 it reduces heading spacing and omits secondary copy, while 390×844 keeps the full copy. Both keep the rotation and appearance controls on screen without a ScrollView. Below 620 points of local height (including 320×568 and landscape), or above 130% font scaling, only the full-screen outer container scrolls, with its vertical indicator hidden; there is no second safe area. The top-right corner remains clear. Parent layouts should provide at least the 48-point action row; a 300×400 slot is supported by the layout design.