Glass badges
Five frosted badges: frost, a primary tint, crystal, smoke and a 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/badges-glassDependencies, the @ai2/tokens theme and the component file are installed together.
Copy the source
components/ui/badges-glass.tsx"use client"
import type * as React from "react"
import { cn } from "@/lib/utils"
import { glassDepth } from "@/components/ui/glass"
/* Glass badge family: 5 frosted/translucent badges. Color comes ONLY from tokens
(alpha utility / color-mix), and the sizes line up with the base badge scale
(+xl). Static (needs no motion).
The surface derives from glassDepth in @ai2/glass (AGENTS.md 4.5) - there is no
hand-rolled blur any more. The largest surface in this file is h-8 (32px) and
the smallest is h-5 (20px): the smallest glass surfaces in the design system.
That is why the scale sits at the very bottom end - sm carries the body, md
appears in only two variations, and lg/xl are NEVER used. There is nothing
behind a 20px badge for a 16px blur to diffuse; it would only cost compositing.
twMerge trap: glassDepth carries a [box-shadow:...] (the ai2 signature). A
second shadow from the same group silently deletes that signature, which is
why the extra light/shadow is applied with drop-shadow (the filter group). */
export type StyledSize = "sm" | "md" | "lg" | "xl"
const box: Record<StyledSize, string> = {
sm: "h-5 gap-1 rounded-md px-1.5 text-xs",
md: "h-6 gap-1.5 rounded-md px-2 text-xs",
lg: "h-7 gap-1.5 rounded-lg px-2.5 text-sm",
xl: "h-8 gap-2 rounded-lg px-3 text-sm",
}
const base =
"relative inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap font-medium [&_svg]:size-3 [&_svg]:shrink-0 [&_i]:text-xs [&_i]:leading-none"
type Props = React.ComponentProps<"span"> & { size?: StyledSize }
/* Frost: plain frosted glass. Depth = sm (4px). The lightest step is a badge's default: in a 20px box the "frosted" feel comes not from the blur but from the library signature's inner top highlight. */
export function FrostBadge({ className, size = "md", children, ...props }: Props) {
return (
<span
data-slot="styled-badge"
className={cn(
base,
box[size],
glassDepth.sm,
"border border-border/60 text-foreground",
className
)}
{...props}
>
{children}
</span>
)
}
/* Tint: info-toned semi-transparent glass. Depth = sm (4px). The tone is info: primary/brand sits on the neutral ramp (chroma ~0.005), so a "primary tint" would stay invisible and produce the same pixels as Smoke. bg DELIBERATELY overrides the library's (tint = surface); the supports- variant is overridden too, otherwise in a browser with backdrop-filter the library's bg-background/50 would take the tint back. */
export function TintBadge({ className, size = "md", children, ...props }: Props) {
return (
<span
data-slot="styled-badge"
className={cn(
base,
box[size],
glassDepth.sm,
"border border-info/25 bg-info/15 text-primary supports-[backdrop-filter]:bg-info/15",
className
)}
{...props}
>
{children}
</span>
)
}
/* Crystal: clear glass. Depth = sm (4px) - the least diffusing in the family, because being "clear" requires being able to see what is behind. It used to draw its own top and bottom inset lines by hand; the library signature now does exactly that (a top highlight plus a bottom edge), and writing a second inset on top would erase the signature through twMerge. Crystal takes its character from a brighter border that foregrounds the signature. */
export function CrystalBadge({ className, size = "md", children, ...props }: Props) {
return (
<span
data-slot="styled-badge"
className={cn(
base,
box[size],
glassDepth.sm,
"border border-[color-mix(in_oklab,var(--color-foreground)_20%,transparent)] text-foreground",
className
)}
{...props}
>
{children}
</span>
)
}
/* Smoke: neutral, smoky glass darkened with foreground. Depth = md (8px) - one of the two "md" steps in this family. Smoke means diffusion, so it sits one step above Frost and Crystal; but in a 32px box this is also the ceiling. What separates it from Tint is the absence of hue; that separation only works because Tint is info. */
export function SmokeBadge({ className, size = "md", children, ...props }: Props) {
return (
<span
data-slot="styled-badge"
className={cn(
base,
box[size],
glassDepth.md,
"border border-foreground/10 bg-foreground/15 text-foreground supports-[backdrop-filter]:bg-foreground/15",
className
)}
{...props}
>
{children}
</span>
)
}
/* Halo: glass body + info halo. Depth = md (8px): the body has to separate a
little from what is behind it for the halo to read, otherwise the halo is just
a dirty edge.
The halo is now a drop-shadow rather than a box-shadow (the filter group): it
used to be a two-layer shadow-[...], and written alongside glassDepth twMerge
would have deleted the signature. drop-shadow already follows the shape and
gives a truer halo on a badge's rounded-md edge than box-shadow would. */
export function HaloBadge({ className, size = "md", children, ...props }: Props) {
return (
<span
data-slot="styled-badge"
className={cn(
base,
box[size],
glassDepth.md,
"border border-info/30 text-foreground drop-shadow-[0_0_4px_color-mix(in_oklab,var(--color-info)_55%,transparent)]",
className
)}
{...props}
>
{children}
</span>
)
}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
Frosted surface with backdrop blur.
import { FrostBadge } from "@/components/ui/badges-glass"
<FrostBadge>Frost</FrostBadge>Tint
A primary-tinted translucent badge.
import { TintBadge } from "@/components/ui/badges-glass"
<TintBadge>Tint</TintBadge>Crystal
Sharp inset highlights.
import { CrystalBadge } from "@/components/ui/badges-glass"
<CrystalBadge>Clear</CrystalBadge>Smoke
Foreground-tinted dark translucent.
import { SmokeBadge } from "@/components/ui/badges-glass"
<SmokeBadge>Smoke</SmokeBadge>Halo
A translucent glass halo.
import { HaloBadge } from "@/components/ui/badges-glass"
<HaloBadge>Halo</HaloBadge>ai2 Glass badges: 5 styled variations on the token system
The ai2 Glass badges are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around frosted, translucent badges. 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: these are static; no motion library work is required. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, there is no motion to reduce.
What is in the ai2 Glass badges?
5 exports in one file: Frost, Tint, Crystal, Smoke 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: these are static; no motion library work is required.
- Reduced-motion aware: Under prefers-reduced-motion, there is no motion to reduce.
- 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 badges 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.