Liquid Glass for React and Next.js

Apple-inspired glass for the web, with refraction and spring-based touch interactions. Built by Zaki Sheriff. Open source under the MIT license.

Demo Video

Every control in the recording is a component from this library. Try them yourself in the live demo.

Add liquid glass to your app

npm i @zakisheriff/liquid-glass

Requires React and React DOM 17 or later. Import the stylesheet once at the app root: app/layout.tsx for the Next.js App Router, or pages/_app.tsx for the Pages Router. Keep components that manage interactive state in a client component.

'use client';

import { useState } from 'react';
import { GlassSwitch } from '@zakisheriff/liquid-glass';
import '@zakisheriff/liquid-glass/styles.css';

export default function Settings() {
  const [enabled, setEnabled] = useState(true);
  return <GlassSwitch checked={enabled} onChange={setEnabled} aria-label="Notifications" />;
}

Liquid glass components

Every example assumes useState from React and the stylesheet imported once. Icons such as <PlusIcon /> stand for any React node, like an SVG icon component.

Button GlassButton

A pill, or a circle when it has an icon and no label. It swells under the finger, leans toward a drag and wobbles as it settles.

Variants

import { GlassButton } from '@zakisheriff/liquid-glass';

<GlassButton>Glass</GlassButton>
<GlassButton variant="clear">Clear</GlassButton>
<GlassButton variant="tinted">Tinted</GlassButton>
<GlassButton variant="filled">Filled</GlassButton>
<GlassButton variant="tinted-filled" tint="#af52de">Tinted filled</GlassButton>

Any colour

<GlassButton variant="tinted" tint="#c2255c">Burgundy label</GlassButton>
<GlassButton variant="filled" tint="#af52de">Purple fill</GlassButton>

Destructive

<GlassButton destructive>Delete</GlassButton>
<GlassButton variant="filled" destructive>Delete</GlassButton>

Icons

{/* Icon with a label */}
<GlassButton icon={<PlusIcon />}>New note</GlassButton>

{/* Icon only: becomes a circle, so give it an accessible name */}
<GlassButton icon={<PlusIcon />} aria-label="Add" />

Sizes

<GlassButton size="sm">Small</GlassButton>   {/* 36px */}
<GlassButton size="md">Medium</GlassButton>  {/* 44px, default */}
<GlassButton size="lg">Large</GlassButton>   {/* 52px */}

Handling clicks

<GlassButton variant="filled" onClick={() => save()} disabled={saving}>
  Save
</GlassButton>

Props

PropTypeDefaultDescription
variant'glass' | 'clear' | 'tinted' | 'tinted-filled' | 'filled''glass'Regular glass, see-through glass, glass with a coloured label, a soft wash of the colour with a matching label, or a solid colour.
tintstringaccent blueAny CSS colour. Colours the label for tinted and the fill for filled.
destructivebooleanfalseRed label, or a red fill with filled, for delete and other irreversible actions.
size'sm' | 'md' | 'lg''md'Height: 36, 44 or 52 px.
iconReactNodeLeading icon. With no children the button becomes a circle.
shimmerbooleantrueLight bloom under the finger while pressed.
thicknessnumber (1–4)1Rim bevel scale. Wider bevels bend more of the backdrop.
intensitynumber (1–10)5Refraction strength, from subtle to heavy.

Also accepts every <button> prop (onClick, disabled, type, aria-*, …).

Toggle Switch GlassSwitch

An iOS toggle. On press the knob lifts into a clear lens that refracts the track. Tap it, or drag the knob with rubber-banding at the ends.

Controlled

import { useState } from 'react';
import { GlassSwitch } from '@zakisheriff/liquid-glass';

function WifiSetting() {
  const [on, setOn] = useState(true);
  return <GlassSwitch checked={on} onChange={setOn} aria-label="Wi-Fi" />;
}

Uncontrolled

<GlassSwitch defaultChecked onChange={(on) => console.log(on)} aria-label="Bluetooth" />

Custom on colour

