Status
Glass
Translucent layers that let content show through, so chrome floats above it.
import { Material, type MaterialThickness } from "macbetter/glass"
const materials: Array<{ material: MaterialThickness; use: string }> = [
{ material: "ultrathin", use: "Over media, where the content is the point" },
{ material: "thin", use: "Small floating panels and HUDs" },
{ material: "regular", use: "Menus, popovers and most surfaces" },
{ material: "thick", use: "Sheets and inspectors over busy content" },
{ material: "chrome", use: "Toolbars, tab bars and title bars" },
{ material: "sidebar", use: "Source lists beside a window's content" },
]
export default function Example() {
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
gap: 14,
width: "100%",
padding: 20,
borderRadius: 18,
backgroundImage: "url(/demos/wallpaper-coast.svg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
{materials.map(({ material, use }) => (
<Material
key={material}
material={material}
style={{ display: "grid", gap: 4, padding: 16, borderRadius: 14, minHeight: 104, alignContent: "end" }}
>
<span style={{ fontWeight: 600, textTransform: "capitalize" }}>{material}</span>
<span style={{ color: "var(--mb-label-secondary)" }}>{use}</span>
</Material>
))}
</div>
)
}When to use it
Apple uses translucency for the layer that sits above content: bars, sidebars, menus, popovers, sheets, and floating controls. It keeps people oriented, because they can see what's underneath, while the chrome stays out of the way.
macbetter has two kinds:
- Materials are frosted panes for surfaces: a toolbar, a sidebar, a popover, a sheet. They blur heavily and come in thicknesses.
- Glass is Liquid Glass, for controls that float over content: a tab bar, a capsule of toolbar buttons, a floating action. It blurs less and bends light at its edges, with a bright rim and an inner glow.
Never put one translucent layer on another. Blur on blur muddies both, and text on the top layer loses its contrast. A menu over a glass toolbar is fine, because the menu is a separate layer above it; a glass button inside a material sidebar is not. Inside glass, use plain controls.
Choosing a material
Thicker materials hide more of what's behind them. Use a thicker one for bigger surfaces and for anything with a lot of text, and a thinner one where the content behind is the point.
| Material | Use it for |
|---|---|
ultrathin | Controls over photos and video, where you want the content to show. |
thin | Small floating panels and HUDs. |
regular | Menus, popovers and most surfaces. The default. |
thick | Sheets and inspectors over busy content, and anywhere with long text. |
chrome | Toolbars, tab bars and title bars: the window's own background, slightly translucent. |
sidebar | Source lists beside a window's content, tinted like the desktop. |
Usage
import { Glass, GlassGroup, Material } from "macbetter/glass"Material is a div with the mb-material class and data-material. Use render to make it another element, such as <nav /> or <aside />. Component styles use the same tokens directly: background: var(--mb-material-regular) with backdrop-filter: var(--mb-material-filter).
Liquid Glass
Glass is a rounded pane by default. Set shape to capsule or circle. variant="clear" lets more through, for glass over photos and video; keep what's on it bold. variant="tinted" carries the accent, for the one prominent floating action.
import { Glass } from "macbetter/glass"
import { PauseIcon, SkipBackIcon, SkipForwardIcon } from "lucide-react"
export default function Example() {
return (
<div
style={{
display: "grid",
placeItems: "center",
gap: 16,
width: "100%",
padding: "40px 20px",
borderRadius: 18,
backgroundImage: "url(/demos/wallpaper-valley.svg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<Glass style={{ display: "grid", gap: 2, padding: "14px 18px", width: 280 }}>
<span style={{ fontWeight: 600 }}>Yosemite Valley</span>
<span style={{ color: "var(--mb-label-secondary)" }}>Sunrise 6:48 AM · 54°</span>
</Glass>
<Glass shape="capsule" style={{ display: "flex", alignItems: "center", gap: 14, padding: "10px 20px" }}>
<SkipBackIcon aria-hidden="true" size={20} />
<PauseIcon aria-hidden="true" size={24} />
<SkipForwardIcon aria-hidden="true" size={20} />
<span style={{ fontWeight: 600 }}>Now Playing</span>
</Glass>
<div style={{ display: "flex", gap: 12 }}>
<Glass variant="clear" shape="capsule" style={{ padding: "8px 16px", fontWeight: 600 }}>
Clear
</Glass>
<Glass variant="tinted" shape="capsule" style={{ padding: "8px 16px", fontWeight: 600 }}>
Tinted
</Glass>
</div>
</div>
)
}Floating clusters
GlassGroup is one capsule of glass around a cluster of plain buttons, like a toolbar's trailing actions or a map's zoom controls. Group related actions so they share one piece of glass, rather than giving each its own. A vertical Separator divides groups within it.
Glass that is itself a control, like a floating location button, takes interactive or renders as a button: it brightens under the pointer and glows from inside when pressed.
"use client"
import { Button } from "macbetter/button"
import { Glass, GlassGroup } from "macbetter/glass"
import { Separator } from "macbetter/separator"
import { LocateFixedIcon, MinusIcon, PlusIcon, ShareIcon, StarIcon, Trash2Icon } from "lucide-react"
export default function Example() {
return (
<div
style={{
position: "relative",
width: "100%",
height: 300,
borderRadius: 18,
overflow: "hidden",
backgroundImage: "url(/demos/wallpaper-meadow.svg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<GlassGroup aria-label="Photo actions" style={{ position: "absolute", insetInlineStart: 16, insetBlockStart: 16 }}>
<Button variant="plain" tone="neutral" shape="circle" aria-label="Share">
<ShareIcon aria-hidden="true" />
</Button>
<Button variant="plain" tone="neutral" shape="circle" aria-label="Favorite">
<StarIcon aria-hidden="true" />
</Button>
<Separator orientation="vertical" />
<Button variant="plain" tone="neutral" shape="circle" aria-label="Delete">
<Trash2Icon aria-hidden="true" />
</Button>
</GlassGroup>
<GlassGroup
aria-label="Zoom"
orientation="vertical"
style={{ position: "absolute", insetInlineEnd: 16, insetBlockStart: 16 }}
>
<Button variant="plain" tone="neutral" shape="circle" aria-label="Zoom in">
<PlusIcon aria-hidden="true" />
</Button>
<Separator />
<Button variant="plain" tone="neutral" shape="circle" aria-label="Zoom out">
<MinusIcon aria-hidden="true" />
</Button>
</GlassGroup>
<Glass
shape="circle"
interactive
render={<button type="button" aria-label="Show my location" />}
style={{
position: "absolute",
insetInlineEnd: 16,
insetBlockEnd: 16,
display: "grid",
placeItems: "center",
width: 48,
padding: 0,
color: "var(--mb-accent-text)",
}}
>
<LocateFixedIcon aria-hidden="true" size={22} />
</Glass>
</div>
)
}Platform behavior
The materials are the same on every platform. Interactive glass flexes on iOS and iPadOS: it swells slightly under a finger, the way Liquid Glass responds to touch. On macOS it brightens on hover and glows a little more on press instead, and never scales.
Accessibility
- Reduce Transparency turns every material and glass surface opaque: the blur and the highlights go, and the edge stays so the shape still reads.
- Increase Contrast makes them opaque too, and replaces the specular rim with a real edge in the control border color.
- In forced colors mode, they become plain
Canvaswith aCanvasTextborder. - In dark mode a material also dims what's behind it, as Apple's do, so labels stay legible over bright content.
- Text on glass uses the label tokens, which hold their contrast against the material, not against the wallpaper. Check text on clear glass against the content you'll actually put behind it.
- A
GlassGroupis agroup. Give it anaria-labelthat names the cluster, and name every button in it.
API
Material
| Prop | Type | Default | Description |
|---|---|---|---|
material | "ultrathin" | "thin" | "regular" | "thick" | "chrome" | "sidebar" | "regular" | How much of what's behind shows through. |
render | ReactElement | (props) => ReactElement | — | Renders a different element with the material's props. |
Glass
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "regular" | "clear" | "tinted" | "regular" | `clear` for glass over media; `tinted` carries the accent. |
shape | "rounded" | "capsule" | "circle" | "rounded" | The pane's shape. |
interactive | boolean | — | Responds to hover and press. Glass rendered as a button or link does this on its own. |
render | ReactElement | (props) => ReactElement | — | Renders a different element, such as a button. |
GlassGroup
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Which way the controls run. |
variant | "regular" | "clear" | "regular" | The glass. |
aria-label | string | — | Names the cluster. |
Classes and tokens
The classes work without the components, in any markup.
| Class, attribute or token | Description |
|---|---|
.mb-material with data-material | A material pane. |
.mb-glass with data-variant, data-shape, data-interactive | A Liquid Glass pane. |
--mb-material-ultrathin … --mb-material-sidebar | Each material's fill, for component styles. |
--mb-material-filter | The backdrop filter every material uses. |
--mb-glass-rim, --mb-glass-edge, --mb-glass-border | The bright top edge and hairline border. |
--mb-glass-fill, --mb-glass-filter, --mb-glass-radius, --mb-glass-shadow | Set on a .mb-glass element to adjust one pane. |
--mb-material-scrim | Set on a .mb-material element: the dimming layer under a dark material. Transparent in light mode. |