# Liquid Glass for the Web

> Apple-inspired liquid glass components for React and Next.js by Zaki Sheriff. Package: @zakisheriff/liquid-glass. MIT licensed. Version 4.0.0.

## Official links
- Demo: https://liquidglass.theatom.lk/
- Documentation: https://liquidglass.theatom.lk/docs
- Source: https://github.com/zakisheriff/Liquid-Glass
- npm: https://www.npmjs.com/package/@zakisheriff/liquid-glass

## Install
```bash
npm i @zakisheriff/liquid-glass
```

## React and Next.js quick start
```tsx
'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" />;
}
```

Import the stylesheet once at the app root. In the Next.js App Router use app/layout.tsx; in the Pages Router use pages/_app.tsx. The library requires React and React DOM >=17. Keep interactive state in a client component.

## Components
### 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**

```tsx
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**

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

**Destructive**

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

**Icons**

```tsx
{/* 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**

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

**Handling clicks**

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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `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. |
| `tint` | `string` | `accent blue` | Any CSS colour. Colours the label for `tinted` and the fill for `filled`. |
| `destructive` | `boolean` | `false` | Red label, or a red fill with `filled`, for delete and other irreversible actions. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Height: 36, 44 or 52 px. |
| `icon` | `ReactNode` |  | Leading icon. With no children the button becomes a circle. |
| `shimmer` | `boolean` | `true` | Light bloom under the finger while pressed. |
| `thickness` | `number (1–4)` | `1` | Rim bevel scale. Wider bevels bend more of the backdrop. |
| `intensity` | `number (1–10)` | `5` | Refraction 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**

```tsx
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**

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

**Custom on colour**

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

**In a settings list, with a visible label**

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

**Disabled**

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

| 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**

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

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

**Control Center block**

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

**Range, steps and colour**

```tsx
{/* 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" />
```

| 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**

```tsx
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**

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

**Compact**

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

| 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**

```tsx
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**

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

**Tabs plus a separate search button**

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

| 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`.

### Menu · `GlassMenu`

A pull-down menu that grows out of its glass button and folds back into it with a jiggle. Slide a finger across the items to pick one.

**Actions**

```tsx
import { GlassMenu } from '@zakisheriff/liquid-glass';

<GlassMenu
  icon={<EllipsisIcon />}
  aria-label="More"
  items={[
    { label: 'Share', icon: <ShareIcon />, onSelect: share },
    { label: 'Duplicate', icon: <CopyIcon />, onSelect: duplicate },
    { label: 'Delete', icon: <TrashIcon />, destructive: true, onSelect: remove },
  ]}
/>
```

**Choices with a checkmark**

```tsx
<GlassMenu
  icon={<SortIcon />}
  aria-label="Sort by"
  items={['Date', 'Name', 'Size'].map((sort) => ({
    label: sort,
    checked: sort === current,
    onSelect: () => setCurrent(sort),
  }))}
/>
```

**Placement and size**

```tsx
{/* Grows from the left edge of a small trigger, 260px wide */}
<GlassMenu icon={<EllipsisIcon />} aria-label="More" align="start" size="sm" width={260} items={items} />
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `icon` | `ReactNode` |  | Icon on the trigger button, such as an ellipsis. |
| `items` | `Array<{ label, icon?, checked?, destructive?, disabled?, onSelect? }>` |  | Menu rows. `checked` adds a trailing checkmark; `destructive` makes the label red. |
| `aria-label` | `string` |  | Accessible name for the trigger. Required. |
| `align` | `'start' \| 'end'` | `'end'` | Edge of the trigger the menu grows from. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Trigger size, as for GlassButton. |
| `width` | `number` | `230` | Menu width in px. |
| `check` | `ReactNode` |  | Replaces the trailing checkmark icon. |
| `className` | `string` |  | Applied to the menu wrapper. |

Escape or a tap outside closes the menu. Arrow keys move between items.

### Search and Text Field · `GlassInput`

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

**Search**

```tsx
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**

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

**Custom icon and clear behaviour**

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

| 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**

```tsx
import { GlassCard } from '@zakisheriff/liquid-glass';

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

**Spacing, corners and tint**

```tsx
<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>
```

| 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**

```tsx
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**

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

**Inside a container**

```tsx
{/* 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>
```

| 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. |

## Styling and interaction
Sliders use step="any" for continuous dragging by default. Set a numeric step for fixed increments. onChange receives the numeric value.
Set data-theme="light" or data-theme="dark" on an ancestor, or follow the system theme. Give icon-only controls an aria-label and fields a label. Tab bars and segmented controls support arrow keys, Home and End. Test contrast over your actual background.

## Performance
≈31 KB JavaScript and ≈5.5 KB 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.

Tips:
- 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 rendering
Chromium uses SVG backdrop displacement for bars, buttons and cards, and refracts a tinted live copy inside tab and segmented lenses. Safari and Firefox use translucent material, highlights and magnified live copies inside lenses (switch, slider, tab bar, segmented). Effects are not identical across browsers. The demo's large refraction lens uses WebGL.

## 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.