<GlassSwitch checked={focus} onChange={setFocus} tint="#ff9f0a" aria-label="Focus" />

In a settings list, with a visible label

<label className="setting">
  <span>Live Activities</span>
  <GlassSwitch checked={live} onChange={setLive} />
</label>

Disabled

<GlassSwitch checked disabled aria-label="Managed by your organisation" />

Props

PropTypeDefaultDescription
checkedbooleanOn or off, when you control the state.
defaultCheckedbooleanStarting state, when the switch manages its own state.
onChange(checked: boolean) => voidCalled with the new state after a tap or a drag.
tintstringsystem greenOn-state colour. Any CSS colour.
disabledbooleanfalseDims the switch and ignores input.
intensitynumber (1–10)5Refraction strength, from subtle to heavy.

Also accepts other <button> props (aria-label, id, className, …).

The switch is a role="switch" button: Space and Enter toggle it. Give it an aria-label, or wrap it in a <label> with visible text.

Slider GlassSlider

A thin iOS track whose thumb lifts into a lens that magnifies the rail, or a Control Center block that fills and stretches under the finger.

Track with a lens thumb

import { GlassSlider } from '@zakisheriff/liquid-glass';

const [volume, setVolume] = useState(40);
<GlassSlider value={volume} onChange={setVolume} aria-label="Volume" />

Control Center block

<GlassSlider
  showThumb={false}
  value={brightness}
  onChange={setBrightness}
  icon={<SunIcon />}
  label="Display"
/>

Range, steps and colour

{/* Snaps to 5 °C steps between 10 and 30 */}
<GlassSlider min={10} max={30} step={5} value={temp} onChange={setTemp} tint="#ff9f0a" aria-label="Temperature" />

Props

PropTypeDefaultDescription
valuenumber0Current value.
onChange(value: number) => voidCalled while dragging, with the full-precision value.
minnumber0Lowest value.
maxnumber100Highest value.
stepnumber | 'any''any'Continuous by default. A number snaps to fixed increments.
showThumbbooleantruetrue: thin track with a lens thumb. false: Control Center block.
iconReactNodeIcon inside the block slider.
labelstringLabel inside the block slider. Its number is rounded for display.
tintstringaccent blue (white for blocks)Fill colour.
intensitynumber (1–10)5Refraction strength, from subtle to heavy.

Also accepts other <input type="range"> props (aria-label, disabled, name, …).

Arrow keys, Page Up/Down, Home and End work as on a native range input.

Segmented Control GlassSegmented

A row of choices. On press the whole control swells and the selection becomes a lens you can drag between segments.

Text segments

import { GlassSegmented } from '@zakisheriff/liquid-glass';

const [mode, setMode] = useState(0);
<GlassSegmented options={['Chat', 'Work', 'Study']} value={mode} onChange={setMode} aria-label="Mode" />

With icons

<GlassSegmented
  options={[{ label: 'Light', icon: <SunIcon /> }, { label: 'Dark', icon: <MoonIcon /> }]}
  value={theme}
  onChange={setTheme}
  aria-label="Appearance"
/>

Compact

<GlassSegmented size="sm" options={['Day', 'Week', 'Month']} value={range} onChange={setRange} />

Props

PropTypeDefaultDescription
optionsArray<string | { label, icon? }>Segment labels, or label and icon pairs.
valuenumberIndex of the selected segment.
onChange(index: number) => voidCalled with the index of the newly selected segment.
size'sm' | 'md''md'Height: 28 or 36 px.
intensitynumber (1–10)5Refraction strength, from subtle to heavy.
aria-labelstringAccessible name for the group.
className, styleApplied to the control.

Arrow keys, Home and End move the selection.

Tab Bar GlassNav

A floating iOS 26 tab bar. The selection inflates into a lens you can drag across the tabs. Icons with labels, or icons only.

Icons and Labels

import { GlassNav } from '@zakisheriff/liquid-glass';

