# Glass

SVG highlights and CSS frosted surfaces, progressively enhanced with WebGPU refraction.

Page: https://sui.draco.dev/docs/components/glass

## CSS + SVG

CSS Gaussian backdrop blur and SVG edge highlights, without initializing WebGPU or capturing the background. Clear and frosted surfaces use the current theme background, while text keeps its semantic color. Switch the site appearance to compare light and dark modes.

### Example: glass-demo

```tsx
"use client";
import { GlassProvider, GlassSurface } from "@workspace/ui/components/glass";
import { useRef } from "react";
import type { ExampleProps } from "../types";

const tiles = [
  "bg-blue-500",
  "bg-emerald-500",
  "bg-amber-400",
  "bg-rose-500",
  "bg-violet-500",
  "bg-cyan-500",
];
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const scene = useRef<HTMLDivElement>(null);
  return (
    <GlassProvider mode="css" captureTarget={scene}>
      <div
        ref={scene}
        className="relative isolate grid min-h-80 w-full items-center gap-4 overflow-hidden rounded-2xl p-6 lg:grid-cols-3"
      >
        <div
          className="absolute inset-0 -z-10 grid grid-cols-3 gap-2 bg-muted p-2"
          aria-hidden="true"
        >
          {tiles.map((tile) => (
            <div key={tile} className={`${tile} rounded-xl`} />
          ))}
        </div>
        <GlassSurface
          glass={false}
          className="rounded-2xl border bg-background p-6 text-foreground"
        >
          <h3>{chinese ? "普通表面" : "Standard surface"}</h3>
          <p className="mt-2 text-muted-foreground text-sm">
            {chinese
              ? "不采样背景，保持默认外观"
              : "Keeps the default appearance without sampling the background."}
          </p>
        </GlassSurface>
        <GlassSurface
          material="clear"
          className="rounded-2xl bg-background p-6 text-foreground"
        >
          <h3>{chinese ? "清透玻璃" : "Clear glass"}</h3>
          <p className="mt-2 text-muted-foreground text-sm">
            {chinese
              ? "轻度模糊与柔和边缘高光，保持背景可辨"
              : "A light blur and soft edge highlights keep the background recognizable."}
          </p>
        </GlassSurface>
        <GlassSurface
          material="frosted"
          className="rounded-2xl bg-background p-6 text-foreground"
        >
          <h3>{chinese ? "磨砂玻璃" : "Frosted glass"}</h3>
          <p className="mt-2 text-muted-foreground text-sm">
            {chinese
              ? "更强的模糊和色调遮罩，适合承载正文。无需截图或 GPU"
              : "A stronger blur and tint support readable content. No snapshot or GPU is needed."}
          </p>
        </GlassSurface>
      </div>
    </GlassProvider>
  );
}
```

## Installation

```bash
bunx --bun shadcn@latest add @sui/glass
```

Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases.

## Usage

```tsx
import { GlassProvider, GlassSurface } from "@workspace/ui/components/glass";
import { useRef } from "react";

export function GlassPanel() {
  const sceneRef = useRef<HTMLDivElement>(null);
  return (
    <GlassProvider mode="css" material="frosted" captureTarget={sceneRef}>
      <div ref={sceneRef} className="relative rounded-2xl bg-muted p-6">
        <GlassSurface className="rounded-2xl bg-background p-6">
          A glass surface
        </GlassSurface>
      </div>
    </GlassProvider>
  );
}
```

## How the surface is rendered

Glass starts with directional SVG edge highlights and a CSS backdrop blur. This base appearance is available without a GPU renderer. Server rendering does not depend on a GPU, and the component keeps its ordinary DOM structure and refs.

The provider defaults to `mode="css"`: it does not request a GPU or capture the document. Opt into `mode="auto"` to attempt WebGPU enhancement. When `navigator.gpu` is available and device initialization and background capture both succeed, WebGPU adds real refraction from a DOM snapshot. The presence of `navigator.gpu` alone does not guarantee enhancement. If initialization, sampling, or rendering fails, the SVG and CSS appearance remains in place.

The default treatment adds no border or decorative outer ring: the soft SVG edge defines the outline. Interactive controls retain their keyboard focus rings.

Glass is opt-in: ordinary components keep `glass={false}` by default. `GlassSurface` itself defaults to `glass={true}` and can be disabled for a direct comparison.

