Wraps any content in a border drawn by a rotating conic gradient in theme colors, with a soft outer glow. Speed, width, radius and arc length are props; it stops at a still angle under reduced motion.
Pro
Most popular$24/ month
For teams shipping weekly
- Unlimited projects
- Branch previews
- 90-day logs
- Priority support
import { Check } from "lucide-react"
import { buttonVariants } from "@/components/ballmac/button"
import { GlowBorder } from "@/components/ballmac/glow-border"
const plans = [
{ name: "Starter", price: "$0", note: "For side projects", features: ["3 projects", "Community support", "7-day logs"] },
{
name: "Pro",
price: "$24",
note: "For teams shipping weekly",
features: ["Unlimited projects", "Branch previews", "90-day logs", "Priority support"],
featured: true,
},
]
function Plan({ plan }: { plan: (typeof plans)[number] }) {
return (
<div className="flex h-full flex-col p-6">
<div className="flex items-center justify-between">
<p className="text-sm font-medium">{plan.name}</p>
{plan.featured && (
<span className="rounded-full bg-[linear-gradient(90deg,var(--chart-1),var(--chart-4))] px-2.5 py-0.5 text-[11px] font-medium text-white">
Most popular
</span>
)}
</div>
<p className="mt-3 flex items-baseline gap-1">
<span className="text-4xl font-semibold tracking-tight tabular-nums">{plan.price}</span>
<span className="text-sm text-muted-foreground">/ month</span>
</p>
<p className="mt-1 text-sm text-muted-foreground">{plan.note}</p>
<ul className="mt-5 flex-1 space-y-2.5 text-sm">
{plan.features.map((f) => (
<li key={f} className="flex items-center gap-2">
<Check className={`size-4 ${plan.featured ? "text-chart-1" : "text-muted-foreground"}`} aria-hidden="true" />
{f}
</li>
))}
</ul>
<a href="#" className={buttonVariants({ variant: plan.featured ? "default" : "outline", className: "mt-6 w-full" })}>
{plan.featured ? "Upgrade to Pro" : "Get started"}
</a>
</div>
)
}
export default function GlowBorderDemo() {
return (
<div className="grid w-full max-w-2xl gap-5 sm:grid-cols-2">
<div className="rounded-[14px] border bg-card sm:my-4">
<Plan plan={plans[0]} />
</div>
<GlowBorder duration={5} width={1.5} radius={14}>
<Plan plan={plans[1]} />
</GlowBorder>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/glow-borderInstall the dependencies.
$ pnpm add motion@^12Copy the source into your project.
components/ballmac/glow-border.tsx// Ballmac UI: Glow Border. https://ui.ballmac.com/components/glow-border "use client" import * as React from "react" import { useReducedMotion } from "motion/react" import { cn } from "@/lib/utils" type GlowBorderProps = React.ComponentProps<"div"> & { /** Seconds for one full rotation of the gradient. */ duration?: number /** Border thickness in pixels. */ width?: number /** Outer corner radius in pixels; the content's radius is reduced by `width` so the ring stays even. */ radius?: number /** Colors of the moving arc, as CSS colors or variables. The arc fades out before the first color repeats. */ colors?: string[] /** How much of the ring the arc covers, 0 to 1. Use 1 for a continuous gradient ring. */ arc?: number /** Strength of the soft outer glow, 0 to 1. */ glow?: number /** Class names for the inner content surface. */ contentClassName?: string } const ANGLE = "--glow-border-angle" let registered = false /** Registers the angle as a typed property so browsers can interpolate it; returns false if unsupported. */ function registerAngle() { if (registered) return true if (typeof CSS === "undefined" || !("registerProperty" in CSS)) return false try { CSS.registerProperty({ name: ANGLE, syntax: "<angle>", inherits: true, initialValue: "0deg" }) } catch { // Already registered (hot reload, a second copy of this file): still usable. } registered = true return true } function gradientFor(colors: string[], arc: number) { if (arc >= 1) return `conic-gradient(from var(${ANGLE}), ${[...colors, colors[0]].join(", ")})` const span = Math.max(0.05, arc) * 100 const step = span / (colors.length + 1) const stops = colors.map((c, i) => `${c} ${(step * (i + 1)).toFixed(2)}%`) return `conic-gradient(from var(${ANGLE}), transparent 0%, ${stops.join(", ")}, transparent ${span.toFixed(2)}%, transparent 100%)` } function GlowBorder({ duration = 4, width = 1.5, radius = 14, colors = ["var(--chart-1)", "var(--chart-4)", "var(--chart-5)", "var(--chart-2)"], arc = 0.55, glow = 0.5, contentClassName, className, style, children, ...props }: GlowBorderProps) { const rootRef = React.useRef<HTMLDivElement>(null) const reduceMotion = useReducedMotion() React.useEffect(() => { const root = rootRef.current if (!root || reduceMotion || !registerAngle() || typeof root.animate !== "function") return const animation = root.animate([{ [ANGLE]: "0deg" }, { [ANGLE]: "360deg" }], { duration: duration * 1000, iterations: Infinity, easing: "linear", }) // Pause while off-screen; browsers already throttle hidden tabs. const observer = new IntersectionObserver(([entry]) => { if (entry?.isIntersecting) animation.play() else animation.pause() }) observer.observe(root) return () => { observer.disconnect() animation.cancel() } }, [duration, reduceMotion]) const background = gradientFor(colors, arc) return ( <div ref={rootRef} data-slot="glow-border" className={cn("relative isolate", className)} style={ { borderRadius: radius, padding: width, // Static angle under reduced motion: the arc rests across the top-right corner. [ANGLE]: "40deg", ...style, } as React.CSSProperties } {...props} > {/* Soft glow behind the ring. */} {glow > 0 && ( <span aria-hidden="true" data-slot="glow-border-glow" className="pointer-events-none absolute inset-0 -z-10 rounded-[inherit] blur-md" style={{ background, opacity: glow }} /> )} {/* The ring: the gradient shows only in the padding around the content. */} <span aria-hidden="true" data-slot="glow-border-ring" className="pointer-events-none absolute inset-0 -z-10 rounded-[inherit] bg-border" > <span className="absolute inset-0 rounded-[inherit]" style={{ background }} /> </span> <div data-slot="glow-border-content" className={cn("relative h-full rounded-[inherit] bg-card text-card-foreground", contentClassName)} style={{ borderRadius: Math.max(0, radius - width) }} > {children} </div> </div> ) } export { GlowBorder, type GlowBorderProps }Update the import paths to match your project setup.
Usage
import { GlowBorder } from "@/components/ballmac/glow-border"The full example is in the Code tab above.
Examples
Buttons
import { ArrowRight, Sparkles } from "lucide-react"
import { GlowBorder } from "@/components/ballmac/glow-border"
export default function GlowBorderButton() {
return (
<div className="flex flex-wrap items-center justify-center gap-4">
<GlowBorder radius={999} width={1.5} duration={3} arc={1} glow={0.7} className="inline-flex" contentClassName="bg-background">
<button
type="button"
className="inline-flex h-11 items-center gap-2 rounded-full px-6 text-sm font-medium outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Sparkles className="size-4 text-chart-4" aria-hidden="true" />
Ask the assistant
</button>
</GlowBorder>
<GlowBorder radius={10} width={1} duration={4} arc={0.35} glow={0.4} className="inline-flex" contentClassName="bg-primary text-primary-foreground">
<a
href="#"
className="inline-flex h-9 items-center gap-1.5 rounded-[9px] px-4 text-sm font-medium outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
Start free trial
<ArrowRight className="size-4 rtl:rotate-180" aria-hidden="true" />
</a>
</GlowBorder>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
durationSeconds for one full rotation of the gradient. | number | 4 |
widthBorder thickness in pixels. | number | 1.5 |
radiusOuter corner radius in pixels; the content's radius is reduced by `width` so the ring stays even. | number | 14 |
colorsColors of the moving arc, as CSS colors or variables. The arc fades out before the first color repeats. | string[] | ["var(--chart-1)", "var(--chart-4)", "var(--chart-5)", "var(--chart-2)"] |
arcHow much of the ring the arc covers, 0 to 1. Use 1 for a continuous gradient ring. | number | 0.55 |
glowStrength of the soft outer glow, 0 to 1. | number | 0.5 |
contentClassNameClass names for the inner content surface. | string | — |
Also accepts the standard attributes of its root element.
Use with AI
Put content inside <GlowBorder>; it renders a padded wrapper whose padding shows a rotating conic gradient and a content surface (bg-card, radius minus width). Use arc={1} for a full rainbow ring, a smaller arc for a comet. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Glow Border (@ballmac/glow-border) to this project with the shadcn MCP, then use it where it fits.
Use it for
- The recommended plan in a pricing table
- One primary call-to-action or an AI entry point
- Marking a card as new, live or selected
Not for
- Several elements at once on the same screen (use it for one focal point)
- A thin light that travels along an existing border (use border-beam)
Registry JSON: https://ui.ballmac.com/r/glow-border.json
Credits
Free to use in personal and commercial projects.
- npm
- motion@^12
- Registry
- shadcn/utils
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Border Beam
A short beam of light that travels around the inside edge of its parent's border at a constant speed. Decorative, token-colored, hidden under reduced motion.
Tilt Card
A card that tilts in 3D toward the pointer with spring smoothing and a moving glare. TiltCardLayer children float at their own depth for parallax. Keyboard focus shows a gentle tilt.
Animated Beam
An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.