glass-ui
componentaccordionsource: ok · hit
Accordion
A vertically stacked set of interactive headings that each reveal a section of content.
npx shadcn add "https://glass-ui.crenspire.com/r/accordion.json"free
Interaction
scroll-revealaccordion
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@radix-ui/react-accordion
iconslucide-react
Files6 · source cached 10/14/2026, 5:24:28 PM
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
lib/liquid-glass/material.ts
import type * as React from "react"
import { cn } from "@/lib/utils"
/* Liquid glass material: turns a role + material into the CSS classes (see the .lg layer in
app/globals.css) and the data-glass attributes the WebGL engine picks up. */
export type Material = "regular" | "clear" | "tinted"
export type GlassRole = "surface" | "sheet" | "bar" | "control" | "knob" | "inline" | "overlay"
export interface GlassOverride { tint?: string; frost?: number }
export interface LiquidProps {
/** Glass material. Default `regular`. */
material?: Material
/** Tint colour (any CSS colour). Defaults to the primary colour for `tinted`. */
tint?: string
/** Per-instance override. */
glass?: GlassOverride
/** Draw with the WebGL engine when a LiquidGlassProvider is present. WebGL glass sits behind
the page, so only elements resting directly on the backdrop should use it. Defaults to
true for in-flow roles and false for sheets; overlays and inline items never use it. */
webgl?: boolean
}
/** `data-*` attributes for the WebGL engine. */
export type DataAttrs = { [K in `data-${string}`]?: string }
const PRESET: Record<GlassRole, string | null> = {
surface: "card", sheet: "sheet", bar: "bar", control: "button", knob: "knob", inline: null, overlay: null,
}
// Roles whose elements usually float over page content default to CSS-only.
const WEBGL_DEFAULT: Record<GlassRole, boolean> = {
surface: true, sheet: false, bar: true, control: true, knob: true, inline: false, overlay: false,
}
const PARAMS: Record<Material, { frost: number; tint: number }> = {
regular: { frost: 1, tint: 0.2 },
clear: { frost: 0.1, tint: 0 },
tinted: { frost: 0.6, tint: 0.35 },
}
export function liquidGlass({ role, material = "regular", tint, glass, webgl = WEBGL_DEFAULT[role] }: LiquidProps & { role: GlassRole }): {
className: string; style: React.CSSProperties; attrs: DataAttrs
} {
const p = PARAMS[material]
const color = glass?.tint ?? tint
const frost = glass?.frost ?? p.frost
const style: Record<string, string> = {}
if (color) {
style["--lg-tint"] = color
if (material !== "tinted") style["--lg-tint-amt"] = "18%"
}
const preset = webgl ? PRESET[role] : null
const attrs: DataAttrs = preset
? { "data-glass": preset, "data-frost": String(frost), "data-tint": String(p.tint) }
: {}
return { className: `lg lg-${material} lg-${role}`, style: style as React.CSSProperties, attrs }
}
/** Spread onto an element: material classes/attrs merged with the element's own className and style. */
export function liquidProps(
opts: LiquidProps & { role: GlassRole },
own: { className?: string; style?: React.CSSProperties } = {}
): { className: string; style: React.CSSProperties } & DataAttrs {
const r = liquidGlass(opts)
return { ...r.attrs, className: cn(r.className, own.className), style: { ...r.style, ...own.style } }
}
/** A component's `variant` (default `liquid`) together with its material (default `regular`). */
export function resolveVariant<V extends string>(variant: V | undefined, material: Material | undefined) {
return { variant: (variant ?? "liquid") as V | "liquid", material: material ?? "regular" }
}
lib/hover-effects.ts
import { cva } from "class-variance-authority"
/**
* Shared hover effect variants for all Glass UI components
*
* Effects:
* - none: No hover effect
* - glow: Soft white rim bloom that intensifies on hover
* - shimmer: Moving shimmer effect across the component
* - ripple: Ripple effect that scales outward on hover
* - lift: Subtle lift effect with shadow increase
* - scale: Scale up slightly on hover
*/
export const hoverEffects = cva(
"transition-all duration-300",
{
variants: {
hover: {
none: "",
glow: "shadow-[0_0_0_1px_rgba(255,255,255,0.35),0_0_28px_-4px_rgba(255,255,255,0.45)] hover:shadow-[0_0_0_1px_rgba(255,255,255,0.5),0_0_36px_-4px_rgba(255,255,255,0.6)]",
shimmer: "relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent hover:before:translate-x-full before:transition-transform before:duration-1000",
ripple: "relative overflow-hidden after:absolute after:inset-0 after:scale-0 after:rounded-full after:bg-white/30 after:transition-transform after:duration-500 hover:after:scale-150",
lift: "hover:-translate-y-1 hover:shadow-lg",
scale: "hover:scale-105",
},
},
defaultVariants: {
hover: "none",
},
}
)
export type HoverEffect = "none" | "glow" | "shimmer" | "ripple" | "lift" | "scale"
app/globals.css
@import "tailwindcss";
@import "tw-animate-css";
/* Design specs and plans quote class names; they are not UI source. */
@source not "../docs/superpowers";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 1.25rem;
/* Base colors - Light mode with dark text */
--background: transparent;
--foreground: oklch(0.15 0 0); /* Near black - dark text for light mode */
--card: transparent;
--card-foreground: oklch(0.15 0 0); /* Near black - dark text */
--popover: transparent;
--popover-foreground: oklch(0.15 0 0); /* Near black - dark text */
/* Primary colors - Light mode */
--primary: oklch(0.5 0.2 250); /* Blue */
--primary-foreground: oklch(0.98 0 0); /* White text on primary */
/* Secondary colors - Light mode */
--secondary: oklch(0.96 0 0); /* Very light gray */
--secondary-foreground: oklch(0.15 0 0); /* Dark text */
/* Muted colors - Light mode */
--muted: oklch(0.85 0 0); /* Dark gray - darker shade to avoid glass effect override */
--muted-foreground: oklch(0.1 0 0); /* Medium gray text */
/* Accent colors - Light mode */
--accent: oklch(0.96 0 0); /* Very light gray */
--accent-foreground: oklch(0.15 0 0); /* Dark text */
/* Destructive colors - WCAG AA compliant (4.5:1 contrast) */
--destructive: oklch(0.55 0.22 25); /* Red - 4.5:1 with white */
--destructive-foreground: oklch(0.98 0 0); /* White text on red */
/* Border and input - WCAG AA compliant (3:1 for UI components) */
--border: oklch(0.88 0 0); /* Light gray - 3:1 contrast */
--input: oklch(0.88 0 0); /* Light gray - 3:1 contrast */
--ring: oklch(0.5 0.2 250); /* Blue ring - 3:1 contrast */
/* Chart colors - WCAG AA compliant */
--chart-1: oklch(0.6 0.2 250); /* Blue */
--chart-2: oklch(0.55 0.15 150); /* Green */
--chart-3: oklch(0.5 0.2 50); /* Yellow */
--chart-4: oklch(0.65 0.2 300); /* Purple */
--chart-5: oklch(0.6 0.2 20); /* Orange */
/* Sidebar colors - Light mode */
--sidebar: oklch(0.98 0 0); /* Near white */
--sidebar-foreground: oklch(0.15 0 0); /* Dark text */
--sidebar-primary: oklch(0.5 0.2 250); /* Blue */
--sidebar-primary-foreground: oklch(0.98 0 0); /* White text on primary */
--sidebar-accent: oklch(0.96 0 0); /* Very light gray */
--sidebar-accent-foreground: oklch(0.15 0 0); /* Dark text */
--sidebar-border: oklch(0.88 0 0); /* Light gray - 3:1 contrast */
--sidebar-ring: oklch(0.5 0.2 250); /* Blue ring - 3:1 contrast */
}
.dark {
/* Base colors - Apple-style white text */
--background: transparent;
--foreground: oklch(1 0 0); /* White - Apple-style */
--card: transparent;
--card-foreground: oklch(1 0 0); /* White - Apple-style */
--popover: transparent;
--popover-foreground: oklch(1 0 0); /* White - Apple-style */
/* Primary colors - Apple-style white text */
--primary: oklch(0.7 0.15 250); /* Light blue */
--primary-foreground: oklch(1 0 0); /* White - Apple-style */
/* Secondary colors - Apple-style white text */
--secondary: oklch(0.25 0 0); /* Dark gray */
--secondary-foreground: oklch(1 0 0); /* White - Apple-style */
/* Muted colors - Apple-style white text */
--muted: oklch(0.18 0 0); /* Darker gray - darker shade to avoid glass effect override */
--muted-foreground: oklch(1 0 0); /* White - Apple-style */
/* Accent colors - Apple-style white text */
--accent: oklch(0.25 0 0); /* Dark gray */
--accent-foreground: oklch(1 0 0); /* White - Apple-style */
/* Destructive colors - WCAG AA compliant (4.5:1 contrast) */
--destructive: oklch(0.65 0.22 25); /* Light red - 4.5:1 with dark background */
--destructive-foreground: oklch(0.12 0 0); /* Near black text on red */
/* Border and input - WCAG AA compliant (3:1 for UI components) */
--border: oklch(0.3 0 0); /* Medium gray - 3:1 contrast */
--input: oklch(0.25 0 0); /* Dark gray - 3:1 contrast */
--ring: oklch(0.6 0.2 250); /* Light blue ring - 3:1 contrast */
/* Chart colors - WCAG AA compliant */
--chart-1: oklch(0.65 0.2 250); /* Light blue */
--chart-2: oklch(0.7 0.15 150); /* Light green */
--chart-3: oklch(0.75 0.2 50); /* Light yellow */
--chart-4: oklch(0.7 0.2 300); /* Light purple */
--chart-5: oklch(0.68 0.2 20); /* Light orange */
/* Sidebar colors - Apple-style white text */
--sidebar: oklch(0.18 0 0); /* Dark gray */
--sidebar-foreground: oklch(1 0 0); /* White - Apple-style */
--sidebar-primary: oklch(0.7 0.15 250); /* Light blue */
--sidebar-primary-foreground: oklch(1 0 0); /* White - Apple-style */
--sidebar-accent: oklch(0.25 0 0); /* Dark gray */
--sidebar-accent-foreground: oklch(1 0 0); /* White - Apple-style */
--sidebar-border: oklch(0.3 0 0); /* Medium gray - 3:1 contrast */
--sidebar-ring: oklch(0.6 0.2 250); /* Light blue ring - 3:1 contrast */
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply text-foreground;
background-color: transparent;
}
}
@layer utilities {
/* Ensure react-day-picker doesn't override the liquid glass */
.lg[class*="rdp"] {
background-color: transparent !important;
}
.lg .rdp {
background-color: transparent !important;
}
.lg .rdp-month {
background-color: transparent !important;
}
}
/* Liquid glass material. Classes come from liquidGlass() in lib/liquid-glass/material.ts.
The rim is a gradient border-box under a padding-box wash, so no pseudo-elements are used. */
@layer components {
:root {
--lg-rim-hi: rgba(255, 255, 255, 0.8);
--lg-rim-lo: rgba(255, 255, 255, 0.18);
--lg-highlight: rgba(255, 255, 255, 0.6);
--lg-drop: rgba(15, 23, 42, 0.14);
}
.dark {
--lg-rim-hi: rgba(255, 255, 255, 0.34);
--lg-rim-lo: rgba(255, 255, 255, 0.06);
--lg-highlight: rgba(255, 255, 255, 0.16);
--lg-drop: rgba(0, 0, 0, 0.45);
}
.lg {
--lg-tint: transparent;
--lg-tint-amt: 0%;
--lg-wash: color-mix(in oklab, var(--lg-tint) var(--lg-tint-amt), var(--lg-base));
color: var(--foreground);
border: 1px solid transparent;
background:
linear-gradient(var(--lg-wash), var(--lg-wash)) padding-box,
linear-gradient(135deg, var(--lg-rim-hi), var(--lg-rim-lo) 35%, var(--lg-rim-lo) 65%, var(--lg-rim-hi)) border-box;
backdrop-filter: blur(var(--lg-blur)) saturate(var(--lg-sat));
-webkit-backdrop-filter: blur(var(--lg-blur)) saturate(var(--lg-sat));
box-shadow: inset 0 1px 0 var(--lg-highlight), 0 1px 2px var(--lg-drop), 0 12px 32px -14px var(--lg-drop);
}
.lg-regular { --lg-base: rgba(255, 255, 255, 0.42); --lg-blur: 16px; --lg-sat: 180%; }
.dark .lg-regular { --lg-base: rgba(255, 255, 255, 0.07); }
.lg-clear { --lg-base: rgba(255, 255, 255, 0.1); --lg-blur: 2px; --lg-sat: 150%; }
.dark .lg-clear { --lg-base: rgba(255, 255, 255, 0.04); }
.lg-tinted { --lg-tint: var(--primary); --lg-tint-amt: 55%; --lg-base: rgba(255, 255, 255, 0.3); --lg-blur: 12px; --lg-sat: 180%; color: var(--primary-foreground); }
.dark .lg-tinted { --lg-base: rgba(30, 32, 40, 0.35); }
.lg-surface { border-radius: 28px; }
.lg-sheet { border-radius: 34px; }
.lg-bar, .lg-control, .lg-knob, .lg-inline { border-radius: 9999px; }
.lg-inline { box-shadow: inset 0 1px 0 var(--lg-highlight); }
.lg-overlay { border-radius: 20px; }
.lg-overlay.lg-regular { --lg-base: rgba(255, 255, 255, 0.72); --lg-blur: 24px; }
.dark .lg-overlay.lg-regular { --lg-base: rgba(28, 30, 38, 0.74); }
.lg-control { transition: scale 0.45s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s; }
.lg-control:active:not(:disabled) { scale: 1.04 0.96; }
/* Edge lensing: Chromium only (html.lg-lens is set by <LiquidFilters />). */
html.lg-lens .lg:not(.lg-inline):not(.lg-overlay) {
backdrop-filter: url(#lg-lens) blur(var(--lg-blur)) saturate(var(--lg-sat));
}
@media (prefers-reduced-motion: reduce) {
.lg-control { transition: none; }
.lg-control:active:not(:disabled) { scale: none; }
}
@media (prefers-reduced-transparency: reduce) {
.lg.lg { --lg-base: color-mix(in oklab, var(--background) 94%, transparent); backdrop-filter: none; -webkit-backdrop-filter: none; }
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
/* Sonner toasts. Sonner injects unlayered CSS that beats the .lg layer, so the liquid material
is re-applied here from the .lg variables (the toast and its buttons carry lg classes). */
[data-sonner-toast],
[data-sonner-toast] [data-button],
[data-sonner-toast] [data-cancel],
[data-sonner-toast] [data-close-button] {
background:
linear-gradient(var(--lg-wash), var(--lg-wash)) padding-box,
linear-gradient(135deg, var(--lg-rim-hi), var(--lg-rim-lo) 35%, var(--lg-rim-lo) 65%, var(--lg-rim-hi)) border-box !important;
border: 1px solid transparent !important;
backdrop-filter: blur(var(--lg-blur)) saturate(var(--lg-sat)) !important;
-webkit-backdrop-filter: blur(var(--lg-blur)) saturate(var(--lg-sat)) !important;
color: var(--foreground) !important;
}
[data-sonner-toast] {
border-radius: 20px !important;
box-shadow: inset 0 1px 0 var(--lg-highlight), 0 1px 2px var(--lg-drop), 0 12px 32px -14px var(--lg-drop) !important;
}
[data-sonner-toast][data-type="success"] {
border-color: rgba(34, 197, 94, 0.3) !important;
}
[data-sonner-toast][data-type="error"] {
border-color: rgba(239, 68, 68, 0.3) !important;
}
[data-sonner-toast][data-type="warning"] {
border-color: rgba(234, 179, 8, 0.3) !important;
}
[data-sonner-toast][data-type="info"] {
border-color: rgba(59, 130, 246, 0.3) !important;
}
/* Toast description text */
[data-sonner-toast] [data-description] {
color: var(--muted-foreground) !important;
}
[data-sonner-toast] [data-button],
[data-sonner-toast] [data-cancel] {
border-radius: 9999px !important;
transition: opacity 0.2s ease !important;
}
[data-sonner-toast] [data-cancel] {
color: var(--muted-foreground) !important;
}
[data-sonner-toast] [data-button]:hover,
[data-sonner-toast] [data-cancel]:hover,
[data-sonner-toast] [data-close-button]:hover {
opacity: 0.9 !important;
}
components/ui/accordion.tsx
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { resolveVariant } from "@/lib/liquid-glass/material"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
variant?: "liquid" | "default"
}
>(({ className, variant, children, ...props }, ref) => {
const v = resolveVariant<"liquid" | "default">(variant, undefined)
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
v.variant === "liquid" ? "rounded-2xl px-3 hover:bg-foreground/5 hover:no-underline" : "hover:bg-accent",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
})
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
components/ui/glass/accordion.tsx
"use client"
import * as React from "react"
import {
Accordion as BaseAccordion,
AccordionContent,
AccordionItem,
AccordionTrigger as BaseAccordionTrigger,
} from "@/components/ui/accordion"
import { cn } from "@/lib/utils"
export interface AccordionTriggerProps extends React.ComponentProps<typeof BaseAccordionTrigger> {
glow?: boolean
}
/**
* Glass UI Accordion - Enhanced accordion with glassy effects
*/
export const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof BaseAccordionTrigger>,
AccordionTriggerProps
>(({ className, glow = false, ...props }, ref) => {
return (
<BaseAccordionTrigger
ref={ref}
className={cn(
glow && "data-[state=open]:shadow-[0_0_0_1px_rgba(255,255,255,0.35),0_0_28px_-4px_rgba(255,255,255,0.45)]",
className
)}
{...props}
/>
)
})
AccordionTrigger.displayName = "AccordionTrigger"
export {
BaseAccordion as Accordion,
AccordionItem,
AccordionContent,
}