The provider shares background capture and rendering settings across its descendants. `captureTarget` can be an element or an element ref; scope it to the visual region you need instead of capturing an unnecessarily large document. In auto mode, instances with the same actual capture target, scene revision, and occlusion set share one background snapshot and one uploaded GPU texture. Each surface uses its own sampling coordinates and rounded outline. Moving a surface updates coordinates without recapturing an unchanged background. Background changes invalidate the snapshot. Portal overlays require another snapshot only when their background or occlusion set differs. Capture uses `html-to-image`; enhancement uses native WebGPU. The Glass runtime uses a static import; the capture dependency and WebGPU renderer load only when enhancement needs them.

CSS, fallback, and enhanced modes share one continuous SVG edge-lighting layer. The GPU handles background refraction and blur without adding a second rim. New frames replace the previous frame only after image decoding completes.

## WebGPU enhancement

The CSS backdrop responds to the underlying page directly. When WebGPU enhancement is active, text changes, ordinary images, grids, resizing, and scrolling can update its background snapshot. Ordinary captures are capped at 5Hz and scrolling captures at 10Hz, with captures serialized through a shared queue, so there is a short delay between a background change and its refracted appearance. This is not a live video feed. Use the provider ref’s `refresh()` when your application needs to request another snapshot.

This demonstration is separate from the CSS + SVG example above. Its status reports the actual rendering mode. The example enables `mode="auto"`, changes background text, and provides refraction, blur, and edge-highlight sliders. Its two independent surfaces share a scene. Scroll the exposed background around the surfaces. Setting strength to `0` lets you compare the captured background with its refracted version when enhancement is active; an unsupported device continues to show the CSS and SVG material.

### Example: glass-dynamic

