Skip to content
Andrew SmithSolutions
← Component library

Accent beam

Motion

A light travelling around a card's border — a conic gradient rotated with @property, on one composited layer.

@propertyConic gradientCompositorCSS only — no dependency

Preview

Now booking

Two engagements open for the quarter.

The beam is decoration. The words are what tell you the state.

Controls

Notes

The rotation animates a registered @property angle, so it interpolates on the compositor. Animating a background-image string instead forces a repaint every frame and drops the card to single digits on a laptop.

The beam is a ::before pseudo-element, so it never enters the accessibility tree at all. It carries no meaning and should never be the only thing telling someone a card is selected.

Honours prefers-reduced-motion by holding the gradient still rather than removing it, so the card keeps its edge treatment either way.

Source

Show
import { cn } from "@/lib/cn";

/**
 * A light travelling around the border.
 *
 * The rotation animates a REGISTERED @property angle, which is what makes this
 * cheap: the browser can interpolate a typed <angle> on the compositor.
 * Animating the background-image string instead re-parses and repaints the
 * gradient every frame, and drops a page of these into single digits.
 *
 * The beam is a ::before pseudo-element, so it never enters the accessibility
 * tree at all. It is decoration and must never be the only thing saying a card
 * is selected, live or in error.
 */
const css = `
@property --beam-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
@keyframes beam-spin {
  to { --beam-angle: 360deg; }
}
.beam { position: relative; isolation: isolate; }
.beam::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: var(--beam-width, 1.5px);
  background: conic-gradient(
    from var(--beam-angle),
    transparent 0turn,
    var(--beam-color, var(--accent)) 0.1turn,
    transparent 0.26turn
  );
  /* Paint the padding ring only: two masks, minus the content box. */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: beam-spin var(--beam-duration, 6s) linear infinite;
  pointer-events: none;
}
/* Hold the gradient still rather than removing it, so the card keeps its
   edge treatment either way. */
@media (prefers-reduced-motion: reduce) {
  .beam::before { animation: none; }
}
`;

const speeds = { slow: "11s", medium: "6s", fast: "3s" } as const;
const widths = { hairline: "1px", fine: "1.5px", bold: "3px" } as const;
const tones = { accent: "var(--accent)", ink: "var(--ink)" } as const;

export type BeamSpeed = keyof typeof speeds;
export type BeamThickness = keyof typeof widths;
export type BeamTone = keyof typeof tones;

export function Beam({
  speed = "medium",
  thickness = "fine",
  tone = "accent",
  className,
  children,
}: {
  speed?: BeamSpeed;
  thickness?: BeamThickness;
  tone?: BeamTone;
  className?: string;
  children: React.ReactNode;
}) {
  return (
    <>
      <style href="beam-spin" precedence="default">
        {css}
      </style>
      <div
        className={cn("beam rounded-xl", className)}
        style={
          {
            "--beam-duration": speeds[speed],
            "--beam-width": widths[thickness],
            "--beam-color": tones[tone],
          } as React.CSSProperties
        }
      >
        {children}
      </div>
    </>
  );
}

export default function Demo({
  speed = "medium",
  thickness = "fine",
  tone = "accent",
}: {
  speed?: BeamSpeed;
  thickness?: BeamThickness;
  tone?: BeamTone;
}) {
  return (
    <Beam speed={speed} thickness={thickness} tone={tone} className="w-full max-w-sm">
      <div className="rounded-xl bg-paper-raised p-7">
        <p className="eyebrow">Now booking</p>
        <p className="mt-4 text-lg text-ink">Two engagements open for the quarter.</p>
        <p className="mt-3 text-[15px] leading-relaxed text-ink-muted">
          The beam is decoration. The words are what tell you the state.
        </p>
      </div>
    </Beam>
  );
}

Next

Button

Next step

Tell me where the business is losing time.

No pitch deck and no discovery-call funnel. Describe the bottleneck and I will tell you plainly whether it is worth building something for, and roughly what that would take.