BrowseGetting Started

Documentation

Getting Started

Vitreo is a set of liquid glass components for Angular. Each piece of glass frosts what sits behind it, catches light along a specular rim, bends the page through an edge lens in Chromium and refracts a backdrop you give it in every browser. Motion runs on springs and steps aside for Reduce Motion; Reduce Transparency turns the glass solid.

Install

Angular 21 or later. The overlays position with Floating UI and trap focus with the CDK.

Install

npm install ngx-vitreo @angular/cdk @floating-ui/dom

Add the styles

One stylesheet carries the whole material. Add it to the styles array of your app in angular.json:

angular.json

"styles": [
  "node_modules/ngx-vitreo/styles/vitreo.css",
  "src/styles.css"
]

Or import it from your global stylesheet. Vitreo needs no Tailwind: it brings its own reset, scoped to glass elements, so the rest of your app keeps its base styles. For a full reset across the app, add ngx-vitreo/styles/preflight.css before it.

styles.css

@import 'ngx-vitreo/styles/vitreo.css';

Provide the preferences

provideVitreo() holds the theme and accessibility preferences, writes them to data-lg-* attributes on <html> and remembers the visitor's choices between visits.

app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideVitreo } from 'ngx-vitreo';

export const appConfig: ApplicationConfig = {
  providers: [
    // Leave defaultTheme out to follow the system setting.
    provideVitreo({ defaultTheme: 'light' }),
  ],
};

Read and change them anywhere with GlassPreferences:

theme-toggle.ts

import { Component, inject } from '@angular/core';
import { GlassButton, GlassPreferences } from 'ngx-vitreo';

@Component({
  selector: 'app-theme-toggle',
  imports: [GlassButton],
  template: `<button glassButton (click)="toggle()">Toggle theme</button>`,
})
export class ThemeToggle {
  private readonly glass = inject(GlassPreferences);

  toggle() {
    const theme = this.glass.preferences().theme === 'light' ? 'dark' : 'light';

    this.glass.setPreferences({ theme });
  }
}

Your first glass

Components are standalone. Import what a template uses:

hero.ts

import { Component } from '@angular/core';
import { GlassButton, GlassPanel } from 'ngx-vitreo';

@Component({
  selector: 'app-hero',
  imports: [GlassButton, GlassPanel],
  template: `
    <glass-panel variant="card" elevation="raised">
      <h2>Glass without effort.</h2>
      <button glassButton variant="prominent">Continue</button>
    </glass-panel>
  `,
})
export class Hero {}

Glass needs something behind it to bend: a wallpaper, a photo or a gradient. See Glass Surface for refraction of a supplied backdrop.

Liquid Glass

Vitreo's glass follows Apple's Liquid Glass. It defines itself by lensing: the curved rim bends and concentrates the light behind it instead of only blurring it, and light falling on that curve picks out its shape.

Two variants.regular, the default, scatters a little light and adapts to what it floats over; use it for anything with text. clear stays transparent and doesn't adapt; use it only over photos and video, with bold, bright content on it. It dims itself as Apple advises: a dark layer that grows to 35% as the media behind it brightens, and none over dark media. Set dim to a number to fix it, or 0 to turn it off. Set the variant per element, or for everything inside a container with [glassVariant].

player.html

<!-- Regular glass over the page -->
<glass-panel variant="card">Settings</glass-panel>

<!-- Clear glass over a video, dimmed for its labels -->
<div class="player" glassVariant="clear">
  <video src="clip.mp4"></video>
  <button glassButton>Pause</button> <!-- dims itself over bright frames -->
</div>

Thickness follows size. A small control is a thin drop; a menu, sheet or sidebar is a thicker slab with a wider rim, stronger lensing, softer scattering and a deeper shadow. Nothing to set: glass measures itself.

Adaptive shadow, tint and colour. Regular glass reads what lies behind it: text, media or a plain surface, and how bright it is, from its colour or its very pixels. It deepens its shadow and tint over text and eases them over plain light ground. Small glass, like a bar or a control, also flips between light and dark with the backdrop, and its labels and symbols flip with it so they always contrast; bigger glass, like a menu or a card, keeps its look and thickens instead over content that works against the theme. Glass on glass, like a button on a card, reads the card's tint too.

A light that moves with the device. Highlights are a fine line along the rim, lit from one light for the whole page, high and to the upper left. On a phone it follows the device's tilt, so light travels around each rim; on a desktop it holds still. On iOS, call requestMotionAccess() from a tap first.

app.config.ts

provideVitreo({ light: 'motion' }); // the default; or 'fixed'

// On iOS, from a tap:
await inject(GlassLightService).requestMotionAccess();

Lensing transitions. Glass appears by gathering its bend and its light, and dialogs, menus, popovers and tooltips open and close the same way, rather than only fading. Pressed buttons swell like gel and spring back.

In Chromium the lens refracts live content. Safari and Firefox show the glass's frost, rim and moving highlights; for refraction there, place glass inside a Glass Scene, which draws it with WebGL2. Tinted mode, Reduce Transparency and Reduce Motion keep their own behaviour.

Forms

Inputs and textareas are native elements, so ngModel and formControl work as usual. Checkbox, Switch, Slider, Radio Group, Segmented Control and Select implement ControlValueAccessor and also offer two-way bindings such as [(checked)] and [(value)].

settings.html

<form [formGroup]="settings">
  <input glassInput formControlName="name" placeholder="Name" />
  <glass-switch formControlName="notifications" />
  <glass-slider formControlName="volume" [max]="100" />
</form>

Conventions

Every component follows the same few rules, so one you know reads like the rest:

  • Controls sit on your own native element: <button glassButton>, <input glassInput>. Put glassButton on an <a> to give a link the button's glass.
  • Containers and parts are tags: <glass-panel>, <glass-tabs-list>, <glass-dialog-content>. Your class lands on the glass itself.
  • State that changes is a model you can bind both ways: [(open)], [(value)], [(checked)].
  • Content the glass draws, such as refractionBackground, is an <ng-template> or a content slot.
  • Behaviour comes as injection functions: inject(GlassPreferences), injectLensFilter(), injectRefractionFilter().