shadimage
whiskeyjack
uibeamsource: ok · hit

Beam

A travelling light around an element's edge, for the one thing on a screen that needs the eye.

npx shadcn add "https://whiskeyjack.net/r/beam.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light

Registry dependencies

https://whiskeyjack.net/r/utils.json

Files1 · source cached 10/9/2026, 11:29:37 AM

components/ui/beam.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

export interface BeamProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Runs the beam. Off, the wrapper is inert and costs nothing visually. */
  active?: boolean;
  /**
   * Which urgency the light carries. `accent` is a nudge, `warning` a
   * deadline approaching, `error` one about to pass.
   */
  tone?: "accent" | "warning" | "error";
  /**
   * Seconds for one full turn. Slower reads as calmer; below ~2s it starts to
   * feel like a loading spinner rather than a highlight.
   */
  durationSec?: number;
  children: React.ReactNode;
}

/**
 * Draws a travelling light around whatever it wraps, for the one thing on a
 * screen that needs the eye. Built for a `Card` -- a goal about to break its
 * streak, a check-in whose window is closing -- but it wraps anything with a
 * border radius.
 *
 * ```tsx
 * <Beam active={goal.atRisk} tone="warning" className="rounded-xl">
 *   <Card>...</Card>
 * </Beam>
 * ```
 *
 * **Match the radius.** The layers inherit `border-radius` from this wrapper,
 * which defaults to `rounded-xl` (a `Card`'s). Pass a `className` with the
 * child's radius when it differs, or the light traces a shape the content does
 * not have.
 *
 * **The beam carries no meaning on its own.** It is decoration drawn in
 * pseudo-elements, invisible to assistive tech, and tone is colour alone --
 * so whatever makes this element urgent has to be in its text too. Reduced
 * motion holds the light still rather than removing it, which keeps the cue
 * for anyone who cannot use the movement.
 *
 * **One at a time.** Two of these on a screen compete, and the effect is worth
 * nothing once it is not the only one.
 */
const Beam = React.forwardRef<HTMLDivElement, BeamProps>(
  (
    { active = false, tone = "accent", durationSec, className, style, children, ...props },
    ref,
  ) => (
    <div
      ref={ref}
      className={cn("wj-beam rounded-xl", className)}
      data-active={active}
      data-tone={tone}
      style={
        durationSec === undefined
          ? style
          : ({ "--wj-beam-duration": `${durationSec}s`, ...style } as React.CSSProperties)
      }
      {...props}
    >
      {children}
    </div>
  ),
);
Beam.displayName = "Beam";

export { Beam };

More from whiskeyjack

Browse all components in this library →