Foundations

Materials

A material is a surface that blurs and tints the content behind it. Apple uses materials for navigation bars, sheets, popovers, and any element that should feel like it sits on top of the rest of the interface without hiding it completely.

Puppertino ships five thickness levels that match Apple’s macOS 27 specification. Each level adapts to light and dark mode, respects reduced transparency, and falls back gracefully when the browser does not support backdrop-filter.

Thickness levels

Five materials, ordered from most see-through to most opaque. Pick the one that matches the importance of the surface. Thinner materials work for light decorative chrome; thicker materials work for surfaces that hold critical content.

Ultra Thin0.36 / 0.10
Thin0.48 / 0.20
Medium0.60 / 0.29
Thick0.72 / 0.40
Ultra Thick0.88 / 0.50

The two opacity values are light mode and dark mode respectively. Light materials are #F6F6F6 at each opacity, except Ultra Thick, which is a slightly darker #ECECEC at 88%. Dark materials are black. Note that dark mode materials are more transparent than light mode materials, in line with the Liquid Glass direction: materials should let more of the background through.

Choosing a thickness

Match the material to the size of the surface and the content it carries.

MaterialUse it for
Ultra ThinFloating decorative chrome that should barely separate from the background
ThinTab bars and toolbars that need slight separation
MediumCards, popovers, and tooltips
ThickSheets, modals, and dialogs
Ultra ThickSurfaces that hold critical content over a busy background

How to use materials

Apply a material class to any element. The class handles the blur, the saturation, and the background fill in one step.

<nav class="p-material-thin">Tab bar content</nav>
<aside class="p-material-medium">Sidebar content</aside>
<div class="p-material-thick">Sheet content</div>

The p-material class on its own applies the blur and saturation without setting a background color. Combine it with a custom background when you need full control.

<div class="p-material" style="background: rgba(0, 100, 255, 0.4);">
  Custom tinted material
</div>

Chrome

Chrome is the material for bars that sit on a window edge: title bars, toolbars, and the band at the top of a scroll view. It is nearly opaque and barely blurred, so content that scrolls under it fades rather than smears. A single hairline separates it from the content.

TokenLightDark
--p-material-chromergba(255,255,255,0.85)rgba(31,31,31,0.55)
--p-material-chrome-separatorrgba(0,0,0,0.05)rgba(255,255,255,0.10)
--p-material-chrome-separator-width0.67px0.67px
--p-material-chrome-blur3px3px

The .p-material-chrome class sets the fill and blur. Draw the separator yourself on the edge that meets the content, so a top bar and a bottom bar can share the class.

<header class="p-material-chrome"
  style="box-shadow: inset 0 -0.67px 0 var(--p-material-chrome-separator);">
  Title
</header>

Touch chrome

iOS bars use a different dark chrome. It darkens the content with black instead of laying a dark gray over it, and its hairline is black, not white. Light mode is the same as macOS. Use .p-material-chrome-touch for touch nav bars, toolbars, and scroll edges.

TokenLightDark
--p-material-chrome-touchrgba(255,255,255,0.85)rgba(0,0,0,0.55)
--p-material-chrome-touch-separatorrgba(0,0,0,0.05)rgba(0,0,0,0.10)

Glass components

Three preset glass surfaces sized for common UI elements.

<button class="p-glass-sm">Pill button</button>
<div class="p-glass-md">Card</div>
<div class="p-glass-lg">Window or sheet</div>
ClassLightDarkShadow
.p-glass-smrgba(242,242,242,0.55)rgba(153,153,153,0.17)--p-shadow-glass-control
.p-glass-mdrgba(245,245,245,0.72)rgba(26,26,26,0.96)--p-shadow-alert
.p-glass-lgrgba(245,245,245,0.72)rgba(26,26,26,0.96)--p-shadow-alert

Small glass lifts a mid-gray backdrop and dims white a little, so a pill reads on both: over #666 it lands at #B3, over white at #F8. Medium and large glass share one recipe, lighter than small glass (#CD over #666). In dark mode the two sizes split further. Small glass is nearly clear: it pulls whatever sits behind it 17% toward #999. Medium and large glass is close to opaque #1A, so a card reads as a solid dark layer over any content.

On iOS, glass cards (sheets, alerts, popovers) cast --p-shadow-overlay, which sits 8px below the card. macOS cards use --p-shadow-alert, 18px below.

Every preset carries the glass edge, --p-glass-edge, and a sensible border radius (24px for small, 14px for medium, 10px for large) so the surface looks finished without extra styling.

The glass edge

The edge is what makes glass read as a separate pane, even on a white page. It is a list of box shadows.

