Animated Border
Drop one inside a card, a button or any positioned box and a light travels its
edge. It is decoration and nothing else: it renders an aria-hidden overlay
with no text, no role and no focus target. Use it on the one card that should
catch the eye — the recommended plan, the panel that is generating something,
the upgrade tile. If the motion is meant to report a wait, the component
that says so is Spinner or
Shimmer, and this goes beside it rather than instead
of it.
The ring is a masked overlay rather than a real border, because a border can
only be a flat color per edge — there is no way to run one gradient around
four of them, and no way to move it. It takes its corner radius from the host
through border-radius: inherit, so a data-forte-radius preset carries
without being told.
Import
import { AnimatedBorder } from "@forte-ui/react";The host has to be positioned
The overlay positions itself against the nearest positioned ancestor.
Card.Root and Button already are, so the example above needs nothing. Any
other host needs position: relative — Tailwind's relative:
<div className="relative rounded-surface border p-6">
<AnimatedBorder />
</div>Examples
Variants
Three lights, one ring. beam rides offset-path, so its speed is constant
in distance along the outline — it is the only one whose pace does not
depend on the shape of the box. shine drifts a soft gradient through the
ring with no leading edge, which reads as sheen rather than as an object.
rotate is a conic sweep about the center, so it turns at constant angular
speed and visibly accelerates into the corners of anything far from square —
a look in its own right on a card, and the wrong one on a wide banner.
Tones
Colors come from the palette, never from a hex prop, so a border re-colors
with the seed like everything else. primary and secondary span the
library's two seeds — that pair is what makes them read as a gradient rather
than as a sheen, and it is the one place both seeds appear at once. The rest
are a single hue with a lighter end derived from it.
There is a fifth, current, which takes both stops from currentColor. It is
the composable one: dropped inside a button or a badge it picks up that
thing's text color and keeps matching it.
Sitting on the host's own border
An overlay's inset resolves against the host's padding box, so by default
the ring lands just inside whatever border the host draws — next to a Card's
1px hairline, the two read as a doubled edge. Pull the ring outward with
--forte-animated-border-inset, or paint the host's border away and let the
ring be the edge.
Two lights on one ring
Each AnimatedBorder is its own overlay, so a second one needs nothing
special. A negative --forte-animated-border-delay starts it already part
way round instead of making it wait for the first lap.
Theming
Reduced motion
Nothing travels. The moving light fades out and the ring becomes a still
gradient in the same colors, so the host keeps the edge it had — it does not
change shape, and it does not pulse. A border carries no information, so
breathing at the reader would spend attention and return none. The animations
are also genuinely paused rather than merely hidden, through
--forte-motion-play, so a faded-out ornament is not still repainting a
gradient every frame for someone who asked for less of that.
Under forced colors the overlay is removed outright. There is nothing to
preserve — the component states no information, and a CanvasText ring over
the host's own CanvasText border is a doubled edge saying nothing twice.
API
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
reverse | boolean | false | Travel the ring the other way round. Unlike `Shimmer`, this does *not* follow the reading direction, and the omission is deliberate: `--forte-direction` exists so that motion with a logical start and end stays correct in RTL, and a light going round and round has neither. Flipping it in RTL would spin the decoration on half the world's pages for no reason a reader could name. |
tone | AnimatedBorderTone | primary | Which colors the light is made of. `primary` and `secondary` span the library's two seeds, which is what makes them read as a gradient rather than as a sheen; the rest are one hue with a lighter end derived from it. `current` is the composable one — it picks up whatever text color it lands in. |
variant | AnimatedBorderVariant | beam | Which light travels the ring. - `beam` — one glow riding the outline at constant speed, rounding the corners with it. The only one of the three whose pace does not depend on the shape of the box, and so the right pick for anything far from square. - `shine` — a soft glow drifting diagonally across the ring. No leading edge; reads as sheen rather than as a moving object. - `rotate` — a conic sweep pivoting about the center. Constant *angular* speed, so on a box far from square the highlight visibly accelerates into the corners — a look in its own right on a card, and the wrong one on a wide banner. |
Theming
| Property | Controls | Default |
|---|---|---|
--forte-animated-border-width | Thickness of the ring. This is the mask's padding, so it is the border width in the literal sense — there is no border here to disagree with it. | 2px |
--forte-animated-border-inset | How far the ring sits inside the host's padding box. Negative pulls it outward, onto the host's own border. | 0px |
--forte-animated-border-duration | One full lap. Speed is this one knob: shorter is faster. | var(--forte-duration-loop-orbit) |
--forte-animated-border-delay | Offset into the lap before the light starts. | 0s |
--forte-animated-border-color | Color at the light's core. Set by tone. | var(--forte-color-primary) |
--forte-animated-border-color-end | Color it fades out through, on its way to transparent. Set by tone. | var(--forte-color-secondary) |
--forte-animated-border-beam-size | Diameter of the traveling glow, and so the beam's visible length. | 6rem |
--forte-animated-border-shine-size | Size of the shine's gradient tile, relative to the host. | 300% |