CircularText

Text set around a circle — a seal, a badge, an arc of words — optionally turning.

Open Source

Installation

CircularText ships with the library — install it once, then import:

import { CircularText } from "@delacour/react-native-ui/circular-text";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add circular-text

Usage

app/index.tsx
import { CircularText } from "@/components/ui/circular-text";
import { Icon } from "@/components/ui/icon";
import { IconStar } from "@/lib/icons/central";
import { View } from "react-native";

export default function Example() {
  return (
    <View className="flex-1 items-center justify-center p-4">
      <CircularText
        centerContent={<Icon icon={IconStar} size="xl" />}
        radius={72}
        repeat="fill"
        spin={{ kind: "loop" }}
      >
        DELACOUR UI
      </CircularText>
    </View>
  );
}

Spanning

span decides how much of the circle the words cover.

Arc
Arc
spanWhat it does
{ kind: "full" }Spreads the text to close the ring exactly, whatever the font's metrics (default)
{ kind: "arc", sweep, startAngle? }Spreads it across sweep degrees, centred on the top unless startAngle says otherwise
{ kind: "natural", startAngle? }Lays it out at the font's own spacing; it may not close the ring
<CircularText radius={64} size="sm" span={{ kind: "arc", sweep: 140 }}>
  Handmade in Wellington
</CircularText>

Direction and side

side points glyph tops away from the centre (outside) or toward it (inside); direction is only the way a loop turns.

Badge
<CircularText radius={52} side="inside">TOP CONTRIBUTOR</CircularText>
<CircularText direction="counterclockwise" radius={52} spin={{ kind: "loop" }}>
  TOP CONTRIBUTOR
</CircularText>

Spinning

spin is none (default), loop at speed degrees per second, or controlled by a SharedValue<number> your app drives from a scroll offset, a gesture or a slider.

Seal
Controlled
<CircularText radius={72} spin={{ kind: "loop", speed: 24 }}>…</CircularText>
<CircularText radius={72} spin={{ kind: "controlled", rotation }}>…</CircularText>

Pausing

press eases the ring to a halt under a finger, controlled from your state; either resumes from the same angle.

Hold to pause
<CircularText pause={{ kind: "press" }} radius={72} spin={{ kind: "loop", speed: 48 }}>
  PRESS AND HOLD
</CircularText>

Content in the middle

centerContent stays upright, still and independently focusable while the ring turns.

<CircularText centerContent={<Avatar name="Aria Whitlock" size="xl" />} radius={52} repeat="fill">
  TOP CONTRIBUTOR
</CircularText>

Repeating a phrase

repeat writes children that many times with separator after each; "fill" estimates enough copies to close the ring.

<CircularText radius={44} repeat="fill" separator=" · " spin={{ kind: "loop", speed: 60 }}>
  UPLOADING
</CircularText>

Recipes

Seal — a filled ring around an icon, turning slowly: repeat="fill", spin={{ kind: "loop" }}, an Icon as centerContent.

Two rings — pass one CircularText as another's centerContent, the inner one smaller, direction="counterclockwise" and faster.

Two rings

Badge — a muted ring around an Avatar, side="inside" for words along the bottom.

Loading ring — a Spinner in the middle and the action's name around it; the ring doubles as the accessible label for the wait.

Reduced motion

Under reduce motion or isMotionCalm, a loop holds still at 0°; a controlled spin still follows its value.

Accessibility

  • A screen reader hears the words once, separators stripped — "DELACOUR • UI • " is read "DELACOUR UI". Pass accessibilityLabel to say something else.
  • Keep ring text at 12pt or more and under about 28 characters per half circle.

SVG text does not grow with the system text size, because radius fixes the circle. Do not put information only in the ring.

API Reference

CircularText

Accepts every View prop except children.

Prop

Type

On this page