```tsx
"use client";
import { Button } from "@workspace/ui/components/button";
import {
  type GlassHandle,
  GlassProvider,
  GlassSurface,
} from "@workspace/ui/components/glass";
import { Input } from "@workspace/ui/components/input";
import { Label } from "@workspace/ui/components/label";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import { useEffect, useId, useRef, useState } from "react";
import type { ExampleProps } from "../types";

const illustration =
  "data:image/svg+xml," +
  encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="600" height="180" viewBox="0 0 600 180"><defs><pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse"><path d="M24 0H0V24" fill="none" stroke="#fff" stroke-opacity=".65" stroke-width="1.5"/></pattern></defs><rect width="600" height="180" fill="#648493"/><circle cx="120" cy="80" r="90" fill="#AE916B"/><circle cx="360" cy="140" r="130" fill="#70866A"/><path d="M0 140L600 20" stroke="#BC6C73" stroke-width="24"/><rect width="600" height="180" fill="url(#grid)"/></svg>',
  );
const rows = Array.from({ length: 16 }, (_, index) => index + 1);
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const stateLabels = chinese
    ? {
        ready: "当前渲染：WebGPU 增强 + SVG 高光",
        fallback: "当前渲染：CSS + SVG（增强不可用）",
        loading: "当前渲染：CSS + SVG（等待增强）",
      }
    : {
        ready: "Rendering: WebGPU enhancement + SVG highlights",
        fallback: "Rendering: CSS + SVG (enhancement unavailable)",
        loading: "Rendering: CSS + SVG (awaiting enhancement)",
      };
  const id = useId();
  const scene = useRef<HTMLDivElement>(null);
  const controller = useRef<GlassHandle>(null);
  const [rendering, setRendering] = useState<"ready" | "fallback" | "loading">(
    "loading",
  );
  useEffect(() => {
    const target = scene.current;
    if (!target) return;
    const update = () => {
      const states = [...target.querySelectorAll('[data-glass="true"]')].map(
        (element) => element.getAttribute("data-glass-state"),
      );
      let next: "ready" | "fallback" | "loading" = "loading";
      if (states.length && states.every((state) => state === "ready"))
        next = "ready";
      else if (states.includes("fallback")) next = "fallback";
      setRendering(next);
    };
    const observer = new MutationObserver(update);
    observer.observe(target, {
      subtree: true,
      attributes: true,
      attributeFilter: ["data-glass-state"],
    });
    update();
    return () => observer.disconnect();
  }, []);
  const [text, setText] = useState("SUI / REFRACTION");
  const [strength, setStrength] = useState(22);
  const [blur, setBlur] = useState(8);
  const [highlight, setHighlight] = useState(0.3);
  const controls = [
    {
      key: "strength",
      label: chinese ? "折射强度" : "Refraction strength",
      value: strength,
      max: 64,
      step: 1,
      change: setStrength,
    },
    {
      key: "blur",
      label: chinese ? "模糊" : "Blur",
      value: blur,
      max: 24,
      step: 1,
      change: setBlur,
    },
    {
      key: "highlight",
      label: chinese ? "边缘高光" : "Edge highlight",
      value: highlight,
      max: 1,
      step: 0.05,
      change: setHighlight,
    },
  ];
  return (
    <div className="grid w-full gap-4">
      <p role="status" className="text-muted-foreground text-sm">
        {stateLabels[rendering]}
      </p>
      <div className="flex flex-wrap items-end gap-3">
        <div className="grid flex-1 gap-2">
          <Label htmlFor={id}>{chinese ? "背景文字" : "Background text"}</Label>
          <Input
            id={id}
            value={text}
            onChange={(event) => setText(event.target.value)}
          />
        </div>
        <Button
          variant="outline"
          type="button"
          onClick={() => controller.current?.refresh()}
        >
          {chinese ? "刷新快照" : "Refresh snapshot"}
        </Button>
      </div>
      <div className="grid gap-4 sm:grid-cols-3">
        {controls.map(({ key, label, value, max, step, change }) => (
          <div key={key} className="grid gap-2">
            <Label htmlFor={`${id}-${key}`}>
              {label}: <output htmlFor={`${id}-${key}`}>{value}</output>
            </Label>
            <input
              id={`${id}-${key}`}
              type="range"
              min={0}
              max={max}
              step={step}
              value={value}
              onInput={(event) => change(Number(event.currentTarget.value))}
              className="w-full accent-primary"
            />
          </div>
        ))}
      </div>
      <GlassProvider
        mode="auto"
        ref={controller}
        captureTarget={scene}
        options={{ strength, blur, highlight }}
      >
        <div
          ref={scene}
          className="relative isolate h-96 overflow-hidden rounded-2xl border bg-muted"
        >
          <ScrollArea
            role="region"
            className="absolute inset-0"
            aria-label={chinese ? "可滚动的背景" : "Scrollable background"}
          >
            <img
              src={illustration}
              alt={
                chinese
                  ? "由圆形和斜线组成的彩色图案"
                  : "Colorful circles and a diagonal stripe"
              }
              className="h-44 w-full object-cover"
            />
            <div className="grid grid-cols-2 gap-2 p-4 sm:grid-cols-4">
              {rows.map((row) => (
                <div
                  key={row}
                  className="grid min-h-24 content-center gap-1 rounded-xl bg-background p-3"
                >
                  <p className="text-primary text-sm">{text}</p>
                  <p className="text-muted-foreground text-xs">
                    {chinese ? "网格" : "Grid"} {row}
                  </p>
                </div>
              ))}
            </div>
          </ScrollArea>
          <GlassSurface
            material="clear"
            className="absolute top-20 left-6 w-[calc(50%-2rem)] rounded-2xl bg-background p-4 text-foreground"
          >
            <p>{chinese ? "文字与图片背景" : "Text and image background"}</p>
            <p className="mt-2 text-muted-foreground text-sm">
              {chinese
                ? "调整滑块或滚动背景，比较折射与原始快照"
                : "Adjust the sliders or scroll to compare refraction with the original snapshot."}
            </p>
          </GlassSurface>
          <GlassSurface
            material="clear"
            className="absolute top-20 right-6 w-[calc(50%-2rem)] rounded-2xl bg-background p-4 text-foreground"
          >
            <p>
              {chinese ? "同一场景，独立表面" : "One scene, separate surfaces"}
            </p>
            <p className="mt-2 text-muted-foreground text-sm">
              {chinese
                ? "同场景共用截图与纹理，独立更新坐标"
                : "Matching scenes share a snapshot and texture, with separate sampling coordinates."}
            </p>
          </GlassSurface>
        </div>
      </GlassProvider>
    </div>
  );
}
```

## Inputs, viewers, overlays

Pass `glass` to supported shared components rather than replacing their native controls. Inputs retain their original DOM node, ref, form behavior, and focus ring. Independent buttons, inputs, choice controls, and toolbar actions inherit glass inside a glass container. Layout wrappers, content, and the native input inside an InputGroup reuse their owning surface. An explicit `glass={false}` disables a control or scope. The example uses an ordinary parent surface with independent glass controls, avoiding nested material sampling.

