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.
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.
| Material | Use it for |
|---|---|
| Ultra Thin | Floating decorative chrome that should barely separate from the background |
| Thin | Tab bars and toolbars that need slight separation |
| Medium | Cards, popovers, and tooltips |
| Thick | Sheets, modals, and dialogs |
| Ultra Thick | Surfaces 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.
| Token | Light | Dark |
|---|---|---|
--p-material-chrome | rgba(255,255,255,0.85) | rgba(31,31,31,0.55) |
--p-material-chrome-separator | rgba(0,0,0,0.05) | rgba(255,255,255,0.10) |
--p-material-chrome-separator-width | 0.67px | 0.67px |
--p-material-chrome-blur | 3px | 3px |
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.
| Token | Light | Dark |
|---|---|---|
--p-material-chrome-touch | rgba(255,255,255,0.85) | rgba(0,0,0,0.55) |
--p-material-chrome-touch-separator | rgba(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>
| Class | Light | Dark | Shadow |
|---|---|---|---|
.p-glass-sm | rgba(242,242,242,0.55) | rgba(153,153,153,0.17) | --p-shadow-glass-control |
.p-glass-md | rgba(245,245,245,0.72) | rgba(26,26,26,0.96) | --p-shadow-alert |
.p-glass-lg | rgba(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.
| Part | Light | Dark |
|---|---|---|
| Keyline, 0.5px outside the shape | Black at 12%, thicker at the sides | Black at 80%, thicker at the sides |
| Rim, 1px inside the top and bottom | White at 70% | White at 20% |
| Glow inside the top and bottom | A soft white band, stronger along the bottom | The 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.
| Layer | What it does |
|---|---|
| Baseline | The 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. |
| Displacement | A 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 glass | The 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.
| Approach | How it works | Support |
|---|---|---|
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 owns | The 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 WebGL | A 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.
- Reduced motion. With
prefers-reduced-motionon, the knob never lifts. The switch still toggles and slides. - Reduced transparency. With
prefers-reduced-transparencyon, the knob stays solid. Glass over a busy track is exactly the contrast problem that setting exists to prevent. - Semantics. The lens is decorative and marked
aria-hidden. The switch is still a checkbox inside its label, and the slider is still a native range input, so screen readers and keyboards see no difference. - Dragging. A switch counts a press that moves less than 6px as a tap. A drag commits the side the knob ended on and fires
inputandchangelike a click would.
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-glass | Clear .p-glass-clear | |
|---|---|---|
| Use it for | Bars, buttons, panels, anything with text | Media controls over photos and video |
| Tint, light, 64px or less across | rgba(242,242,242,0.55) | rgba(178,178,178,0.14) |
| Tint, light, larger | rgba(245,245,245,0.72) | rgba(178,178,178,0.14) |
| Tint, dark, 64px or less across | rgba(153,153,153,0.17) | rgba(178,178,178,0.14) |
| Tint, dark, larger | rgba(26,26,26,0.96) | rgba(178,178,178,0.14) |
| Frost | About 4px on a 50px surface, growing with size; 160% saturation | About 10px at every size; 120% saturation |
| Bend ramping up across the rim | 20% of the shorter side, counted up to 48px | 55% |
| Extra bend right at the edge | 70% of the shorter side, counted up to 48px | 45% |
| Rim width | 20% of the shorter side, up to 24px | 27.5% |
| Adapts to what’s behind it | Yes, when 64px or less across | No |
| 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:
- Frost comes before the bend. The blur is applied to the page first and the lens bends the blurred result, so the rim stays a crisp, compressed sliver of what’s behind the glass instead of being smeared flat. Regular frost grows with size: a 50px button blurs about 4px, and a 160px panel about 8px. Clear frosts about 10px at any size.
- Small and large glass tint differently. Apple’s small glass (buttons, tab bars, nav circles) is lighter than its cards in light mode and nearly clear in dark mode, where cards are close to opaque. The script marks each surface
data-p-glass-size="small"or"large"by the same 64px rule, and the CSS picks--p-glass-tintor--p-glass-tint-large. - Small glass adapts. A Regular surface 64px or less across, like a button or a tab bar, turns into dark glass over dark content and light glass over light content, whatever the page’s appearance. The script sets
data-p-themeon the surface, so its labels flip with it. Larger surfaces keep the page’s appearance. Apple does the same, because a whole panel flipping would be distracting. Set--p-glass-adaptive: 0on a surface to keep it fixed, or give it your owndata-p-theme.
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.
| Layer | What draws it |
|---|---|
| Frost and refraction | A 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. |
| Smoothing | The surface’s ::before layer, a light backdrop-filter blur over the canvas. |
| Tint | The same layer’s background: --p-glass-tint on small surfaces, --p-glass-tint-large on larger ones. |
| Edge | The 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.
| Property | Default | What it does |
|---|---|---|
--p-glass-tint | See Variants | Color laid over the refraction on surfaces 64px or less across |
--p-glass-tint-large | See Variants | Color laid over the refraction on larger surfaces |
--p-glass-frost | 3.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-grow | 1 (Clear 0) | Whether the frost grows with the surface’s size |
--p-glass-blur | 1px (Clear 2px) | Light smoothing on top of the canvas, also scaled by --p-glass-scale |
--p-glass-saturate | 160% | Color boost under the frost |
--p-glass-rim | --p-glass-edge | The keyline and rim. See The glass edge. |
--p-glass-refraction | 0.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-depth | 0.7 (Clear 0.45) | Extra bend right at the edge, same unit |
--p-glass-bevel-width | 0.2 (Clear 0.275) | How far in from the edge the rim bends, up to 24px |
--p-glass-specular | 0 | Two fixed rim highlights, at the top left and bottom right, on (1) or off (0) |
--p-glass-adaptive | 1 | Small Regular surfaces flip light or dark to suit the backdrop (1), or keep the page’s appearance (0) |
--p-glass-magnify | 1 | Zoom of the content under the glass |
--p-glass-surface | --p-glass-md | The plain material shown before and instead of glass |
--p-glass-z | 1 | The 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.
| Value | Meaning |
|---|---|
| none | Not enhanced. The surface is its plain material. |
pending | Enhanced and waiting for the first snapshot, usually under 100ms. The material shows on the ::before layer, so nothing flashes. |
live | Refracting. The material swaps for the tint in 200ms. |
off | Left 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.
- One layer per page. Every surface has to share one
z-index,--p-glass-z(default1). A fixed tab bar sits at70, so on a page that also has inline glass, set--p-glass-z: 70on them. Otherwise the canvas covers their content, and the script warns in the console. - No stacking contexts above it. A surface inside an ancestor with a
transform,filter,opacity, az-index, or similar stays a plain material. The script checks, marks itoff, and warns. - Fixed elements are not captured. A fixed header does not show up in the glass beneath it.
- Size. Safari can turn unstable once a single surface covers about half the viewport. Keep glass to chrome.
- Bend on long pages. Glassworks bends in snapshot coordinates. Puppertino converts against the snapshot’s longer side, so on a tall page the side edges bend a little less than the top and bottom.
data-p-glass-backdropevens it out.
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
- Reduced transparency, increased contrast, and forced colors. The script never starts, and every surface stays its plain material. Under reduced transparency that material turns nearly opaque. Glass over a busy backdrop is exactly the contrast problem those settings exist to prevent.
- Reduced motion. Nothing on the glass moves on its own, so it stays as it is.
- Contrast. The frost and tint of Regular glass carry labels over most backdrops, but the backdrop decides. Check text against the busiest spot behind it and aim for 4.5:1. Small dark glass is nearly clear, so its white labels lean on dark content behind it; it turns light only over content brighter than 85% luminance. Clear glass guarantees nothing, so reserve it for white icons over dimmed media.
- Semantics. Glass is paint. The canvas is decorative, and the surface keeps its role, its label, its focus ring, and its 44px target.
- Without WebGL, a capture engine, or the script. Every surface stays its material. Nothing breaks.
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.
| Class | Shadow value | Use for |
|---|---|---|
.p-elevation-1 | 0 1px 3px rgba(0,0,0,0.12) | Cards, raised buttons |
.p-elevation-2 | 0 4px 12px rgba(0,0,0,0.15) | Popovers, tooltips |
.p-elevation-3 | 0 8px 30px rgba(0,0,0,0.20) | Modals, sheets |
.p-elevation-4 | 0 16px 48px rgba(0,0,0,0.25) | Context menus |
.p-elevation-window | Multi-layer | Floating 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.
| Token | Light | Dark | Used by |
|---|---|---|---|
--p-shadow-overlay | 0 8px 48px rgba(0,0,0,0.25) | 45% | Menus, notifications, popovers |
--p-shadow-alert | 0 18px 48px rgba(0,0,0,0.25) | 45% | Alerts, medium and large glass |
--p-shadow-window | 0 18px 54px rgba(0,0,0,0.30), 0 0 1px rgba(0,0,0,0.8) | 57%, same hairline | Active windows, dialogs, sheets |
--p-shadow-window-inactive | 0 7px 21px rgba(0,0,0,0.20), 0 0 1px rgba(0,0,0,0.8) | 35%, same hairline | Inactive windows |
--p-shadow-glass-control | 0 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.