Browse docsScene

Effects

Glass Scene

Draws the glass inside it as real bevelled glass that bends its backdrop and melts together.

pnpm dlx shadcn@4.14.1 add @glassyui/glass-scene

Preview

Liquid Glass

Light bends at every edge.

Demos ready. Update 0.

Installation

First add the exact @glassyui mapping to components.json as shown in Getting Started. Then run the pinned item command:

Install Glass Scene

pnpm dlx shadcn@4.14.1 add @glassyui/glass-scene
What this installs9 files, 1 package

Direct registry declarations

These values belong to @glassyui/glass-scene itself in registry.json.

Package dependencies

  • clsx

Registry dependencies

  • @glassyui/glass-provider

File targets

  • components/glassyui/glass-scene.tsx
  • components/glassyui/lib/merge-refs.ts
  • components/glassyui/lib/scene-context.ts
  • components/glassyui/lib/scene-renderer.ts
  • components/glassyui/lib/liquid-motion.ts

Complete installed closure

This follows every namespaced registry dependency recursively and de-duplicates the packages and target files installed with this item.

Package dependencies

  • clsx

Installed files

  • components/glassyui/glass-scene.tsx
  • components/glassyui/lib/merge-refs.ts
  • components/glassyui/lib/scene-context.ts
  • components/glassyui/lib/scene-renderer.ts
  • components/glassyui/lib/liquid-motion.ts
  • components/glassyui/glass-provider.tsx
  • components/glassyui/liquid-filters.tsx
  • components/glassyui/lib/preferences.ts
  • components/glassyui/styles/liquid.css

Usage

Save this as scene-example.tsx. It is the complete example shown above, including its artwork.

Usage

'use client';

import { useState } from 'react';
import { GlassButton } from '@/components/glassyui/glass-button';
import { GlassPanel } from '@/components/glassyui/glass-panel';
import { GlassDrop, GlassScene } from '@/components/glassyui/glass-scene';

const backdrop = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="700" viewBox="0 0 1200 700">
  <defs><linearGradient id="sky" x2="1" y2="1"><stop stop-color="#28140e"/><stop offset="1" stop-color="#a13e24"/></linearGradient><linearGradient id="wave" x2="1" y2="0"><stop stop-color="#ffb88d"/><stop offset=".5" stop-color="#ef542b"/><stop offset="1" stop-color="#a53722"/></linearGradient></defs>
  <rect width="1200" height="700" fill="url(#sky)"/>
  <path d="M-100 340C180 150 480 580 790 300S1190 230 1300 130V800H-100Z" fill="url(#wave)"/>
  <path d="M-100 440C220 210 480 660 790 400S1170 300 1300 210" fill="none" stroke="#ffe2cc" stroke-opacity=".65" stroke-width="3"/>
  <path d="M-100 610C180 410 490 770 840 550S1210 560 1300 420V800H-100Z" fill="#481a10"/>
</svg>`)}`;

type SceneExampleProps = { onAction?: (message: string) => void };