The example combines an editable input, a popover, a dialog, Editor, and Code Viewer. Ordinary content avoids repeated material layers. The dialog input is an independent interactive control and inherits glass, demonstrating nested glass while retaining its native focus ring. Keyboard, focus, copy, and editing behavior remains available.

### Example: glass-composition

```tsx
"use client";
import { Button } from "@workspace/ui/components/button";
import { CodeViewer } from "@workspace/ui/components/code-viewer";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@workspace/ui/components/dialog";
import { Editor } from "@workspace/ui/components/editor";
import { GlassProvider } from "@workspace/ui/components/glass";
import { Input } from "@workspace/ui/components/input";
import { Label } from "@workspace/ui/components/label";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@workspace/ui/components/popover";
import { useId, useRef, useState } from "react";
import type { ExampleProps } from "../types";
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const id = useId();
  const scene = useRef<HTMLDivElement>(null);
  const [code, setCode] = useState('const theme = "bamboo";');
  return (
    <GlassProvider mode="css" captureTarget={scene}>
      <div
        ref={scene}
        className="relative isolate grid w-full gap-4 overflow-hidden rounded-2xl bg-muted p-5"
      >
        <div
          className="absolute inset-0 -z-10 grid grid-cols-3 gap-3 p-3"
          aria-hidden="true"
        >
          <div className="rounded-2xl bg-emerald-400/70" />
          <div className="rounded-2xl bg-blue-400/70" />
          <div className="rounded-2xl bg-rose-400/70" />
        </div>
        <div className="grid gap-4 rounded-2xl border bg-background p-5">
          <div className="grid gap-2">
            <Label htmlFor={id}>
              {chinese
                ? "原生输入仍可交互"
                : "The native input stays interactive"}
            </Label>
            <Input
              id={id}
              glass
              placeholder={chinese ? "输入一些文字" : "Type something"}
            />
          </div>
          <div className="flex flex-wrap gap-2">
            <Popover>
              <PopoverTrigger render={<Button variant="outline" glass />}>
                {chinese ? "玻璃浮层" : "Glass popover"}
              </PopoverTrigger>
              <PopoverContent glass>
                <p className="text-sm">
                  {chinese
                    ? "浮层保留 SVG 高光与 CSS 磨砂，内容维持清晰"
                    : "The popover keeps SVG highlights and CSS frosting while its content stays clear."}
                </p>
              </PopoverContent>
            </Popover>
            <Dialog>
              <DialogTrigger render={<Button variant="outline" glass />}>
                {chinese ? "打开对话框" : "Open dialog"}
              </DialogTrigger>
              <DialogContent glass>
                <DialogHeader>
                  <DialogTitle>
                    {chinese ? "玻璃对话框" : "Glass dialog"}
                  </DialogTitle>
                  <DialogDescription>
                    {chinese
                      ? "保留焦点管理、键盘和关闭行为"
                      : "Focus management, keyboard access, and dismissal are preserved."}
                  </DialogDescription>
                </DialogHeader>
                <Input
                  aria-label={chinese ? "对话框输入" : "Dialog input"}
                  placeholder={chinese ? "仍可输入" : "Still editable"}
                />
              </DialogContent>
            </Dialog>
          </div>
        </div>
        <Editor
          value={code}
          onChange={setCode}
          language="typescript"
          height={180}
          glass
          labels={
            chinese
              ? {
                  copied: "已复制",
                  copyFailed: "复制失败",
                  loading: "正在加载编辑器",
                  loadingPreview: "正在加载预览",
                  error: "无法加载编辑器",
                  preview: "预览",
                  hidePreview: "隐藏预览",
                  split: "并排",
                  copy: "复制",
                  fullscreen: "全屏",
                  exitFullscreen: "退出全屏",
                }
              : undefined
          }
          toolbarCopy={false}
          toolbarMode={false}
          fullscreen={false}
        />
        <CodeViewer
          code={code}
          lang="typescript"
          glass
          title={chinese ? "当前代码" : "Current code"}
          maxHeight={160}
          labels={
            chinese
              ? {
                  copy: "复制代码",
                  copied: "已复制",
                  copyFailed: "复制失败",
                  loading: "正在高亮",
                  error: "无法高亮代码",
                  empty: "没有代码",
                  expand: "展开",
                  collapse: "折叠",
                  writing: "正在编写",
                  ready: "就绪",
                }
              : undefined
          }
        />
      </div>
    </GlassProvider>
  );
}
```

