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:
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.
Set data-glass on the root or on any container:
<html data-glass="thin"><section data-glass="thick">
<Card>...</Card>
</section>| Preset | Light veil | Dark veil | Blur | Saturate | Use it for |
|---|---|---|---|---|---|
thin | 20% | 62% | 4px | 150% | Decorative surfaces over calm backgrounds |
soft | 28.3% | 65.3% | 8px | 146.7% | Secondary text and layered panels |
regular | 36.7% | 68.7% | 12px | 143.3% | A balanced choice for most interfaces |
thick | 45% | 72% | 16px | 140% | 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.
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.storageKey and defaultPreset for GlassPresetScript and
GlassPresetProvider. If they differ, the pre-hydration attribute and the
provider state can diverge and cause a flash.storageKey stable at the application root.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";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.
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.
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.
GlassPresetProvider| Prop | Type | Default | Description |
|---|---|---|---|
preset | GlassPreset | — | Controlled preset. When provided, the provider does not update its internal value; update this prop from onPresetChange. |
defaultPreset | GlassPreset | "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. |
storageKey | string | — | 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. |
children | ReactNode | — | Subtree that can read the preset with useGlassPreset. |
| Attribute | Description |
|---|---|
data-glass | Written to <html> after hydration with the active preset. GlassPresetScript may set it earlier. |
useGlassPresetReturns the provider state. It throws when called outside GlassPresetProvider.
| Field | Type | Description |
|---|---|---|
preset | GlassPreset | The active preset. |
setPreset | (preset: GlassPreset) => void | Requests a change. In controlled mode, update the preset prop from onPresetChange. |
GlassPresetScope| Prop | Type | Default | Description |
|---|---|---|---|
preset | GlassPreset | — | Preset applied as data-glass to the container. Required. |
...props | React.ComponentProps<"div"> | — | Passed to the rendered div. data-glass is omitted from the type. |
| Attribute | Description |
|---|---|
data-glass | The preset applied to the container and inherited by its descendants. |
GlassPresetScript| Prop | Type | Default | Description |
|---|---|---|---|
storageKey | string | — | localStorage key read before React hydrates. |
defaultPreset | GlassPreset | "thin" | Preset used when storage and the existing attribute are empty or invalid. |
nonce | string | — | CSP nonce for the inline script. |
getGlassPresetScriptReturns the inline script as a string. It accepts the same storageKey and
defaultPreset options as GlassPresetScript, without nonce.
GlassPresettype GlassPreset = "thin" | "soft" | "regular" | "thick";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.
localStorage under storageKey.try / catch; private mode and sandboxed frames
fall back to in-memory state.storageKey is set.index.css after the core stylesheet.GlassPresetScript in the document head and GlassPresetProvider at the
application root.storageKey and defaultPreset for the script and provider, and
keep both stable.GlassPresetScope for local overrides instead of changing the global
value.