Skip to content

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.

animated-border/basic.tsx
Pro
Everything in Starter, plus unlimited projects.

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.

animated-border/variants.tsx
beam
One glow, constant speed.
shine
A drifting sheen, no leading edge.
rotate
A conic sweep about the center.

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.

animated-border/tones.tsx
primary
secondary
danger
neutral

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.

animated-border/on-the-border.tsx
default
Ring inside the hairline.
inset -1px
Pulled onto the hairline.
border removed
The ring is 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.

animated-border/stacked.tsx
two, half a lap apart
two, counter-rotating

Theming

animated-border/theming.tsx
width 4px
duration 2s
beam-size 3rem

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

Props for AnimatedBorder
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
reversebooleanfalseTravel 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.
toneAnimatedBorderToneprimaryWhich 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.
variantAnimatedBorderVariantbeamWhich 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

Theming tokens for AnimatedBorder
PropertyControlsDefault
--forte-animated-border-widthThickness 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-insetHow far the ring sits inside the host's padding box. Negative pulls it outward, onto the host's own border.0px
--forte-animated-border-durationOne full lap. Speed is this one knob: shorter is faster.var(--forte-duration-loop-orbit)
--forte-animated-border-delayOffset into the lap before the light starts.0s
--forte-animated-border-colorColor at the light's core. Set by tone.var(--forte-color-primary)
--forte-animated-border-color-endColor it fades out through, on its way to transparent. Set by tone.var(--forte-color-secondary)
--forte-animated-border-beam-sizeDiameter of the traveling glow, and so the beam's visible length.6rem
--forte-animated-border-shine-sizeSize of the shine's gradient tile, relative to the host.300%