export function SceneExample({ onAction }: SceneExampleProps) {
  const [merged, setMerged] = useState(false);
  const [flowing, setFlowing] = useState(false);
  const [message, setMessage] = useState('');

  function announce(next: string) {
    setMessage(next);
    onAction?.(next);
  }

  return (
    <>
      <style>{`
        .glassy-scene-example { container-type: inline-size; width: 100%; height: 340px; overflow: hidden; border-radius: 22px; }
        .glassy-scene-example-card { position: absolute; top: 24px; left: 24px; width: min(260px, calc(100% - 48px)); padding: 18px; }
        .glassy-scene-example-drop { position: absolute; border-radius: 50%; transition: right 700ms var(--lg-spring), bottom 700ms var(--lg-spring); }
        .glassy-scene-example-large { right: 24px; bottom: 78px; width: clamp(84px, 25cqi, 144px); height: clamp(84px, 25cqi, 144px); }
        .glassy-scene-example-small { right: calc(24px + clamp(84px, 25cqi, 144px) + 34px); bottom: 92px; width: clamp(58px, 17cqi, 98px); height: clamp(58px, 17cqi, 98px); }
        .glassy-scene-example[data-merged="true"] .glassy-scene-example-small { right: calc(24px + clamp(84px, 25cqi, 144px) - 18px); bottom: 106px; }
        .glassy-scene-example-actions { --lg-scene-merge: 0px; position: absolute; bottom: 22px; left: 24px; display: flex; gap: 8px; }
        [data-lg-reduce-motion="1"] .glassy-scene-example-drop { transition: none; }
        @media (prefers-reduced-motion: reduce) { .glassy-scene-example-drop { transition: none; } }
      `}</style>
      <GlassScene
        backdrop={backdrop}
        className="glassy-scene-example"
        data-merged={merged}
        flow={flowing}
        optics={{ merge: 24 }}
      >
        <GlassDrop className="glassy-scene-example-drop glassy-scene-example-large" />
        <GlassDrop className="glassy-scene-example-drop glassy-scene-example-small" />
        <GlassPanel className="glassy-scene-example-card" variant="card">
          <p style={{ margin: 0, fontSize: 12, fontWeight: 600 }}>Liquid Glass</p>
          <p style={{ margin: '6px 0 0', fontSize: 22, fontWeight: 700, lineHeight: 1.15 }}>
            Light bends at every edge.
          </p>
        </GlassPanel>
        <div className="glassy-scene-example-actions">
          <GlassButton
            aria-pressed={merged}
            onClick={() => {
              setMerged(!merged);
              announce(`Drops ${merged ? 'apart' : 'merged'}.`);
            }}
            size="sm"
            variant="prominent"
          >
            Merge
          </GlassButton>
          <GlassButton
            aria-pressed={flowing}
            onClick={() => {
              setFlowing(!flowing);
              announce(`Flow ${flowing ? 'off' : 'on'}.`);
            }}
            size="sm"
          >
            Flow
          </GlassButton>
        </div>
      </GlassScene>
      {!onAction ? (
        <span
          role="status"
          style={{
            position: 'absolute',
            width: 1,
            height: 1,
            overflow: 'hidden',
            clipPath: 'inset(50%)',
          }}
        >
          {message}
        </span>
      ) : null}
    </>
  );
}

After configuring the registry, install every component this example uses:

Install example components

pnpm dlx shadcn@4.14.1 add @glassyui/glass-button @glassyui/glass-panel @glassyui/glass-scene

Examples

Render the copied example

import { SceneExample } from "./scene-example";

export default function Page() {
  return <SceneExample />;
}

API

GlassScene

Stage that paints its backdrop and draws the glass inside it on one WebGL2 canvas.

backdrop

The image the glass refracts: a URL, or a CSS url() or var() value. Serve it from your origin or with CORS.

flow

Lets drops ripple like liquid. Reduced motion holds them still. Defaults to false.

optics

ior (1.5), thickness (40), bevel (28), dispersion (0.15), frost (1), merge (12) and specular (1). Lengths are CSS pixels.

GlassDrop

A plain drop of glass. Size, round and place it with CSS.

merge

GlassDrop only: this drop’s merge distance in pixels. Defaults to the scene’s.

Notes

What joins the scene

GlassSurface, GlassPanel, GlassButton and GlassDrop inside a scene hand their glass to it. Ghost buttons, glass nested in other glass and surfaces with a generated refraction keep their own. Nested glass drops its shadow and tints away from the pane’s text, so its labels stay legible.

Legible text

Text on scene glass keeps 4.5:1 over any backdrop: where the backdrop is too bright or too dark, the face dims or lifts just enough and the rim keeps its light. Each piece protects the colour in --lg-scene-ink, the muted text on surfaces and the label on buttons. Set it to transparent on glass without text.

Optics

Each piece is a thick slab floating just above the backdrop. Light bends through its top, its body and the air beneath, so steeper rims and a higher ior bend more, and dispersion splits the bent light into a spectrum. Past the critical angle a rim mirrors the scene instead, and tinted glass absorbs more where it is thicker and casts a tinted shadow.

Browsers

The scene needs WebGL2, which every current browser has. Without it the scene logs one warning and every piece keeps its CSS glass.

Accessibility

Reduced transparency, increased contrast and forced colours switch the canvas off and return the CSS glass. Reduced motion holds the flow still. The canvas never takes clicks or focus.

Accessibility

Keep the Radix-backed semantics intact and preserve visible labels, focus states, and keyboard operation when copying the source.

Performance

Use provider quality preferences for dense app screens. Reserve strong refraction for hero or showcase surfaces.