Styled card
Five card surfaces: glass, a gradient border, a glow, a 3D tilt and a spotlight. Each is sized by padding, token-driven and wraps any content.
Installation
The styled layer is free and installs like any other ai2 component.
Run the following command
npx shadcn@latest add @ai2/card-styledDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install motionCopy the source
components/ui/card-styled.tsx"use client"
import * as React from "react"
import { motion, useMotionValue, useReducedMotion, useSpring } from "motion/react"
import { cn } from "@/lib/utils"
/* Styled card family: 5 decorative opt-in card surfaces. The consumer can drop any content inside (children). Colour comes ONLY from semantic tokens (transparency via color-mix), size = the padding plus radius scale. The pointer physics (tilt and spotlight) are held with useMotionValue plus useSpring and are turned off completely under reduced-motion. */
export type StyledSize = "sm" | "md" | "lg" | "xl"
const sizes: Record<StyledSize, string> = {
sm: "rounded-lg p-4",
md: "rounded-xl p-5",
lg: "rounded-2xl p-6",
xl: "rounded-2xl p-8",
}
type Props = React.ComponentProps<"div"> & { size?: StyledSize }
/* Glass: buzlu token yuzey + backdrop-blur + ust highlight. backdrop-filter
desteklenmezse opak surface'a duser. */
export function GlassCard({ className, size = "md", children, ...props }: Props) {
return (
<div
data-slot="styled-card"
className={cn(
"relative border border-border bg-surface-2/70 text-card-foreground shadow-[inset_0_1px_0_color-mix(in_oklab,var(--color-foreground)_12%,transparent)] before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-1/2 before:rounded-[inherit] before:bg-[linear-gradient(to_bottom,color-mix(in_oklab,var(--color-foreground)_8%,transparent),transparent)] supports-[backdrop-filter]:bg-surface-2/40 supports-[backdrop-filter]:backdrop-blur-xl",
sizes[size],
className
)}
{...props}
>
{children}
</div>
)
}
/* GradientBorder: a token gradient border (the padding-box/border-box background-clip trick); the inner surface stays bg-card. */
export function GradientBorderCard({ className, size = "md", children, style, ...props }: Props) {
return (
<div
data-slot="styled-card"
className={cn(
"relative border border-transparent bg-clip-padding text-card-foreground",
sizes[size],
className
)}
style={{
background:
"linear-gradient(var(--color-card),var(--color-card)) padding-box, linear-gradient(135deg, color-mix(in oklab, var(--color-primary) 70%, transparent), color-mix(in oklab, var(--color-foreground) 15%, transparent)) border-box",
...style,
}}
{...props}
>
{children}
</div>
)
}
/* Glow: a soft token glow that strengthens on hover. */
export function GlowCard({ className, size = "md", children, ...props }: Props) {
return (
<div
data-slot="styled-card"
className={cn(
"relative border border-border bg-card text-card-foreground shadow-[0_0_24px_-12px_color-mix(in_oklab,var(--color-primary)_35%,transparent)] transition-shadow duration-(--motion-base) ease-(--motion-ease) hover:shadow-[0_0_44px_-8px_color-mix(in_oklab,var(--color-primary)_50%,transparent)] motion-reduce:transition-none",
sizes[size],
className
)}
{...props}
>
{children}
</div>
)
}
/* Tilt: the card tilts in 3D towards the cursor (rotateX/rotateY from the pointer, with a spring). The tilt is off under reduced-motion. */
export function TiltCard({ className, size = "md", children, style, ...props }: Props) {
const ref = React.useRef<HTMLDivElement>(null)
const reduce = useReducedMotion()
const rx = useMotionValue(0)
const ry = useMotionValue(0)
const springRx = useSpring(rx, { stiffness: 300, damping: 22 })
const springRy = useSpring(ry, { stiffness: 300, damping: 22 })
const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (reduce || !ref.current) return
const r = ref.current.getBoundingClientRect()
const px = (e.clientX - r.left) / r.width - 0.5
const py = (e.clientY - r.top) / r.height - 0.5
rx.set(py * -12)
ry.set(px * 12)
}
const reset = () => {
rx.set(0)
ry.set(0)
}
return (
<div
ref={ref}
data-slot="styled-card"
onPointerMove={onMove}
onPointerLeave={reset}
className={cn("relative", className)}
style={style}
{...props}
>
<motion.div
className={cn(
"relative border border-border bg-card text-card-foreground shadow-[0_10px_30px_-14px_color-mix(in_oklab,var(--color-foreground)_40%,transparent)]",
sizes[size]
)}
style={
reduce
? undefined
: { rotateX: springRx, rotateY: springRy, transformPerspective: 800 }
}
>
{children}
</motion.div>
</div>
)
}
/* Spotlight: a radial token light spot follows the cursor across the card. The spot is off under reduced-motion. */
export function SpotlightCard({ className, size = "md", children, ...props }: Props) {
const ref = React.useRef<HTMLDivElement>(null)
const reduce = useReducedMotion()
const x = useMotionValue(0)
const y = useMotionValue(0)
const springX = useSpring(x, { stiffness: 200, damping: 26 })
const springY = useSpring(y, { stiffness: 200, damping: 26 })
const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (reduce || !ref.current) return
const r = ref.current.getBoundingClientRect()
x.set(e.clientX - r.left - 150)
y.set(e.clientY - r.top - 150)
}
return (
<div
ref={ref}
data-slot="styled-card"
onPointerMove={onMove}
className={cn(
"group relative overflow-hidden border border-border bg-card text-card-foreground",
sizes[size],
className
)}
{...props}
>
<motion.div
aria-hidden="true"
className="pointer-events-none absolute left-0 top-0 size-[300px] rounded-full opacity-0 transition-opacity duration-(--motion-base) group-hover:opacity-100 motion-reduce:hidden"
style={
reduce
? { display: "none" }
: {
x: springX,
y: springY,
background:
"radial-gradient(circle, color-mix(in oklab, var(--color-primary) 30%, transparent), transparent 70%)",
}
}
/>
<div className="relative">{children}</div>
</div>
)
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Variations
5 takes on the same idea. Each is its own export, and every one accepts a size prop (sm, md, lg, xl) aligned to the base Button scale.
Glass
A frosted surface-token card with a top highlight.
import { GlassCard } from "@/components/ui/card-styled"
<GlassCard>
<h3>Card title</h3>
<p>...</p>
</GlassCard>Gradient border
A token gradient border on a clean card body.
import { GradientBorderCard } from "@/components/ui/card-styled"
<GradientBorderCard>
<h3>Card title</h3>
<p>...</p>
</GradientBorderCard>Glow
A soft token glow that intensifies on hover.
import { GlowCard } from "@/components/ui/card-styled"
<GlowCard>
<h3>Card title</h3>
<p>...</p>
</GlowCard>Tilt
A 3D perspective tilt toward the cursor.
import { TiltCard } from "@/components/ui/card-styled"
<TiltCard>
<h3>Card title</h3>
<p>...</p>
</TiltCard>Spotlight
A token spotlight that follows the cursor.
import { SpotlightCard } from "@/components/ui/card-styled"
<SpotlightCard>
<h3>Card title</h3>
<p>...</p>
</SpotlightCard>ai2 Styled card: 5 styled variations on the token system
The ai2 Styled card are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around decorative card surfaces. They are free and MIT licensed, and every color comes from a semantic token, so they theme with the rest of ai2 in light and dark.
Motion runs on framer-motion: framer-motion drives the tilt and spotlight from the pointer without re-rendering. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, the tilt and spotlight are disabled and the cards stay still.
What is in the ai2 Styled card?
5 exports in one file: Glass, Gradient border, Glow, Tilt and Spotlight. Each renders a native button and takes a size prop (sm, md, lg, xl) aligned to the base Button. They are separate from the base Button on purpose: the base keeps its clean variant, tone and size axes, while the styled layer carries the effects.
You own the file. Copy the one category file and you have all 5 variations, with no runtime dependency on ai2 itself.
Why use it
- On-system by construction: Every color resolves to an ai2 semantic token, so the buttons follow your theme in light and dark with no extra work.
- Effect without the sprawl: The decorations live in a dedicated styled file, so the base Button keeps its clean, predictable API.
- Accessible and honest: Each renders a real button element, keeps a visible focus ring, and respects prefers-reduced-motion.
Features
- Token-driven color: No hardcoded hex or oklch; the look recolors with your theme tokens.
- framer-motion: framer-motion drives the tilt and spotlight from the pointer without re-rendering.
- Reduced-motion aware: Under prefers-reduced-motion, the tilt and spotlight are disabled and the cards stay still.
- Size aligned to the base: Every variation takes sm, md, lg and xl matching the base Button height scale, so styled and base buttons line up in a row.
Production tips
- Use it for emphasis, not everywhere: Styled buttons draw the eye. Reserve them for the one action you want people to take on a screen, and use the base Button for the rest.
- Keep labels as verbs: The decoration adds weight, so a clear action label keeps the button scannable.
- Pick one variation per surface: The variations share a family; using two different ones in the same view competes for attention.
Works with the rest of ai2
The Styled card sit alongside the base Button and the rest of the @ai2 registry. They share the same token file, so a styled action next to a base button or a badge stays visually consistent in both modes.