macbetter

Status

Glass

Translucent layers that let content show through, so chrome floats above it.

ultrathinOver media, where the content is the point
thinSmall floating panels and HUDs
regularMenus, popovers and most surfaces
thickSheets and inspectors over busy content
chromeToolbars, tab bars and title bars
sidebarSource lists beside a window's content
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.

MaterialUse it for
ultrathinControls over photos and video, where you want the content to show.
thinSmall floating panels and HUDs.
regularMenus, popovers and most surfaces. The default.
thickSheets and inspectors over busy content, and anywhere with long text.
chromeToolbars, tab bars and title bars: the window's own background, slightly translucent.
sidebarSource 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.

Yosemite ValleySunrise 6:48 AM · 54°
Now Playing
Clear
Tinted
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 Canvas with a CanvasText border.
  • 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 GlassGroup is a group. Give it an aria-label that names the cluster, and name every button in it.

API

Material

PropTypeDefaultDescription
material"ultrathin" | "thin" | "regular" | "thick" | "chrome" | "sidebar""regular"How much of what's behind shows through.
renderReactElement | (props) => ReactElement—Renders a different element with the material's props.

Glass

PropTypeDefaultDescription
variant"regular" | "clear" | "tinted""regular"`clear` for glass over media; `tinted` carries the accent.
shape"rounded" | "capsule" | "circle""rounded"The pane's shape.
interactiveboolean—Responds to hover and press. Glass rendered as a button or link does this on its own.
renderReactElement | (props) => ReactElement—Renders a different element, such as a button.

GlassGroup

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Which way the controls run.
variant"regular" | "clear""regular"The glass.
aria-labelstring—Names the cluster.

Classes and tokens

The classes work without the components, in any markup.

Class, attribute or tokenDescription
.mb-material with data-materialA material pane.
.mb-glass with data-variant, data-shape, data-interactiveA Liquid Glass pane.
--mb-material-ultrathin … --mb-material-sidebarEach material's fill, for component styles.
--mb-material-filterThe backdrop filter every material uses.
--mb-glass-rim, --mb-glass-edge, --mb-glass-borderThe bright top edge and hairline border.
--mb-glass-fill, --mb-glass-filter, --mb-glass-radius, --mb-glass-shadowSet on a .mb-glass element to adjust one pane.
--mb-material-scrimSet on a .mb-material element: the dimming layer under a dark material. Transparent in light mode.