PartLightDark
Keyline, 0.5px outside the shapeBlack at 12%, thicker at the sidesBlack at 80%, thicker at the sides
Rim, 1px inside the top and bottomWhite at 70%White at 20%
Glow inside the top and bottomA soft white band, stronger along the bottomThe same, fainter

The sides get the keyline only. Use the token on your own glass surfaces.

.my-panel {
  background: var(--p-glass-md);
  backdrop-filter: saturate(180%) blur(30px);
  box-shadow: var(--p-glass-edge), var(--p-shadow-alert);
}

With increased contrast on, the keyline becomes a solid 1px line at 45%.

Liquid Glass

Liquid Glass is the material Apple introduced across its platforms in 2025. It bends the content behind it the way a real lens does, instead of only blurring it. The web has no native version, so there are two ways to approximate it: faux Liquid Glass and real-time Liquid Glass. Puppertino ships both. Switch knobs and slider thumbs get the faux lens below, and bars, buttons, and panels get Puppertino Glass, which refracts the real page.

<label class="p-switch" aria-label="Wi-Fi">
<input type="checkbox" checked />
<span></span>
</label>

<script src="src/js/liquid_glass.js"></script>

Press and hold a switch, then drag across it. The knob lifts into glass, and the track bends through it. Drag a slider and its thumb lifts into clear glass that shows the track through it. Release and the glass settles back into a solid knob.

Faux Liquid Glass

Faux Liquid Glass never looks at the page behind it. A switch knob always sits on its own track, and a slider thumb always sits on its fill. Because the content under the lens is known, the lens can redraw that content inside itself, and nobody can tell it is a copy.

It comes in two looks. Every switch and slider uses the liquid lens by default: the track is cut out under the glass and redrawn inside it. A switch magnifies and bends its copy. A slider shows it at its real size and in focus, with the fill color wrapped around the rim. Add data-p-lens="clear" to a switch’s label or a slider input for the flatter macOS 27 and iOS 27 look instead: a nearly transparent body, a hairline rim, and a soft shadow below, with the track showing through unmagnified. The rest of this section describes the liquid lens.

The liquid lens is three layers stacked inside the control.

LayerWhat it does
BaselineThe track itself. On a switch, a capsule is cut out of it wherever the lens sits, so the page shows through the glass rather than the flat track. The cut is a CSS mask that grows with the lens.
DisplacementA copy of the track inside the lens, scaled up 1.65x and run through an SVG goo filter. The filter blurs the copy, then crushes its alpha (16a − 10) so the edges re-harden. The track color fuses into a rim at the glass edge, which reads as refraction.
Knockout and glassThe resting knob fades out as the lens lifts, and a stack of 11 shadows draws the glass: white specular edges, a dark inner shade for curvature, and a soft cast shadow. An SVG filter splits red and blue a fraction of a pixel apart for faint chromatic aberration.

Every value scales with the lens. The reference is a 50px lens with a 2px goo blur and a 0.8px color split; a 24px touch knob gets half of each. The SVG filter values are absolute, so the script builds a set per lens height rather than letting the rim blur away at small sizes.

One number drives all of it. --p-lens-lift runs from 0, a plain knob, to 1, fully lifted glass. The scale, the blur that sharpens the lens as it lifts, the cover fade, and the rim all read from it, so they can never fall out of step. It moves over 350ms on a spring sampled from a lightly damped oscillator, with a 3% overshoot as it lands. Browsers without linear() easing get cubic-bezier(0.32, 1.2, 0.36, 1).

Because nothing samples the backdrop, faux Liquid Glass works in Safari, Chrome, and Firefox alike. Consider it for small controls whose surroundings you control.

The original implementation of this effect is by Jhey Tompkins. Puppertino adapts it to real form controls: the switch stays a native checkbox, so forms, labels, and keyboard toggling keep working.

How to use it

Load the script once. It adds the lens to every .p-switch on the page and to every .p-slider while it is dragged. The markup does not change.

<script src="src/js/liquid_glass.js"></script>

Switches added after the page loads need one call.

PuppertinoLiquidGlass.enhance(container);

Without the script, switches keep their CSS lens morph and sliders keep a solid thumb. Nothing breaks.

Real-time Liquid Glass

Real-time Liquid Glass refracts whatever is actually behind the element: text, photos, video, other controls. It is what Apple renders on toolbars, tab bars, and sheets. Every version of it on the web runs on a displacement map, an image whose red and green channels say how far to shift each pixel horizontally and vertically. Built from a rounded-rectangle distance field and Snell’s law, it bends content inward at the edges like a convex lens. The derivation is in specs/liquid-glass.md.

