Wrapper that rims its content with a currentColor glow inside and out, in dim, steady or breathing states. Can also cover the viewport as a presence overlay; breathing holds steady under reduced motion.
import * as React from "react"
import { cn } from "cn"
// A rim of light around whatever it wraps, colored by currentColor. Also a
// full-viewport presence wash when `viewport` is set.
type RimGlowState = "dim" | "steady" | "breathing"
const STATE_OPACITY: Record<RimGlowState, number> = {
dim: 0.55,
steady: 1,
breathing: 1,
}
// breathing drives its own opacity from the stylesheet, so the reduced-motion
// branch can substitute a steady value instead of collapsing into steady.
const BREATHE_CSS = `@keyframes rg-breathe{0%,100%{opacity:1}50%{opacity:0.7}}.rg-breathe{animation:rg-breathe 4.4s ease-in-out infinite}@media (prefers-reduced-motion: reduce){.rg-breathe{animation:none;opacity:0.8}}`
function mix(pct: number): string {
const p = Math.round(Math.max(0, Math.min(100, pct)))
return `color-mix(in oklab, currentColor ${p}%, transparent)`
}
/**
* Wraps its children in a rim of light in the text color: a hairline with
* glows inside and outside the edge. Set a `text-*` class for the color and
* a `rounded-*` class for the corners; the rim follows both.
*
* `state="breathing"` pulses slowly and holds still at 80% under reduced
* motion. `viewport` instead draws only the inner glow as a fixed
* full-screen overlay that takes no pointer events, beside the children.
*/
function RimGlow({
state = "steady",
intensity = 1,
spread = 24,
viewport = false,
className,
children,
...props
}: React.ComponentProps<"div"> & {
/** dim = lowered · steady = full · breathing = a slow 4.4s pulse */
state?: RimGlowState
/** 0–1.5 — multiplies every glow layer's alpha */
intensity?: number
/** Glow reach in px — how far the rim bleeds inward/outward */
spread?: number
/**
* Draws the glow as a fixed full-screen overlay that takes no pointer
* events. Children render in place, outside it, and `className` and other
* props go on the overlay.
*/
viewport?: boolean
}) {
// A negative or non-finite blur invalidates the whole box-shadow.
const reach = Number.isFinite(spread) ? Math.max(0, spread) : 24
const boxShadow = [
`inset 0 0 0 1px ${mix(85 * intensity)}`,
`inset 0 0 ${Math.round(reach * 0.5)}px ${mix(45 * intensity)}`,
`inset 0 0 ${Math.round(reach * 1.6)}px ${mix(18 * intensity)}`,
...(viewport
? []
: [
`0 0 ${Math.round(reach * 0.75)}px ${mix(30 * intensity)}`,
`0 0 ${Math.round(reach * 2)}px ${mix(12 * intensity)}`,
]),
].join(", ")
const overlay = (
<span
aria-hidden="true"
className={cn(
"pointer-events-none rounded-[inherit]",
viewport ? "fixed inset-0" : "absolute inset-0",
state === "breathing" && "rg-breathe"
)}
style={
state === "breathing"
? { boxShadow }
: { boxShadow, opacity: STATE_OPACITY[state] }
}
/>
)
const breathe = state === "breathing" && (
// href + precedence so React hoists one copy, not one per instance
<style href="rim-glow-breathe" precedence="default">
{BREATHE_CSS}
</style>
)
// The viewport glow sits beside the children, not around them, so they
// keep their place in the page and their pointer events.
if (viewport)
return (
<>
{children}
<div
data-slot="rim-glow"
data-state={state}
className={cn("pointer-events-none fixed inset-0 z-50", className)}
{...props}
>
{breathe}
{overlay}
</div>
</>
)
return (
<div
data-slot="rim-glow"
data-state={state}
className={cn("relative", className)}
{...props}
>
{breathe}
{overlay}
{children}
</div>
)
}
export { RimGlow, type RimGlowState }
pnpm dlx shadcn add @01orbital/rim-glownpx shadcn add @01orbital/rim-glowyarn shadcn add @01orbital/rim-glowbunx --bun shadcn add @01orbital/rim-glowFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/glow/rim-glow.tsx, as source you own and edit.
The example also uses card and item. Install them too:
pnpm dlx shadcn add @01orbital/card @01orbital/itemnpx shadcn add @01orbital/card @01orbital/itemyarn shadcn add @01orbital/card @01orbital/itembunx --bun shadcn add @01orbital/card @01orbital/itemimport { cn } from "cn"
import { RimGlow } from "@/components/glow/rim-glow"
import { Card, CardContent } from "@/components/ui/card"
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"
// Every word on the page. Change copy here, not in the markup.
const COPY = {
title: "Assistant",
body: "Reading three files and drafting the migration plan…",
} as const
export default function RimGlowDemo({
animate = false,
}: {
// Breathe the rim; the card's play button turns it on.
animate?: boolean
}) {
return (
<div className="dark relative isolate grid min-h-full w-full flex-1 place-items-center overflow-hidden bg-background text-foreground">
{/* The rim takes the card's radius and glows in the current color. */}
<RimGlow
state={animate ? "breathing" : "steady"}
spread={28}
className="w-full max-w-md rounded-xl text-primary"
>
<Card>
<CardContent>
<Item>
<ItemMedia>
<span
aria-hidden="true"
className={cn(
"size-2 rounded-full bg-primary",
animate && "animate-pulse motion-reduce:animate-none"
)}
/>
</ItemMedia>
<ItemContent>
<ItemTitle>{COPY.title}</ItemTitle>
<ItemDescription>{COPY.body}</ItemDescription>
</ItemContent>
</Item>
</CardContent>
</Card>
</RimGlow>
</div>
)
}Prop
Type
It also accepts the HTML attributes of the element it renders.
npm: cn. The CLI installs them for you.
No other registry pieces.
Glow Section
Dark section wrapper lit by a Glow Rig lamp with optional film grain over a fixed near-black ground. Passes the bulb color to cards inside; lamp position, intensity and grain are configurable.
Side Glow
Soft blue radial glow anchored to one edge of a positioned parent: top, right, bottom or left. Ships with SeamLine, a one-pixel overlay-blended rule for marking a panel edge.