const [tab, setTab] = useState(0);
<GlassNav
  activeIndex={tab}
  onChange={setTab}
  aria-label="Sections"
  items={[
    { label: 'Home', icon: <HomeIcon /> },
    { label: 'Saved', icon: <BookmarkIcon /> },
    { label: 'Library', icon: <GridIcon /> },
    { label: 'Search', icon: <SearchIcon /> },
  ]}
/>

Icons Only

{/* Labels become each tab's accessible name */}
<GlassNav showLabels={false} activeIndex={tab} onChange={setTab} items={tabs} />

Tabs plus a separate search button

<div style={{ display: 'flex', gap: 12 }}>
  <GlassNav activeIndex={tab} onChange={setTab} items={tabs} />
  <GlassButton icon={<SearchIcon />} aria-label="Search" size="lg" />
</div>

Props

PropTypeDefaultDescription
itemsArray<{ label: string; icon?: ReactNode }>The tabs. Icons sit above labels.
activeIndexnumber0Index of the selected tab.
onChange(index: number) => voidCalled when a tab is tapped, or when the lens is dragged onto one.
showLabelsbooleantruefalse shows icons only.
intensitynumber (1–10)5Refraction strength, from subtle to heavy.
aria-labelstringAccessible name for the tab list.
className, styleApplied to the bar.

Arrow keys, Home and End move between tabs. Position the bar yourself, for example position: fixed; bottom: 24px.

Search and Text Field GlassInput

A glass text field with a leading icon, an optional trailing accessory and a clear button.

Search

import { GlassInput } from '@zakisheriff/liquid-glass';

const [query, setQuery] = useState('');
<GlassInput
  type="search"
  placeholder="Search"
  value={query}
  onChange={(event) => setQuery(event.target.value)}
  trailing={<MicIcon />}
  aria-label="Search"
/>

Text field with a label

<GlassInput label="Name" placeholder="Your name" value={name} onChange={(e) => setName(e.target.value)} />

Custom icon and clear behaviour

<GlassInput
  icon={<MailIcon />}
  type="email"
  placeholder="Email"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
  onClear={() => setEmail('')}
  tint="#30d158"
/>

Props

PropTypeDefaultDescription
typestring'text'Any input type. 'search' adds a magnifier icon.
valuestringCurrent text.
onChange(event) => voidStandard input change event.
placeholderstringHint shown while empty.
labelstringSmall label above the field.
iconReactNodeLeading icon.
trailingReactNodeTrailing accessory, such as a microphone.
onClear() => voidCalled by the clear button. Without it, clearing sends an empty onChange.
tintstringCaret and focus glow colour.
intensitynumber (1–10)5Refraction strength, from subtle to heavy.

Also accepts every other <input> prop (name, autoFocus, disabled, …), and ref reaches the input.

Card GlassCard

A refracting glass panel for grouping content and controls: frosted, or clear.

Regular and clear

Frosted panel
See-through panel
import { GlassCard } from '@zakisheriff/liquid-glass';

<GlassCard>Frosted panel</GlassCard>
<GlassCard variant="clear">See-through panel</GlassCard>

Spacing, corners and tint

Now playing
Sunset mix
<GlassCard padding="20px" radius="32px" tint="rgb(255 55 95 / 0.18)">
  <h3>Now playing</h3>
  <GlassSlider value={progress} onChange={seek} aria-label="Position" />
</GlassCard>

Props

PropTypeDefaultDescription
variant'regular' | 'clear''regular'Frosted or see-through glass.
paddingstring'24px'Inner padding, any CSS length.
radiusstring'28px'Corner radius, any CSS length.
tintstringCSS colour washed over the glass.
thicknessnumber (1–4)1Rim bevel scale.
intensitynumber (1–10)5Refraction strength, from subtle to heavy.

Also accepts every <div> prop (className, style, onClick, …).

Sheet GlassSheet

A floating bottom sheet with a spring entrance. Drag or flick it down to dismiss. With detents it opens at half height and drags up to full screen.

Fits its content

import { GlassSheet, GlassButton } from '@zakisheriff/liquid-glass';

