Animated accents
StableShiny and gradient text, a beam round a border, a shine border, ripples, lines that type in and a scanning column.
The animated details of the Magic UI and Aceternity style screens, in plain CSS, under the brand’s rule that nothing loops on its own: text shines twice, beams lap once (and again on hover), borders shine once. The two that repeat, the scan and the ripple, are for a strip people can pause and for loading.
Frames of light
import { useState } from 'react'import { Logo } from '@rchursin/brand/Logo'import { Button } from '@rchursin/product/ui'import { BorderBeamTile } from '@rchursin/screens'
/** * Frames of light that run once. A beam laps the border on arrival and again on each hover * (fx-beam, or BorderBeamTile for an app-icon tile); fx-shine-border sends one pass of light * round a card. Replay remounts them. With reduced motion they hold still. */export default function AccentFrames() { const [run, setRun] = useState(0) return ( <div className="flex w-full max-w-2xl flex-col items-center gap-8"> <div key={run} className="flex w-full flex-col items-center gap-6 sm:flex-row sm:items-stretch sm:justify-center"> <BorderBeamTile> <Logo variant="mark" title="" className="size-9 text-fg" /> </BorderBeamTile>
{/* Beam: a p-px wrapper that clips the spinning light; the content sits on top */}Usage
import { BorderBeamTile } from '@rchursin/screens'// The fx-* classes come with the styles: import '@rchursin/product.css'<div className="relative isolate overflow-hidden rounded-2xl bg-fg/10 p-px"> <span aria-hidden="true" className="fx-beam" /> <div className="relative rounded-[15px] bg-raised p-5">…</div></div>Examples
Shiny and gradient text
A small label that shines twice and a heading with one slow pass of grey.
import { useState } from 'react'import { Button } from '@rchursin/product/ui'
/** * fx-shiny-text sends a highlight across the words twice, then rests on a grey that keeps 4.5:1 * contrast for small text. fx-gradient-text makes one slow pass of grey through a heading. */export default function AccentText() { const [run, setRun] = useState(0) return ( <div className="flex w-full max-w-lg flex-col items-center gap-8 text-center"> <div key={run} className="flex flex-col items-center gap-4"> <p className="inline-flex h-7 items-center rounded-full px-3 text-[13px] ring-1 ring-line"> <span className="fx-shiny-text">New: the weekly digest</span> </p> <h3 className="fx-gradient-text text-[34px]/[1.1] font-semibold tracking-[-0.035em] text-balance">Measure every skill your agents use</h3> </div> <Button variant="secondary" size="sm" icon="rotate-ccw" onClick={() => setRun((n) => n + 1)}> Replay </Button> </div> )}Lines that type in
Terminal output, one line every 110ms. With reduced motion the lines are simply there.
import { useState, type CSSProperties } from 'react'import { Button } from '@rchursin/product/ui'
/** * .fx-reveal-line brings lines in one by one, like terminal output: set --i on each line for * its order (110ms apart). It only animates when motion is allowed; otherwise the lines are there. */const output = [ { text: '$ npx skill-scan', dim: false }, { text: 'Reading 1,284 sessions…', dim: true }, { text: '14 skills found', dim: false }, { text: '3 never used · 2 overlap', dim: false }, { text: 'Report saved to skill-report.md', dim: true },]
export default function AccentRevealLines() { const [run, setRun] = useState(0) return ( <div className="flex w-full max-w-md flex-col items-start gap-5"> <figure className="lux-edge w-full rounded-2xl bg-ink p-5 font-mono text-[12.5px]/[1.8] text-paper shadow-lift"> <figcaption className="sr-only">Sample terminal output</figcaption> <div key={run}> {output.map((line, i) => ( <span key={line.text} className={`fx-reveal-line block ${line.dim ? 'text-fog' : ''}`} style={{ '--i': i } as CSSProperties}> {line.text} </span> ))} </div> </figure> <Button variant="secondary" size="sm" icon="rotate-ccw" onClick={() => setRun((n) => n + 1)}> Replay </Button> </div> )}The two that repeat
fx-scan runs only after Play and stops on Pause. fx-ripple shows while something loads, here for three seconds.
import { useEffect, useState, type CSSProperties } from 'react'import { Button } from '@rchursin/product/ui'
/** * The two effects that repeat, and how to keep them in check. fx-scan sweeps a column across a * strip until paused (data-paused on an ancestor), so it gets a Play / Pause button. fx-ripple * is a loading indicator: it runs only while something is loading (here, three seconds). * The strip is 14 columns with 4px gaps, so the column is (100% - 52px) / 14 wide and travels * 13 widths plus 13 gaps. */const days = Array.from({ length: 14 }, (_, i) => i)const rings = [ [220, 0.018, 0.24], [160, 0.03, 0.12], [100, 0.045, 0],] as const
export default function AccentLoops() { const [playing, setPlaying] = useState(false) const [loading, setLoading] = useState(true)API
BorderBeamTile props
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | What sits in the tile, usually the logo mark. |
className | string | 'size-22' | Classes for the outer tile. Passing it replaces the default 88px size, so include a size. |
CSS
| Class or variable | What it does |
|---|---|
fx-shiny-text | A highlight that crosses the text twice (2 × 2.4s), then rests on --shine-base. The default resting grey is #6b6b6b (5.3:1 on white) and #9c9c9c in dark mode (7:1 on #0d0d0d), so small text keeps 4.5:1. --shine-peak sets the highlight. |
fx-gradient-text | One slow 4.5s pass of grey through the text (--g-a, --g-b), then it holds. For large headings: the grey part is about 3.4:1 on white. |
fx-beam | A spinning conic beam (--beam: dark, white in dark mode). Put it in a relative, isolated, overflow-hidden wrapper with p-px, under content that is relative. One 3.5s lap on mount; hovering the wrapper with a mouse sends it round again. |
fx-beam-light | Forces a white beam, for dark buttons and panels in light mode. |
fx-shine-border | Wrap a card: its 1px padding becomes the border (--sb-base) and one 5s pass of light runs round it. Give the inner element a radius 1px smaller. |
fx-ripple | One ring of a loading ripple, centred absolutely. Set width, height, --rp-a and animation-delay per ring. It pulses until removed, so render it only while loading. |
fx-reveal-line | A line that fades and rises 4px into place, --i × 110ms + 250ms after mount. Motion allowed only; data-paused on an ancestor pauses it. |
fx-scan | A column that sweeps across its track on a 6s loop, to translateX(--scan-to). Motion allowed only. data-paused on an ancestor pauses it: always give people a button for that. |
Accessibility
- Beams, rings and scans are aria-hidden spans. The text effects keep the real text in the page, so screen readers read it as usual.
- With reduced motion all of them hold still: the text rests, beams and borders don’t run, ripples and scans stop and lines are simply there.
- Anything that moves for more than five seconds alongside other content needs a way to pause it (WCAG 2.2.2). That is why fx-scan has data-paused and fx-ripple is for loading only.
- Shiny and gradient text paint the letters with a background, so check their resting greys against the background, as noted above.
Guidelines
Do
- Use one accent per view, on the thing you want seen first.
- If you change --shine-base, keep it at 4.5:1 against the background for small text.
- Show fx-ripple only while something is loading.
Don’t
- Don’t loop fx-scan without a pause button.
- Don’t use gradient text for body copy or anything under 24px.
- Don’t put a beam and a shine border on the same card.
In use
Used on this site
Related
- Effect buttonsA shimmer button, a shiny announcement pill and a glowing border, from the launch-page screens.
- BackgroundsDot and grid patterns, masks and vignettes to fade them, a retro floor grid, a spotlight, a lamp and a softly lit card.
- TerminalA dark terminal window with sample scan output: usage bars, a context cost and a verdict per item, printed line by line.
- Run wallA month of agent runs as a wall of squares, with a sweeping day highlight you can pause.
- Magic UI screensFive screens with Magic UI’s effects: retro grid, border beam, shiny and gradient text, shimmer buttons, ripples and a dock.
Source
The component’s own files, exactly as they run on this site.
/* Effects behind the Magic UI- and Aceternity-style screens, written in plain CSS (no animation library). Light values are the default; anything inside a `.dark` ancestor switches to the dark values. All motion stops for people who ask for reduced motion. Everything sits in @layer components, so Tailwind utilities on the same element win (e.g. `fx-dots [background-size:20px_20px]`).
Ported from the RChursin_ UI kit (rchursin/styles/effects.css). The word field at the end is a site addition (not in the kit), so `kit.ts diff` reports that block as the only drift.*/
@layer components { @keyframes rc-spin { to { transform: translate(-50%, -50%) rotate(360deg); } } @keyframes rc-shine { from {