Glass avatars
Five frosted avatars: frost, a primary tint, a hover sheen, clear crystal and a glass halo. Each is sized and token-driven, with a solid fallback where backdrop-filter is unsupported.
Installation
The styled layer is free and installs like any other ai2 component.
Run the following command
npx shadcn@latest add @ai2/avatars-glassDependencies, the @ai2/tokens theme and the component file are installed together.
Copy the source
components/ui/avatars-glass.tsx"use client"
import type * as React from "react"
import { cn } from "@/lib/utils"
import { glassDepth } from "@/components/ui/glass"
/* Glass avatar family: 5 decorative avatars on a frosted/translucent theme.
Initial-based (no external image) - a consumer can put an <img> in place of
the initials. Color comes ONLY from tokens (transparency via color-mix), size
= the box scale. The sheen sweep is pure CSS and is disabled under
reduced-motion (motion-reduce).
The surface derives from glassDepth in @ai2/glass (AGENTS.md 4.5).
What is special about this family: what sits "behind" the glass overlay is not
the page but the avatar's OWN opaque face (the initials). So the blur is not
diffusing content here, it is diffusing a single pair of letters - and in a
32px circle a 16px blur erases the letters completely. That is why the scale
is shifted down: sm carries the body, md is used only on HaloGlass, the
largest surface, and on the default Frost. lg/xl are not used. */
export type StyledSize = "sm" | "md" | "lg" | "xl"
const box: Record<StyledSize, string> = {
sm: "size-8 text-xs",
md: "size-10 text-sm",
lg: "size-12 text-base",
xl: "size-16 text-lg",
}
const face =
"flex size-full items-center justify-center rounded-full bg-secondary font-medium text-secondary-foreground select-none"
type Props = React.ComponentProps<"div"> & { size?: StyledSize; initials?: string }
/* Frost: a frosted-glass overlay. Depth = md (8px), the canonical default: the initials stay readable but sit behind the frost - the "normal" state of this family. */
export function FrostAvatar({ className, size = "md", initials = "AI", ...props }: Props) {
return (
<div data-slot="styled-avatar" className={cn("relative inline-flex", box[size], className)} {...props}>
<span className={face}>{initials}</span>
<span
aria-hidden="true"
className={cn(
glassDepth.md,
"pointer-events-none absolute inset-0 rounded-full border border-surface-3"
)}
/>
</div>
)
}
/* Tint: an info-toned transparent overlay. Depth = sm (4px): the initials still have to be readable behind the tint, and what carries the colour is the surface itself rather than the blur. The tone is info: primary/brand sits on the neutral ramp, so it would be an invisible tint. */
export function TintAvatar({ className, size = "md", initials = "AI", ...props }: Props) {
return (
<div data-slot="styled-avatar" className={cn("relative inline-flex", box[size], className)} {...props}>
<span className={face}>{initials}</span>
<span
aria-hidden="true"
className={cn(
glassDepth.sm,
"pointer-events-none absolute inset-0 rounded-full border border-info/30",
"bg-[color-mix(in_oklab,var(--color-info)_20%,transparent)] supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-info)_20%,transparent)]"
)}
/>
</div>
)
}
/* Sheen: a single token light sweep passing over it on hover. Depth = sm (4px): what this variation is about is the motion, not the glass - the more the blur grows, the more the sweep becomes a blurred smudge. bg was also pulled from the library's /50 to /25: the glass overlay sits above the sheen in the DOM and /50 was swallowing the sweep. The blur and the signature come from the library, only the surface opacity is tuned for this composition. */
export function SheenAvatar({ className, size = "md", initials = "AI", ...props }: Props) {
return (
<div
data-slot="styled-avatar"
className={cn("group relative inline-flex overflow-hidden rounded-full", box[size], className)}
{...props}
>
<span className={face}>{initials}</span>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-y-0 -left-full w-2/3 -skew-x-12 bg-gradient-to-r from-transparent via-foreground/20 to-transparent transition-[left] duration-700 ease-out group-hover:left-full motion-reduce:hidden"
/>
<span
aria-hidden="true"
className={cn(
glassDepth.sm,
"pointer-events-none absolute inset-0 rounded-full border border-surface-3",
"bg-background/25 supports-[backdrop-filter]:bg-background/25"
)}
/>
</div>
)
}
/* Crystal: clear glass. Depth = sm (4px), the least diffusing in the family - the
initials read most clearly here, which is the condition for being "clear". It
used to draw its own top/bottom inset lines by hand; the lib signature already
does exactly that, and writing a second inset on top of it would have deleted the
signature through twMerge. */
export function CrystalAvatar({ className, size = "md", initials = "AI", ...props }: Props) {
return (
<div data-slot="styled-avatar" className={cn("relative inline-flex", box[size], className)} {...props}>
<span className={face}>{initials}</span>
<span
aria-hidden="true"
className={cn(
glassDepth.sm,
"pointer-events-none absolute inset-0 rounded-full",
"border border-[color-mix(in_oklab,var(--color-foreground)_20%,transparent)]"
)}
/>
</div>
)
}
/* HaloGlass: a transparent glass halo ring (an outer offset ring). Depth = md (8px). This is the LARGEST surface in the file (it extends outside the avatar with -inset-1.5) and the only true glass with the page behind it: the others have the avatar's own opaque face behind them, while this ring sits over the page. Because it is the one variation where the blur really does a job, it sits a step higher. */
export function HaloGlassAvatar({ className, size = "md", initials = "AI", ...props }: Props) {
return (
<div data-slot="styled-avatar" className={cn("relative inline-flex", box[size], className)} {...props}>
<span
aria-hidden="true"
className={cn(
glassDepth.md,
"pointer-events-none absolute -inset-1.5 rounded-full border border-surface-3"
)}
/>
<span className={cn(face, "relative border border-surface-3")}>{initials}</span>
</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.
Frost
A frosted overlay ring from surface tokens.
import { FrostAvatar } from "@/components/ui/avatars-glass"
<FrostAvatar />Tint
A primary-tinted translucent overlay.
import { TintAvatar } from "@/components/ui/avatars-glass"
<TintAvatar />Sheen
A token sheen sweeps across on hover.
import { SheenAvatar } from "@/components/ui/avatars-glass"
<SheenAvatar />Crystal
Sharp inset highlights read as clear glass.
import { CrystalAvatar } from "@/components/ui/avatars-glass"
<CrystalAvatar />Halo
A translucent glass halo ring.
import { HaloGlassAvatar } from "@/components/ui/avatars-glass"
<HaloGlassAvatar />ai2 Glass avatars: 5 styled variations on the token system
The ai2 Glass avatars are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around frosted, translucent avatars. 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 sweeps the sheen; the rest are static. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, the sheen stops and the avatars stay still.
What is in the ai2 Glass avatars?
5 exports in one file: Frost, Tint, Sheen, Crystal and Halo. 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 sweeps the sheen; the rest are static.
- Reduced-motion aware: Under prefers-reduced-motion, the sheen stops and the avatars 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 Glass avatars 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.