const [open, setOpen] = useState(false);
<GlassButton onClick={() => setOpen(true)}>Details</GlassButton>
<GlassSheet open={open} onClose={() => setOpen(false)} aria-label="Details">
  <p>Anything goes here.</p>
</GlassSheet>

Half height, then full screen

<GlassSheet open={open} onClose={() => setOpen(false)} detents={['medium', 'large']} aria-label="Comments">
  {comments}
</GlassSheet>

Inside a container

{/* Stays inside a positioned element (e.g. a phone mockup) without locking page scroll */}
const [frame, setFrame] = useState<HTMLDivElement | null>(null);
<div ref={setFrame} style={{ position: 'relative', height: 600, overflow: 'hidden' }}>
  <GlassSheet open={open} onClose={() => setOpen(false)} container={frame}>…</GlassSheet>
</div>

Props

PropTypeDefaultDescription
openbooleanWhether the sheet is shown.
onClose() => voidCalled when the scrim is tapped, Escape is pressed or the sheet is swiped down.
detentsArray<'medium' | 'large'>Resting heights. ['medium', 'large'] opens at half height. Unset: fits its content.
containerHTMLElement | nullviewportPositioned element to keep the sheet inside.
tintstringCSS colour washed over the glass.
aria-labelstringAccessible name for the dialog.
intensitynumber (1–10)5Refraction strength, from subtle to heavy.

Theming and interaction

Set data-theme="light" or data-theme="dark" on an ancestor, or follow the system theme. Place glass over gradients, images or scrolling content so its transparency and refraction are visible.

Sliders drag continuously with step="any" by default. Pass a numeric step, such as step={5}, to snap to fixed increments. The callback receives the full numeric value.

Give fields and icon-only controls accessible labels. Tab bars and segmented controls support arrow keys, Home and End. Check text contrast over your own backgrounds.

Lightweight by design

The whole library is ≈31 KB of JavaScript and ≈5.5 KB of CSS gzipped.

Keeping it fast

Browser support

Chromium applies SVG displacement to the live backdrop for bars, buttons and cards, and refracts a tinted live copy inside tab and segmented lenses. In Safari and Firefox the lenses (switch, slider, tab bar and segmented control) refract a live copy of the content beneath them, bending and stretching it at the rim; bars, buttons and cards use translucent glass and highlights. Effects vary by browser. The large refraction example in the demo uses WebGL.

This project recreates an Apple-inspired visual style for websites; it is independent of Apple Inc.

Common questions

Is liquid glass lightweight enough for a website?

Yes. The whole library is about 31 KB of JavaScript and 5.5 KB of CSS gzipped, with no runtime dependencies. Refraction maps are computed once per size and cached, springs stop when they settle, and lens copies only do work while a control is pressed. The GPU does the drawing, so keep large refracting panels over content that is not constantly animating.

What is Liquid Glass for the Web?

@zakisheriff/liquid-glass is an open-source React component library by Zaki Sheriff. It brings an Apple-inspired liquid glass appearance to web apps, with refraction, translucent surfaces and spring-based touch interactions. It is an independent project, not an Apple product.

How do I add liquid glass to a React or Next.js app?

Run npm i @zakisheriff/liquid-glass, import @zakisheriff/liquid-glass/styles.css once, then import the components you need. In Next.js, import the stylesheet in app/layout.tsx or pages/_app.tsx and add 'use client' to components that manage state. React and React DOM 17 or later are required.

Does liquid glass work in Safari, Firefox and Chrome?

Yes. Chromium applies SVG displacement to the live backdrop. In Safari and Firefox the lenses (switch, slider, tab bar, segmented control) refract a live copy of the content beneath them, bending and stretching it at the rim; bars, buttons and cards there use translucent glass and highlights. The demo also includes a WebGL refraction example. Place glass over gradients, images or content to make the effect visible.

Is the React liquid glass library free to use?

Yes. The component library is MIT licensed and can be used in personal and commercial projects under that license. See the repository license and third-party notices for details.