What differs is where that map gets applied, and that decides which browsers keep the effect.

ApproachHow it worksSupport
backdrop-filter: url(#filter)An SVG feDisplacementMap bends the page behind the element. No extra code and no copies of the content.Chromium only. Safari and Firefox drop the declaration and the refraction disappears.
A plain filter on content the component ownsThe same primitive, applied to a layer the component itself renders, like a copy of a track fill. The page behind is untouched.Every modern engine.
Capture the DOM, refract it in WebGLA library snapshots the page (snapDOM, or html2canvas), uploads it as a texture, and a shader does the refraction, chromatic aberration, and specular highlights.Every WebGL browser, including Safari and Firefox.

The second row is what the faux lens uses, because a control always knows what sits under it. Aave documented it in Building Glass for the Web, and their switch and slider work the way Puppertino’s do: the glass refracts a copy of the track fill rather than the page. Their write-up is also the best list of Safari’s quirks, including filter output being cached by filter ID and live <video> never reaching the filter pipeline.

The third row is what a surface needs, because a tab bar or a floating button can sit over anything. That is Puppertino Glass.

Accessibility of the lens

The faux lens Puppertino ships steps aside whenever it would get in the way.

Puppertino Glass

Puppertino Glass refracts the real page behind a surface. Content bends at the rim like it would through a lens, frosts under the middle, and keeps moving as the page scrolls. It is Apple’s Liquid Glass for bars, buttons, and panels, in Safari, Chrome, Firefox, and Edge.

<nav class="p-tab-bar p-tab-bar-fixed" aria-label="Sections">
<a class="p-tab-bar__tab" href="/" aria-current="page">
  <i class="ph-bold ph-house" aria-hidden="true"></i><span>Home</span>
</a>
<a class="p-tab-bar__tab" href="/search/">
  <i class="ph-bold ph-magnifying-glass" aria-hidden="true"></i><span>Search</span>
</a>
</nav>

<button class="p-glass" type="button">Share</button>
<button class="p-glass-clear" type="button" aria-label="Play">…</button>

<script src="https://cdn.jsdelivr.net/npm/@zumer/snapdom@3.1.0/dist/snapdom.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@codedgar/glassworks@2.0.0/dist/glassworks.umd.min.js"></script>
<script src="src/js/glass.js"></script>

Drag the pill across the photo. The dog’s outline bends at the pill’s rim, and the middle of the pill frosts it without bending it. The corner toggle flips the page between light and dark while the photo stays the same. The pill and the Clear button stay small enough to adapt on their own, so over the pale ground the pill may keep a light tint in dark mode, the way Apple’s buttons do.

When to use it

Use Puppertino Glass for the chrome that floats over content: tab bars, toolbars, floating buttons, and media controls. Apple reserves Liquid Glass for this navigation layer. Content itself stays on solid backgrounds, so the glass reads as a separate plane.

Avoid stacking glass on glass. Each surface refracts a snapshot of the page taken without any glass in it, so glass inside glass shows the page, not the outer pane. Apple forbids it for the same reason.

For surfaces that should blur without bending, like sheets and sidebars, consider a thickness level instead. It costs no script and no snapshot.

Variants

Two variants, after Apple’s.

Regular .p-glassClear .p-glass-clear
Use it forBars, buttons, panels, anything with textMedia controls over photos and video
Tint, light, 64px or less acrossrgba(242,242,242,0.55)rgba(178,178,178,0.14)
Tint, light, largerrgba(245,245,245,0.72)rgba(178,178,178,0.14)
Tint, dark, 64px or less acrossrgba(153,153,153,0.17)rgba(178,178,178,0.14)
Tint, dark, largerrgba(26,26,26,0.96)rgba(178,178,178,0.14)
FrostAbout 4px on a 50px surface, growing with size; 160% saturationAbout 10px at every size; 120% saturation
Bend ramping up across the rim20% of the shorter side, counted up to 48px55%
Extra bend right at the edge70% of the shorter side, counted up to 48px45%
Rim width20% of the shorter side, up to 24px27.5%
Adapts to what’s behind itYes, when 64px or less acrossNo
Before the first frame--p-glass-md material (--p-glass-sm when 64px or less across)--p-glass-sm material

Regular is the default. Its frost and tint keep labels readable over busy content. Clear is barely tinted and looks the same in light and dark, so it lets the color of the backdrop through. It’s frosted, not crisp: Apple’s Clear blurs more than Regular. White icons on it still need a dim photo or video behind them, which Apple calls a dimming layer. Avoid Clear over text or light backgrounds. Nothing guarantees contrast there.

The values come from measuring Apple’s own glass (NSGlassEffectView on macOS 26) over black, white, text, stripes, and a photo, and matching Puppertino to it. Two things behave as Apple’s do:

The bend follows the edge. Near the rim, content shifts straight toward the centre, at right angles to the nearest edge, so straight lines behind the glass stay straight until they reach the rim. It stops growing past 48px because Apple’s does: a 50pt button and a 74pt bar both shift content by a similar amount at the rim. The middle of the pane isn’t bent or magnified at all.

The tab bar is Regular glass. It starts from its own surface, --p-tab-bar-surface, rather than --p-glass-md.

Anatomy

Glass is four layers, bottom to top.

LayerWhat draws it
Frost and refractionA WebGL canvas under the surface. It blurs a snapshot of the page, then bends it inside a band around the rim, toward the centre, so the edge shows a compressed sliver of what’s behind the glass. The middle is left flat.
SmoothingThe surface’s ::before layer, a light backdrop-filter blur over the canvas.
TintThe same layer’s background: --p-glass-tint on small surfaces, --p-glass-tint-large on larger ones.
EdgeThe same layer’s shadows, --p-glass-rim: a dark 0.5px keyline outside, and an equally bright 1px rim inside the top and the bottom.

Your content sits above all four. Every value is a custom property, so a surface can be tuned in CSS.

PropertyDefaultWhat it does
--p-glass-tintSee VariantsColor laid over the refraction on surfaces 64px or less across
--p-glass-tint-largeSee VariantsColor laid over the refraction on larger surfaces
--p-glass-frost3.8 (Clear 10.5)Blur in CSS px, applied before the bend. Regular multiplies it by --p-glass-scale, which the script sets from the surface’s size
--p-glass-frost-grow1 (Clear 0)Whether the frost grows with the surface’s size
--p-glass-blur1px (Clear 2px)Light smoothing on top of the canvas, also scaled by --p-glass-scale
--p-glass-saturate160%Color boost under the frost
--p-glass-rim--p-glass-edgeThe keyline and rim. See The glass edge.
--p-glass-refraction0.2 (Clear 0.55)Bend that ramps up across the rim, as a fraction of the shorter side (counted up to 48px)
--p-glass-bevel-depth0.7 (Clear 0.45)Extra bend right at the edge, same unit
--p-glass-bevel-width0.2 (Clear 0.275)How far in from the edge the rim bends, up to 24px
--p-glass-specular0Two fixed rim highlights, at the top left and bottom right, on (1) or off (0)
--p-glass-adaptive1Small Regular surfaces flip light or dark to suit the backdrop (1), or keep the page’s appearance (0)
--p-glass-magnify1Zoom of the content under the glass
--p-glass-surface--p-glass-mdThe plain material shown before and instead of glass
--p-glass-z1The layer every surface shares. See Limits.

Bends are relative to the surface up to 48px, so a 44px button bends a little less than a 62px tab bar, and anything bigger bends like the tab bar. The glass inherits the surface’s border-radius, so set one. A capsule is border-radius: 999px.

How to use it

Load a capture engine, Glassworks, and the script, in that order. Every .p-glass, .p-glass-clear, and .p-tab-bar on the page turns into glass.

<script src="https://cdn.jsdelivr.net/npm/@zumer/snapdom@3.1.0/dist/snapdom.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@codedgar/glassworks@2.0.0/dist/glassworks.umd.min.js"></script>
<script src="src/js/glass.js"></script>

With a bundler, install Glassworks 2 and a capture engine, then hand the library over instead of relying on globals.

npm i @codedgar/glassworks @zumer/snapdom
import glassworks, { createSnapdomEngine } from '@codedgar/glassworks';
import { snapdom } from '@zumer/snapdom';
import '@codedgar/puppertino/js/glass.js';

PuppertinoGlass.use(glassworks, { engine: createSnapdomEngine(snapdom) });

Surfaces added after the page loads need one call. Opt a surface out with data-p-glass="off", and it keeps its material.

PuppertinoGlass.enhance(container);

Glass is a snapshot of the page, not a live view. Scrolling and moving the glass update in real time, and video is picked up automatically. When the content behind the glass changes, like a new list or a swapped image, take a new snapshot.

PuppertinoGlass.refresh();

Appearance changes recapture on their own, from the system setting, .p-dark-mode, .p-light-mode, or data-p-theme.

By default the snapshot covers the whole <body>. On a long page, mark the region the glass sits over with data-p-glass-backdrop. A smaller snapshot captures faster and uses less GPU memory. Every glass surface on the page has to sit inside it.

<main data-p-glass-backdrop>…</main>

States

The script marks each surface with data-p-glass.

ValueMeaning
noneNot enhanced. The surface is its plain material.
pendingEnhanced and waiting for the first snapshot, usually under 100ms. The material shows on the ::before layer, so nothing flashes.
liveRefracting. The material swaps for the tint in 200ms.
offLeft alone, set by you or because the glass could not render there.

It also sets data-p-glass-size to small (64px or less across) or large, and updates it when the window resizes. That attribute picks the tint.

Interaction states belong to the component. The tab bar keeps its 50% opacity press and its focus ring, and a glass button keeps whatever you give it. Glassworks turns off pointer events on the surface. Puppertino turns them back on, so glass still takes clicks.

Limits

Glassworks draws every surface on one canvas, placed just under the highest surface. That shapes where glass can go.

Dark mode

The refraction is the page itself, so it follows whatever the page does. Small glass goes from 55% #F2F2F2 to 17% #999999, nearly clear, so a dark button takes its tone from the content behind it. Larger glass goes from 72% #F5F5F5 to 96% #1A1A1A, close to opaque, so a dark card never drops below about #1A and stays a quiet surface over bright content. The rim drops to 20% white and the keyline darkens to 80% black. Clear glass keeps 14% #B2B2B2 in both. The snapshot retakes itself when the appearance changes, so the glass never shows the old scheme.

Accessibility

Glassworks is by the author of Puppertino, a fork of liquidGL by NaughtyDuk, whose WebGL renderer it keeps. It adds snapDOM captures, about four times faster than html2canvas, and a fix for lenses drifting on long pages. Puppertino Glass is the layer on top: the variants, the tokens, the material hand-off, and the accessibility behavior.

Shadows

Five elevation levels for depth that does not rely on translucency. Use these when you need a defined edge or when materials are not appropriate.

Elevation 1
Elevation 2
Elevation 3
Elevation 4
Window
ClassShadow valueUse for
.p-elevation-10 1px 3px rgba(0,0,0,0.12)Cards, raised buttons
.p-elevation-20 4px 12px rgba(0,0,0,0.15)Popovers, tooltips
.p-elevation-30 8px 30px rgba(0,0,0,0.20)Modals, sheets
.p-elevation-40 16px 48px rgba(0,0,0,0.25)Context menus
.p-elevation-windowMulti-layerFloating windows
<div class="p-elevation-1">Subtle lift</div>
<div class="p-elevation-3">Modal-level depth</div>

Shadow tokens

Components share five shadow tokens. They follow the appearance, and dark mode makes every one of them heavier, because a dark shadow has less to darken.

TokenLightDarkUsed by
--p-shadow-overlay0 8px 48px rgba(0,0,0,0.25)45%Menus, notifications, popovers
--p-shadow-alert0 18px 48px rgba(0,0,0,0.25)45%Alerts, medium and large glass
--p-shadow-window0 18px 54px rgba(0,0,0,0.30), 0 0 1px rgba(0,0,0,0.8)57%, same hairlineActive windows, dialogs, sheets
--p-shadow-window-inactive0 7px 21px rgba(0,0,0,0.20), 0 0 1px rgba(0,0,0,0.8)35%, same hairlineInactive windows
--p-shadow-glass-control0 8px 15px rgba(0,0,0,0.02)4%Small glass buttons, toolbar groups
.my-menu { box-shadow: var(--p-shadow-overlay); }

Accessibility

Materials are beautiful, but they trade contrast for translucency. Three accessibility paths are built in.

Reduced transparency. When the user has prefers-reduced-transparency enabled, all materials switch to opaque backgrounds. Text legibility wins over the visual effect.

Browsers without backdrop-filter. Older browsers fall back to a near-opaque background so the surface stays visible.

Increased contrast. With prefers-contrast: more, the glass keyline becomes a solid 1px line, and the chrome separator darkens.

Forced colors. Shadows are dropped in forced-colors mode, so glass and chrome surfaces get a 1px outline in the system text color.

Contrast checks. Always verify that text on top of a material meets at least 4.5:1 against the underlying content. If you cannot guarantee the background, use a thicker material or fall back to a solid color.

Backwards compatibility

The legacy .p-shadow-1 through .p-shadow-4, .p-to-shadow-1 through .p-to-shadow-4, and .p-blur-1 through .p-blur-4 classes from earlier Puppertino versions remain available. Pages that use them do not need to change.

For new work, prefer the .p-material-* and .p-elevation-* classes. They follow the macOS 27 specification and adapt automatically to dark mode and accessibility preferences.

Copied