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-glassRequires 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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | On or off, when you control the state. | |
defaultChecked | boolean | Starting state, when the switch manages its own state. | |
onChange | (checked: boolean) => void | Called with the new state after a tap or a drag. | |
tint | string | system green | On-state colour. Any CSS colour. |
disabled | boolean | false | Dims the switch and ignores input. |
intensity | number (1–10) | 5 | Refraction 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
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Current value. |
onChange | (value: number) => void | Called while dragging, with the full-precision value. | |
min | number | 0 | Lowest value. |
max | number | 100 | Highest value. |
step | number | 'any' | 'any' | Continuous by default. A number snaps to fixed increments. |
showThumb | boolean | true | true: thin track with a lens thumb. false: Control Center block. |
icon | ReactNode | Icon inside the block slider. | |
label | string | Label inside the block slider. Its number is rounded for display. | |
tint | string | accent blue (white for blocks) | Fill colour. |
intensity | number (1–10) | 5 | Refraction 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
| Prop | Type | Default | Description |
|---|---|---|---|
options | Array<string | { label, icon? }> | Segment labels, or label and icon pairs. | |
value | number | Index of the selected segment. | |
onChange | (index: number) => void | Called with the index of the newly selected segment. | |
size | 'sm' | 'md' | 'md' | Height: 28 or 36 px. |
intensity | number (1–10) | 5 | Refraction strength, from subtle to heavy. |
aria-label | string | Accessible name for the group. | |
className, style | Applied 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
| Prop | Type | Default | Description |
|---|---|---|---|
items | Array<{ label: string; icon?: ReactNode }> | The tabs. Icons sit above labels. | |
activeIndex | number | 0 | Index of the selected tab. |
onChange | (index: number) => void | Called when a tab is tapped, or when the lens is dragged onto one. | |
showLabels | boolean | true | false shows icons only. |
intensity | number (1–10) | 5 | Refraction strength, from subtle to heavy. |
aria-label | string | Accessible name for the tab list. | |
className, style | Applied 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
| Prop | Type | Default | Description |
|---|---|---|---|
type | string | 'text' | Any input type. 'search' adds a magnifier icon. |
value | string | Current text. | |
onChange | (event) => void | Standard input change event. | |
placeholder | string | Hint shown while empty. | |
label | string | Small label above the field. | |
icon | ReactNode | Leading icon. | |
trailing | ReactNode | Trailing accessory, such as a microphone. | |
onClear | () => void | Called by the clear button. Without it, clearing sends an empty onChange. | |
tint | string | Caret and focus glow colour. | |
intensity | number (1–10) | 5 | Refraction 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
import { GlassCard } from '@zakisheriff/liquid-glass';
<GlassCard>Frosted panel</GlassCard>
<GlassCard variant="clear">See-through panel</GlassCard>Spacing, corners and tint
<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
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'regular' | 'clear' | 'regular' | Frosted or see-through glass. |
padding | string | '24px' | Inner padding, any CSS length. |
radius | string | '28px' | Corner radius, any CSS length. |
tint | string | CSS colour washed over the glass. | |
thickness | number (1–4) | 1 | Rim bevel scale. |
intensity | number (1–10) | 5 | Refraction 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
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | Whether the sheet is shown. | |
onClose | () => void | Called when the scrim is tapped, Escape is pressed or the sheet is swiped down. | |
detents | Array<'medium' | 'large'> | Resting heights. ['medium', 'large'] opens at half height. Unset: fits its content. | |
container | HTMLElement | null | viewport | Positioned element to keep the sheet inside. |
tint | string | CSS colour washed over the glass. | |
aria-label | string | Accessible name for the dialog. | |
intensity | number (1–10) | 5 | Refraction 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.
- No runtime dependencies. React and React DOM are the only peers.
- Refraction maps are computed once per glass size, cached and shared, then drawn by the browser. Nothing is recalculated while you scroll.
- Springs run on one shared animation frame loop and stop when they settle.
- Lens copies only copy and lay out content while a control is pressed, and stop entirely off screen.
- Refraction pauses while the page scrolls and for glass off screen; blur and the rim carry the look in motion.
- On slower devices resting surfaces keep blur and rim only, and lenses still bend while held. setGlassQuality() pins high or low.
- The demo page, with 99 glass elements, holds about 11 MB of JavaScript memory and renders at 60 fps.
Keeping it fast
- The GPU cost is in what moves under the glass. Keep large refracting panels over static or slow content.
- Use a few large glass surfaces rather than many small overlapping ones.
- Lower intensity, or use GlassCard variant="clear", on low-end devices.
- Quality is picked automatically from the device and the first scrolls. Call setGlassQuality('high') or setGlassQuality('low') to fix it.
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.