Plugins

Glass presets

Switch the shared Liquid Glass material between thin, soft, regular, and thick, with a provider, scoped overrides, persistence, and a no-flash script.

The glass preset plugin changes the shared glass material without touching component code. It ships inside @xingxingmofashu/ui; no separate install is needed. It contains two parts:

  • The plugin index.css defines the thin, soft, regular, and thick materials.
  • @xingxingmofashu/ui/plugins/glass-preset manages data-glass, React state, and persistence.

Import the stylesheet after the core stylesheet:

@import "tailwindcss";
@import "@xingxingmofashu/ui";
@import "@xingxingmofashu/ui/plugins/glass-preset/index.css";

Without the plugin stylesheet, the core stylesheet still renders the default thin material. The data-glass attribute only has an effect after the preset stylesheet is imported.

Apply a preset

Set data-glass on the root or on any container:

<html data-glass="thin">
<section data-glass="thick">
  <Card>...</Card>
</section>
PresetLight veilDark veilBlurSaturateUse it for
thin20%62%4px150%Decorative surfaces over calm backgrounds
soft28.3%65.3%8px146.7%Secondary text and layered panels
regular36.7%68.7%12px143.3%A balanced choice for most interfaces
thick45%72%16px140%Dense text and busy photography

Presets also set --ui-glass-overlay-surface, the stronger veil used by text-bearing overlays: thin 64% / 76%, soft 69% / 79%, regular 74% / 82%, and thick 80% / 86% (light / dark).

thin is also the default when no attribute is present. The presets only change the shared --ui-glass-* variables; component internals, layout, and text styles stay the same. They inherit into descendants, and the light-dark() values follow the active color-scheme, so a preset inside a dark page resolves its dark values automatically. A nested .light or .dark subtree resets the color veils to the default thin values, but --ui-glass-blur and --ui-glass-saturate still inherit from the preset ancestor; add data-glass there too if the full preset should continue across that theme boundary.

React setup

Render GlassPresetScript in the document head and wrap the application with GlassPresetProvider:

import type { ReactNode } from "react";

import {
  GlassPresetProvider,
  GlassPresetScript,
} from "@xingxingmofashu/ui/plugins/glass-preset";

export default function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <GlassPresetScript storageKey="app-glass" defaultPreset="thin" />
      </head>
      <body>
        <GlassPresetProvider
          defaultPreset="thin"
          storageKey="app-glass"
        >
          {children}
        </GlassPresetProvider>
      </body>
    </html>
  );
}
  • GlassPresetScript runs before React hydrates and restores the stored preset, which avoids a flash of the default material.
  • GlassPresetProvider writes data-glass to <html> and provides the current value to useGlassPreset.
  • storageKey is optional. Omit it when the application manages persistence itself.
  • Use the same storageKey and defaultPreset for GlassPresetScript and GlassPresetProvider. If they differ, the pre-hydration attribute and the provider state can diverge and cause a flash.
  • Keep storageKey stable at the application root.

Change the preset at runtime

Call setPreset from useGlassPreset. No component code changes are needed:

"use client";

import { useGlassPreset } from "@xingxingmofashu/ui/plugins/glass-preset";

const PRESETS = ["thin", "soft", "regular", "thick"] as const;

export function PresetPicker() {
  const { preset, setPreset } = useGlassPreset();

  return (
    <div role="group" aria-label="Glass material">
      {PRESETS.map((value) => (
        <button
          key={value}
          type="button"
          aria-pressed={preset === value}
          onClick={() => setPreset(value)}
        >
          {value}
        </button>
      ))}
    </div>
  );
}

Without the provider, set the attribute directly:

document.documentElement.dataset.glass = "thick";

Controlled state

Pass preset and onPresetChange to own the value. In controlled mode, setPreset only calls onPresetChange; update the preset prop in that callback or the material will not change:

"use client";

import { useState, type ReactNode } from "react";

import {
  GlassPresetProvider,
  type GlassPreset,
} from "@xingxingmofashu/ui/plugins/glass-preset";

export function AppShell({ children }: { children: ReactNode }) {
  const [preset, setPreset] = useState<GlassPreset>("thin");

  return (
    <GlassPresetProvider preset={preset} onPresetChange={setPreset}>
      {children}
    </GlassPresetProvider>
  );
}

The provider still writes the active value to data-glass, but in controlled mode storageKey only persists the value. It does not restore the value into the preset prop. Use the uncontrolled defaultPreset + storageKey setup when storage should restore the preset automatically. If you need both, initialize the controlled state from storage yourself.