[TabBar](/docs/components/tab-bar) provides independent application navigation with a moving glass lens. It shares the same provider and material foundation while keeping routing under application control.

## Materials

Choose `material="clear"` for a more transparent surface or `material="frosted"` for stronger frosting and a denser tint. Clear uses a `6px` blur and a `0.60` CSS tint opacity; frosted uses `8px` and `0.78`. The provider defaults to `frosted`; `GlassSurface` can override the inherited material for one surface. An explicit `options.blur` overrides the material blur. The default demonstration uses CSS and SVG only, with ordinary, clear, and frosted surfaces side by side.

## Themes and fallback

The default tint comes from the current surface background token. It follows light, dark, and accent themes while preserving primary and danger colors. Adjust `strength`, `blur`, `tint`, `tintOpacity`, and `highlight` on the provider for a shared treatment. Soft SVG highlights are confined to the surface edge; `highlight` controls their intensity. The default SVG and CSS appearance retains these edge-light details; both `highlight` and `blur` continue to apply without WebGPU.

When WebGPU or background capture is unavailable, the default frosted surface remains visible. Controls remain usable. The glass effect should improve a surface visually without being required to understand it.

Glass strengthens neutral secondary text and placeholders locally, while preserving the theme's primary, destructive, and focus colors. Check text against the actual background when choosing clear glass or overriding tint opacity; transparency alone cannot guarantee readable contrast over every background. Use a denser tint or an ordinary surface when the underlying content makes text difficult to read.

WebGPU enhancement checks the actual colors of text and input placeholders belonging to the surface, including their alpha, and adjusts the captured background toward a shared tint that supports at least 4.5:1 contrast. Independently painted children and nested glass surfaces handle their own backgrounds. If the collected colors cannot share a readable background, enhancement falls back to CSS and SVG. CSS fallback, generated or SVG text, custom blending, filters, and outer opacity still require checking against the rendered scene.

## Snapshot limitations

The WebGPU enhancement uses DOM capture, which is not a browser compositor screenshot. Cross-origin images need suitable CORS permission; cross-origin iframes cannot be read. Embedded iframe documents, video frames, tainted canvases, and other browser-managed surfaces may be absent or inaccurate in the snapshot. Canvas and other GPU-rendered content are not guaranteed to be captured.

Rapid animation can outpace the capture limit. Use ordinary surfaces or the frosted fallback when accurate live media is important, and keep the capture area modest on mobile devices.

Positive horizontal and vertical scaling preserves the original layout while mapping the snapshot and corner radii to the viewport. Rotation, skew, perspective, and mirrored transforms use the CSS + SVG fallback instead of displaying a misaligned snapshot.

## API reference

| Prop | Type | Default / behavior |
| --- | --- | --- |
| Provider `mode` | `"css" \| "auto"` | `"css"`; `"auto"` attempts WebGPU refraction. |
| Provider `material` | `"clear" \| "frosted"` | `"frosted"`; shared material. |
| Provider `options` | `GlassOptions` | Shared rendering settings. |
| Provider `captureTarget` | `HTMLElement \| null \| RefObject<HTMLElement \| null>` | Optional scoped DOM capture target. |
| Provider `ref` | `Ref<GlassHandle>` | `refresh(): void` requests a snapshot. |
| Options `strength` | `number` | `22`; WebGPU refraction strength. |
| Options `blur` | `number` | Material default; an explicit value overrides CSS and enhanced blur. |
| Options `tint` | `string` | Current surface background color. |
| Options `tintOpacity` | `number` | CSS and enhancement share the material default; an explicit value overrides both. |
| Options `highlight` | `number` | `0.3`. |
| Surface `glass` | `boolean` | `true`; ordinary component props default to `false`. |
| Surface `material` | `"clear" \| "frosted"` | Inherits the provider material. |
| Surface other props | Native div props | Includes `className`, `style`, and `ref`. |

The module exports `GlassProvider`, `GlassSurface`, `GlassOptions`, `GlassCaptureTarget`, `GlassProviderProps`, `GlassMode`, `GlassMaterial`, and `GlassHandle`. SVG and CSS provide the default surface; the optional enhancement uses [WebGPU](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API). The public SUI props above govern component integration.
