Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Text

Pro

Iris Text for React

Text that opens like a camera's iris, each letter seen through a circle that grows from its centre. For React with Base UI, when a title should be revealed rather than moved.

Framework

Iris Text reveals a line a letter at a time, each letter seen through a circle that opens from its centre, like a camera's aperture, until the whole of it shows. The letters never move: they're in place from the first frame, and only what you see of them grows. It plays once, when the text scrolls into view.

Use it for a heading, a product name or a title card with a precise, photographic feel. For letters that slide into place, use Splitting Text; for letters that come into focus from a blur, Blur Text; for letters that unfold, Paper Fold Text. Each letter is clipped by a circle that a CSS keyframe animation opens, delayed by its place in the line, with no animation library; once a letter is open the clip is gone. With reduced motion the text is simply there.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Usage

import { IrisText } from "@/components/ui/iris-text";
<h2>
  <IrisText text="Picture perfect" />
</h2>

Demos

Words

by="words" opens each word through one circle, 180 ms apart: for a longer line, and for scripts whose letters join, such as Arabic, which a split between letters would break.

Preview width

Timing

duration is how long one letter takes to open, 600 ms by default, and stagger the time between one letter and the next, 60 ms. Short ones suit a small label; long ones, a title revealed slowly.

Preview width

Accessibility

Screen readers read the whole text once, as plain text: the opening letters are hidden from them, and a visually hidden copy carries it. Words wrap whole, and the line keeps its direction on a right-to-left page. With reduced motion every letter is open from the start.

FAQ

When does the animation play?

Once, when at least a quarter of the text is in view: an IntersectionObserver on the text starts it, then stops watching. Pass trigger="mount" to play it as soon as the page shows it, for text that's always on screen.

Does the circle cut off parts of letters?

No. It opens to just past the corners of each letter's box, then the clip is removed altogether, so accents, descenders and the overhang of an italic show whole once a letter is open.

Does the line move while it opens?

No. Each letter is in its place from the start and only its clip changes, so nothing around the text shifts.

What if JavaScript is off or slow to load?

With trigger="view", the server renders the letters closed, waiting, and a page without JavaScript shows the text at once. With trigger="mount" the animation runs from CSS alone, before the page's script loads.

How do I run something after it finishes?

Pass onComplete: it's called once, when the last letter or word is open.

What happens with reduced motion?

The circles don't open: the text is there from the start, fully readable.

API Reference

IrisText

<span> data-slot="iris-text"

PropType
textstring

The text.

by?"characters" | "words"

What opens on its own: each character, or each word (for scripts whose letters join, such as Arabic).

Type
"characters" | "words"
Default
"characters"
duration?number

How long one character or word takes to open, in milliseconds. @default 600.

onComplete?() → void

Called once the last character or word is open.

Returns
void
stagger?number

The time between one character or word and the next, in milliseconds.

Default
60 (characters), 180 (words)
trigger?"mount" | "view"

When it plays: as soon as it's on the page, or once it scrolls into view.

Type
"mount" | "view"
Default
"view"
render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Merged with the part's classes.

AttributeType
data-last?string
data-view?string

"in" when visible is true, otherwise "out".

CSS variableType
--iris-text-durationstring

One unit's opening.

All 97 text effects