docs / core / glow-card
GlowCard
a Card whose border lights up on the edge facing the pointer, so a single card earns the directional glow BentoGrid shares across a whole grid. it takes every Card prop.
intensity · recedesrsc · server
provenance
authored in usvalayer
core / primitivesintensity
recedes · safe anywhere, including dense surfacescomposition
the one card on a view that earns the pointer glow, a hero or a highlighted tieranywhere a plain Card goes: it is a drop-in with a lit edgenever a grid of them. shared glow across many cells is BentoGridnot on a touch-first surface. the glow needs a pointer to followa11y
the glow layer isaria-hidden · everything else is a plain Cardjest-axedependencies
Card from the same packagelive demo · try it out
interactive
hover near the edges
Move your cursor toward the border. The lit arc turns to follow the pointer.
customize
surfaceshares Card's surface scale
eyebrowmono label
titlethe card heading
usage
import { CardBody, CardEyebrow, CardHeader, CardTitle, GlowCard } from "usva/primitives/card";
<GlowCard>
<CardHeader>
<CardEyebrow>interactive</CardEyebrow>
<CardTitle>hover near the edges</CardTitle>
</CardHeader>
<CardBody>Move your cursor toward the border.</CardBody>
</GlowCard>props
| prop | type | default | notes |
|---|---|---|---|
| …CardProps | CardProps | — | every Card prop applies: surface, highlight, interactive, and the rest. |
get it
npx shadcn add https://usva.build/r/card.jsonsource · components/ui/card.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
import { Badge, type BadgeProps } from "./badge";
type Div = React.HTMLAttributes<HTMLDivElement>;
const make = (base: string, name: string) =>
Object.assign(
React.forwardRef<HTMLDivElement, Div>(({ className, ...p }, ref) => (
<div ref={ref} className={cn(base, className)} {...p} />
)),
{ displayName: name },
);
export type CardHighlight = "none" | "wash" | "edge" | "ring";
/**
* The shared surface vocabulary. One word picks how any card-like surface
* (Card, StatCard, Panel, Dialog) sits on the page, and the same choice reads
* consistently across all of them.
* elevated: the default. Lit-from-above surface fill, rim light, shadow.
* flat: quiet surface fill, no lift. The sisu/dashboard workhorse.
* glass: translucent, blurs what sits behind it. A rare, purposeful
* choice, never a default.
* outline: transparent, carried by its border alone.
*/
export type CardSurface = "elevated" | "flat" | "glass" | "outline";
/** Background + rim treatment per surface. No shadow; consumers pick the level. */
export const SURFACE_SKIN: Record<CardSurface, string> = {
elevated: "rim-light bg-surface",
flat: "bg-surface",
glass: "rim-light bg-surface/65 backdrop-blur-md",
outline: "bg-transparent",
};
/** Whether a surface reads as raised (gets its host's elevation shadow). */
export const SURFACE_ELEVATED: Record<CardSurface, boolean> = {
elevated: true,
flat: false,
glass: true,
outline: false,
};
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
interactive?: boolean;
highlight?: CardHighlight;
surface?: CardSurface;
/** @deprecated use `highlight="wash"` */
wash?: boolean;
}
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
(
{
className,
interactive,
highlight = "none",
surface = "elevated",
wash,
...p
},
ref,
) => {
const resolved: CardHighlight =
highlight === "none" && wash ? "wash" : highlight;
return (
<div
ref={ref}
data-interactive={interactive ? "" : undefined}
data-highlight={resolved !== "none" ? resolved : undefined}
data-surface={surface}
className={cn(
"relative isolate rounded-2xl border border-border text-ink",
SURFACE_SKIN[surface],
resolved === "ring"
? "glow-ring"
: SURFACE_ELEVATED[surface] && "shadow-floating",
resolved === "wash" && "wash-accent",
resolved === "edge" &&
"after:absolute after:inset-x-5 after:top-0 after:h-px after:hairline-accent",
interactive &&
"transition-control duration-base ease-soft hover:-translate-y-0.5 motion-reduce:transform-none motion-reduce:transition-none",
interactive && resolved !== "ring" && "hover:shadow-overlay",
className,
)}
{...p}
/>
);
},
);
Card.displayName = "Card";
export interface CardHeaderProps extends Div {
row?: boolean;
}
export const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
({ className, row, ...p }, ref) => (
<div
ref={ref}
className={cn(
"relative p-5 after:absolute after:inset-x-5 after:bottom-0 after:h-px after:hairline-accent",
row ? "flex flex-row items-start gap-4" : "flex flex-col gap-1.5",
className,
)}
{...p}
/>
),
);
CardHeader.displayName = "CardHeader";
export const CardBody = make("p-5", "CardBody");
export const CardFooter = make(
"flex items-center gap-2 p-5 border-t border-border",
"CardFooter",
);
export const CardEyebrow = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...p }, ref) => (
<p
ref={ref}
className={cn(
"font-mono text-[0.65rem] uppercase leading-none tracking-[0.16em] text-muted",
className,
)}
{...p}
/>
));
CardEyebrow.displayName = "CardEyebrow";
export interface CardTitleProps
extends React.HTMLAttributes<HTMLHeadingElement> {
/** Heading level. Pick it to fit the page outline. */
as?: "h2" | "h3" | "h4" | "h5" | "h6";
}
export const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
({ className, as: Heading = "h3", ...p }, ref) => (
<Heading
ref={ref}
className={cn(
"text-balance text-sm font-semibold tracking-[-0.01em] text-ink",
className,
)}
{...p}
/>
),
);
CardTitle.displayName = "CardTitle";
export const CardIcon = make(
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-2 text-muted [&_svg]:h-4 [&_svg]:w-4",
"CardIcon",
);
export const CardActions = make(
"ml-auto flex items-center gap-1",
"CardActions",
);
export function CardBadge({ className, ...props }: BadgeProps) {
return <Badge className={cn("self-start", className)} {...props} />;
}
CardBadge.displayName = "CardBadge";
source · components/ui/glow-card.tsxexactly what this command copies
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Card, type CardProps } from "./card";
/**
* A Card whose border lights up on the edge facing the pointer. The arc turns
* to follow the cursor and brightens as you approach the edge, so a single card
* gets the same directional glow the BentoGrid shares across a whole grid.
*/
export const GlowCard = React.forwardRef<HTMLDivElement, CardProps>(
({ className, children, ...props }, ref) => {
const localRef = React.useRef<HTMLDivElement | null>(null);
const setRefs = React.useCallback(
(node: HTMLDivElement | null) => {
localRef.current = node;
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
},
[ref],
);
const onPointerMove = React.useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
const el = localRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
el.style.setProperty("--edge-x", `${e.clientX - r.left}px`);
el.style.setProperty("--edge-y", `${e.clientY - r.top}px`);
el.style.setProperty("--edge-o", "1");
},
[],
);
const onPointerLeave = React.useCallback(() => {
localRef.current?.style.setProperty("--edge-o", "0");
}, []);
return (
<Card
ref={setRefs}
className={cn("relative", className)}
onPointerMove={onPointerMove}
onPointerLeave={onPointerLeave}
{...props}
>
<span aria-hidden className="edge-glow" />
{children}
</Card>
);
},
);
GlowCard.displayName = "GlowCard";