GlassPresetScript is independent: it can still read storageKey and set the attribute before hydration even when the provider is controlled. If the script resolves a different value than the controlled preset, the first paint uses the script value until the provider writes the controlled value after hydration. To avoid that flash, make the script resolve the same value as the controlled preset, or render the matching data-glass value on the server. The script only changes the DOM attribute; it does not change the value returned by useGlassPreset before hydration.

Scope a preset to a container

GlassPresetScope applies a preset to one container without changing the global value. It renders a div with data-glass and accepts normal div props:

import { Card } from "@xingxingmofashu/ui";
import { GlassPresetScope } from "@xingxingmofashu/ui/plugins/glass-preset";

<GlassPresetScope preset="thick" className="rounded-3xl">
  <Card title="Deployment" description="Production" />
</GlassPresetScope>

The preset's color veils inherit into descendants until another data-glass value or a nested .light / .dark boundary; --ui-glass-blur and --ui-glass-saturate continue to inherit across that boundary. Add data-glass at the boundary if you need the full preset there. The scope does not read or write the provider state or localStorage.

Script-only setup

If you do not need the React provider, use GlassPresetScript on its own or generate the script with getGlassPresetScript:

import {
  GlassPresetScript,
  getGlassPresetScript,
} from "@xingxingmofashu/ui/plugins/glass-preset";

<GlassPresetScript
  storageKey="app-glass"
  defaultPreset="thin"
  nonce={nonce}
/>;

const script = getGlassPresetScript({
  storageKey: "app-glass",
  defaultPreset: "thin",
});

getGlassPresetScript returns a plain string for frameworks that inject scripts through their own API. GlassPresetScript accepts a CSP nonce prop. The generated inline script escapes <, U+2028, and U+2029 in its string literals, so a runtime-provided storageKey cannot close the script element.

The script resolves a preset in the same order as the provider: a valid stored value, then a valid data-glass attribute already on <html>, then defaultPreset.

API reference

GlassPresetProvider

PropTypeDefaultDescription
presetGlassPreset—Controlled preset. When provided, the provider does not update its internal value; update this prop from onPresetChange.
defaultPresetGlassPreset"thin"Initial value for uncontrolled mode, including the server render and the first client render before persisted state is read. Ignored when preset is provided.
storageKeystring—localStorage key used to persist the preset. In uncontrolled mode it also restores the value after hydration. Omit to let the application manage persistence.
onPresetChange(preset: GlassPreset) => void—Called when setPreset requests a change, in both controlled and uncontrolled mode.
childrenReactNode—Subtree that can read the preset with useGlassPreset.
AttributeDescription
data-glassWritten to <html> after hydration with the active preset. GlassPresetScript may set it earlier.

useGlassPreset

Returns the provider state. It throws when called outside GlassPresetProvider.

FieldTypeDescription
presetGlassPresetThe active preset.
setPreset(preset: GlassPreset) => voidRequests a change. In controlled mode, update the preset prop from onPresetChange.

GlassPresetScope

PropTypeDefaultDescription
presetGlassPreset—Preset applied as data-glass to the container. Required.
...propsReact.ComponentProps<"div">—Passed to the rendered div. data-glass is omitted from the type.
AttributeDescription
data-glassThe preset applied to the container and inherited by its descendants.

GlassPresetScript

PropTypeDefaultDescription
storageKeystring—localStorage key read before React hydrates.
defaultPresetGlassPreset"thin"Preset used when storage and the existing attribute are empty or invalid.
noncestring—CSP nonce for the inline script.

getGlassPresetScript

Returns the inline script as a string. It accepts the same storageKey and defaultPreset options as GlassPresetScript, without nonce.

GlassPreset

type GlassPreset = "thin" | "soft" | "regular" | "thick";

Behavior

Hydration

When the provider is uncontrolled, GlassPresetProvider renders its children with defaultPreset on the server and on the first client render. After hydration it reads localStorage or the existing data-glass attribute and updates the context. This keeps server HTML and the first client render identical when a component calls useGlassPreset. Without GlassPresetScript, a persisted value is applied after hydration, so the first paint can briefly show the default material. Controlled mode uses the preset prop on both server and client; it does not replace that value with persisted state.

Persistence

  • Values are stored in localStorage under storageKey.
  • Invalid stored values are ignored.
  • Storage access is wrapped in try / catch; private mode and sandboxed frames fall back to in-memory state.
  • The provider writes the active value after hydration, including in controlled mode when storageKey is set.

Checklist

  • Import the plugin index.css after the core stylesheet.
  • Put GlassPresetScript in the document head and GlassPresetProvider at the application root.
  • Use the same storageKey and defaultPreset for the script and provider, and keep both stable.
  • Use GlassPresetScope for local overrides instead of changing the global value.
  • Test the selected preset over the busiest background in the application.