Ripple Grid

A faint square grid of thin lines behind a page's content. A click or a tap on the parent sends a ring out from that point: as the wave passes, the lines bend outward and brighten a little toward the primary colour; the ring widens, fades, and the grid lies flat again. Several rings add up where they cross.

Installation

Import the class, and @use the entry point in your global styles. Nothing warns you when the @use is missing.

import { Component } from '@angular/core';
import { WrRippleGrid } from 'ngwr/animations/ripple-grid';

@Component({ imports: [WrRippleGrid] })
export class MyComponent {}

Basic

Put <wr-ripple-grid /> first inside the region it should fill. The parent takes the click, passively and never stopped, so a link or a button over the grid works as before and still starts a ring under the pointer; a keyboard activation is a click with no point and rings nothing. One 2D canvas, drawn on the CPU every frame while something moves and idle otherwise: the frame loop runs only while a ring is alive, on screen and with the document visible; at rest the flat grid is drawn once, as straight lines.

<main class="not-found">
  <wr-ripple-grid />
  <wr-result-404 />
</main>

Click anywhere in this box.

A ring runs out from the click and the grid bends as it passes.

The parent

The host is absolute, inset: 0, at z-index: -1, under no pointer and masked out at the edges. The parent provides two things: it is positioned, so the host fills it, and isolated, so the host's negative z-index lands at the floor of the parent's own stacking context, under the content and over the page background.

.not-found {
  position: relative;
  isolation: isolate;
}

Spacing

spacing is the distance between two grid lines, in px, 32 unset; the grid is centred, so the margins at the edges are equal. A finer grid bends in more places as the wave passes, a coarser one reads as a few lines moving. The two colours are the --wr-ripple-grid-ink and --wr-ripple-grid-glow hooks, set on the host, where the defaults are declared, and read through resolveCssColorChannels and re-read when the scheme changes. The wave's speed, band, height and life are the model's numbers, not inputs.

<wr-ripple-grid spacing="20" />
<wr-ripple-grid spacing="48" />

spacing="20"

A finer grid.

spacing="48"

A coarser grid.

Accessibility

PartBehaviour
Screen readers Decorative: the host is aria-hidden, holds no text, takes no pointer and is never focusable. The click listener on the parent is passive and stops nothing, so every control over the grid behaves as before.
Reduced motion Under prefers-reduced-motion: reduce only the flat grid is drawn and a click rings nothing; a preference that flips mid-flight drops the live rings at once.
Contrast A line shows its ink at a fifth, so the text over the grid reads as it does on the plain surface. Under forced-colors: active nothing is drawn.
Server The server renders the empty canvas, absolutely positioned, so the browser's first draw shifts no layout.

API

NameDescriptionTypeDefault
spacingDistance between two grid lines, in px.number32

CSS variables

Set these on the element itself, with a rule more specific than its Declared on selector. In a global stylesheet, put a class of yours on an ancestor it really has: <body> for an overlay panel, which renders outside your markup. In a component stylesheet the selector alone works, but only for an element in that component's own template. A value on a wrapper or :root is shadowed, because ngwr/animations/ripple-grid declares each default on its own element.

VariableDefaultDeclared on
--wr-ripple-grid-glowvar(--wr-color-primary).wr-ripple-grid
--wr-ripple-grid-inkvar(--wr-color-on-surface-muted).wr-ripple-grid

See also