Rotating Text
TextCycles through a list of words in place.
A first screen in type as large as it will go: the name or the claim in a few lines that rise once, one after another, from masks of their own (SplitText by line). A line of roles can lead them, its word turning once through the list and settling within five seconds (RotatingText, fluid, with cycles), as in "I design / build / write". Under the type, an optional reel: a muted loop that plays while it is on screen, opens from a rounded inset to the full width as the page scrolls (ScrollExpand), and has a visible play and pause button. Zero JavaScript without a reel.
Sample Studio, a design studio in Lisbon
Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.
Variants · each row writes the attribute the prop writes; the section's own CSS answers
JavaScript of its own
0 B
Uses Reveal and the shared runtime (1.9 kB raw, once per site). With those included: 3.0 kB raw.
With video: 578 B raw of its own on Reveal and the runtime, measured on its own fixture. The default above ships none of it.
CSS 22.7 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 115 · not Firefox · Safari 26; needs scroll-timeline, property. Elsewhere: without scroll timelines (Firefox) the reel is full width from the start; the lines still rise and the word still turns.
The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:
npx shadcn@latest add @moonarc-pro/hero-kinetic---
import HeroKinetic from '@/components/moonarc/pro/HeroKinetic.astro';
---
<!-- a studio: three lines, and the reel under them -->
<HeroKinetic
eyebrow="Northfold, a design studio in Rotterdam"
headline={'Identity, web\nand motion for\nnew companies.'}
facts={['Est. 2014', '14 people', 'Rotterdam and Jakarta']}
video={{ src: ['/video/reel.webm', '/video/reel.mp4'], poster: '/video/reel.jpg', label: 'The studio\'s reel: posters, packaging and websites from four projects', caption: 'Reel 2026' }}
/>
<!-- one person: the role turns once and settles on the first -->
<HeroKinetic
eyebrow="Ines Vallory"
roles={{ before: 'I', words: ['design', 'build', 'write'], after: 'websites' }}
headline={'for companies with\nsomething to show.'}
/>Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: split-text, rotating-text, reveal, scroll-expand, animated-icons.
| Prop | Type | Default | Description |
|---|---|---|---|
headline | string | none | The big type, one line per \n; each line rises from its own mask, 110 ms after the one before. The headings' text is the whole string (SplitText keeps one copy for screen readers). |
roles | { before?: string; words: string[]; after?: string; position?: 'start' | 'end' } | none | A line of roles before the headline (start, default) or after it: before, then a word turning through words, then after. The word turns once and settles on the first, the round inside 4.5 s whatever the number of words (WCAG 2.2.2), in the theme's primary colour; a screen reader hears the words as a list. |
eyebrow | string | none | A short line over the type, in mono. Slot "eyebrow" wins. |
copy | string | none | A paragraph under the type. Slot "copy" wins. |
facts | string[] | [] | Short facts in a mono row under the type: a city, a year, what the studio does. |
level | 1 | 2 | 1 | Heading level: 1 on the page's own hero, 2 under another h1. |
video | { src: string | string[]; poster: string; label: string; caption?: string } | none | The reel under the type: webm first, then mp4, silent (it plays muted). label is what it shows, for a reader who cannot see it; caption a line beside the button. It plays while a quarter of it is on screen and pauses off screen; the button pauses it for good, until the reader plays it again. Without JavaScript the video's own controls are shown instead. |
playLabel | string | 'Play the reel' | Accessible name of the reel's button; aria-pressed says whether the reel plays. |
expand | number | 0.72 | Starting width of the reel as a share of the full width, 0.2–1; it opens to the full width as the page scrolls. |
tone | 'surface' | 'muted' | 'ink' | 'brand' | unset (paints nothing) | The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. The turning word is the theme's primary on an unset, surface or muted ground, and the text colour on ink and brand. |
density | 'compact' | 'cozy' | 'roomy' | 'cozy' | Block padding ×0.8 / ×1 / ×1.25 through --ma-density. |
align | 'start' | 'center' | 'start' | The type on the inline start or centred. |
The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.
.ma-in.ma-kinetic.ma-kinetic__actions.ma-kinetic__bar.ma-kinetic__caption.ma-kinetic__copy.ma-kinetic__eyebrow.ma-kinetic__facts.ma-kinetic__headline.ma-kinetic__inner.ma-kinetic__lines.ma-kinetic__mask.ma-kinetic__reel.ma-kinetic__roles.ma-kinetic__toggle.ma-kinetic__video.ma-kinetic__word.ma-split__mask
slot="eyebrow"slot="copy"slot="actions"
The lines and the roles are in place from the start (they fade in, without travel), the word stays on the first, the reel is full width and nothing plays by itself: the button is the only way to start it.
ClientRouterThe reel's script binds through the shared runtime and pauses the loop before the swap; the lines and the word are CSS and play again on the next page's own hero.
Replaces: Studio heroes (Framer templates) · TextReveal hero (Magic UI) · Showreel heroes (Awwwards studio sites). See the migration table.
Cycles through a list of words in place.
An image that starts narrow and rounded and opens to the full width as it scrolls into view.
Splits a string into words, characters or lines on the server and reveals them with a capped stagger when they scroll into view.
A split hero: a tall headline revealed line by line on the left, a live panel sliding in from the right, on a slowly drifting grid.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown