# ethereal-glow — full reference
> Travelling-light borders and black-hole accretion glows as React components — pure CSS gradients and masks by default, with a dithered-canvas renderer alongside, all driven by one shared 60fps loop for every mounted instance.
- Docs: https://ethereal.ale.design/docs
- Playground: https://ethereal.ale.design/playground
- Source: https://github.com/TheAleSch/ethereal
- License: MIT
---
# ethereal-glow
**[Playground](https://ethereal.ale.design/playground)** ·
**[Docs](https://ethereal.ale.design/docs)** ·
**[ethereal.ale.design](https://ethereal.ale.design)**
Travelling-light, black-hole and dithered glow effects for React. Two renderers
use CSS gradients and masks; the dithered renderer uses a pixelated canvas. All
three are driven by **one shared ~60fps loop** for every mounted instance.
Three effects:
- **``** — a comet head travels along your element behind a
spotlight mask: a lit stretch of the border ring, an interior color wash,
thin light needles and a white-hot core.
- **``** — a black-hole accretion disk: a white-hot head
orbits trailing a doppler-tinted plasma stream, wrapped in a graduated
gravitational lens, a living photon rim and a lensed halo.
- **``** — the travelling-light comet rendered as quantized
canvas blocks through a Bayer 4×4 matrix.
## Install
```sh
npm i ethereal-glow
```
React ≥18 is a peer dependency. All three components are client components
(`'use client'`) — safe to import from React Server Components.
## Use
Drop the effect inside any element that has `position: relative` and
`isolation: isolate`:
```tsx
import { Ethereal, EventHorizon } from 'ethereal-glow'
```
When you can't edit the child (inputs, third-party components), use the
wrapper form — replaced elements can't contain the effect span:
```tsx
import {
EtherealWrap,
EventHorizonWrap,
EtherealDitherWrap,
} from "ethereal-glow";
;
```
All props are optional overrides of the exported defaults (`ETHEREAL`,
`EVENT_HORIZON`, `ETHEREAL_DITHER`). The playground's **Copy link** / code panel emits a
ready-to-spread props object:
```tsx
```
Event Horizon ships presets:
```tsx
import { EVENT_HORIZON_PRESETS } from "ethereal-glow";
;
```
## EtherealDither
The comet as **digital dithered blocks** — a canvas at one-pixel-per-cell
resolution upscaled with `image-rendering: pixelated`, intensity quantized
through a Bayer 4×4 matrix. Retro-terminal cousin of ``, same
host contract and shared ticker:
```tsx
import { EtherealDither, EtherealDitherWrap } from "ethereal-glow";
;
```
Key props: `block` (cell px), `levels` (quantization steps), `reach` (glow
radius), `band` (border band px), `bleed` (grid overhang px), plus the
familiar `colors` / `path` / `heads` / `spin` / `duration` / `hover`.
## States
`Ethereal` (and `EtherealWrap`) accept a `state` prop — a named **variation of
the config you gave**, not a different look bolted on top. The built-in states
are derived from your own config: your colors, path, geometry and pacing go in,
and the same effect comes back quicker and more restless (`thinking`). A red
comet going `around` stays a red comet going around. Changing state rebuilds
the layers with a fade-in
(`transitionMs`, default `320`, `0` disables). Built-ins mirror an AI chat
composer:
```tsx
const [state, setState] = useState<'idle' | 'thinking'>('idle')
```
| State | Character |
| ---------- | ------------------------------------------------------------------------------------------------------------------------- |
| `idle` | your base config, untouched |
| `thinking` | your config, quicker and more restless — shorter duration, more wander and flicker, a tighter pulse, hover reactions off |
`EventHorizon` and `EtherealDither` derive the same temperament from their own
config shapes (orbit and shimmer; block wander and flicker).
The derivation is a fallback, not an override: anything you state explicitly
wins. Define or override states with the `states` prop — an entry there
replaces the derived value key by key, and leaves the rest derived. A state is
keyed by **variant**, not by config key directly:
| Variant | Applied |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `light` / `dark` | the theme branch, picked by the resolved theme |
| `.base` | that theme's config for the state |
| `.whileHover` | merged on top while hovered (mouse only — a touch tap never latches it on) |
| `.whilePressed` | merged on top while pressed, by pointer **or** keyboard (Enter/Space), held ~600ms so a quick click still reads as a pulse |
Theme owns interaction, so light and dark can carry different hover treatments.
Within a theme, later slots win: `whilePressed` overrides `whileHover`.
Full precedence, lowest to highest: defaults → your flat props → `themes`
branch → **derived state variation** → the named state's config (built-in,
then `states`, then its hover/press slots) → the flat `whileHover` /
`whilePressed` props.
To show a state's variation without mounting the component — a preview, a
generated snippet — call the rule directly: `deriveEtherealState(cfg,
'thinking')` (also `deriveEventHorizonState`, `deriveEtherealDitherState`)
returns the `Partial` that would be merged over `cfg`.
```tsx
```
The same shape works on `` and `` — all three
resolve states through one shared implementation.
For a hover or press treatment that is the _same_ in both themes, skip the
state entirely and use the top-level `whileHover` / `whilePressed` props:
```tsx
```
Both are flat `Partial` — no theme branch. They are spread after the
resolved state, so for any one key use the prop overlay **or** the state's
slot, not both.
### Per-theme base config
When light and dark want different _values_ — not just a dimmer version of
the same glow — branch the base config with `themes`:
```tsx
```
The full merge order, lowest to highest:
| Layer | Source |
| ----- | ------------------------------------------------------------------ |
| 1 | exported defaults (`ETHEREAL`, `EVENT_HORIZON`, `ETHEREAL_DITHER`) |
| 2 | your flat props |
| 3 | `themes[resolvedTheme]` |
| 4 | the named `state`'s theme branch, then its hover/press slots |
| 5 | the flat `whileHover` / `whilePressed` props |
`themes` sits below states on purpose: a state is the more specific thing, so
`state="error"` can still override the theme baseline. If it were the other
way round, one `themes.dark.duration` would silently pin duration across every
state.
`themes` is the only per-theme mechanism — all three effects take it, and it is
the whole light-mode story for each of them. "Same config, just dimmer on
white" is `themes={{ light: { strength: 0.6 } }}`.
Mind the shape asymmetry between the two props: a `themes` branch is flat, but
a `states` branch has interaction slots — so `themes.light.duration` works while
`themes.light.base.duration` is a silent no-op.
### Theme resolution
Zero configuration in the common case. On mount each effect resolves, in order:
1. the `theme` prop, if you passed one — detection is skipped entirely
2. the host's own `data-theme="dark" | "light"`
3. the host's own `.dark` / `.light` class
4. the same two checks on every ancestor
5. `prefers-color-scheme: dark`
6. `light`
| Setup | Resolves via |
| -------------------------------------------------------------- | --------------------------- |
| Tailwind `darkMode: 'class'` (and v4's `@custom-variant dark`) | `.dark` on ``, step 4 |
| shadcn/ui + next-themes | `class="dark"`, step 4 |
| next-themes `attribute="data-theme"` | step 2 / 4 |
| Tailwind `darkMode: 'media'`, or no theme system at all | step 5 |
| anything else | `themeDetector` |
Changes are picked up live — one document-wide `MutationObserver` shared by
every mounted instance watches `data-theme` and `class`, and a `matchMedia`
listener covers the OS scheme. A theme toggle updates the glow without a
remount.
Two escape hatches:
```tsx
{/* pin it */}
myThemeStore.current} /> {/* replace the chain */}
```
Both opt out of watching: if you own the value, you own re-rendering when it
changes.
## Key props
### Ethereal
| Prop | Default | What it does |
| ----------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------- |
| `colors` | 9-color rainbow | Palette cycled across blobs/needles |
| `path` | `'bottom'` | `bottom` line · `around` full perimeter · `breathe` stationary pulse · `static` fixed band + waveform needles |
| `heads` / `spin` | `1` / `'same'` | Dual comets, co- or counter-rotating |
| `place` | `'internal'` | `internal` · `external` · `ext-border` (glow only outside) · `both` |
| `spotShape` | `'round'` | `round` = the spotlight is a chain of circles that individually follow the path — never a stretched ellipse |
| `spotSamples` | `0` | Circles in the round chain (0 = auto from spotW/spotH ratio) |
| `hotspots` / `hotSpread` | `1` / `22` | Extra white-hot cores fanned along the path, each walking it individually |
| `hover` | `'none'` | `boost` · `speed` · `reveal` |
| `duration`, `spotW/H`, `needles`, `glowBlur`, … | | See `EtherealCfg` — every field is typed and documented in the source |
### EventHorizon
| Prop | Default | What it does |
| ------------------------------------------ | --------------------- | ------------------------------------------------------------------------------ |
| `colors` | warm oranges + violet | Doppler palette down the tail |
| `duration` / `dir` | `6` / `1` | Orbit period and direction |
| `ring`, `tail`, `nodes`, `node`, `shimmer` | | Accretion ring thickness, tail length, stream density, node size, surge amount |
| `blur`, `halo`, `dist` | `14`, `0.9`, `0` | Halo blur, opacity, and distance off the border |
| `shape` | `'adaptive'` | `round` = fixed-circle silhouette through corners |
| `corner` | `0.3` | Superellipse corner exponent (lower = squarer path) |
| `lens` | `4` | Graduated backdrop-filter lensing strength (0 = off) |
| `shadow` | `0.35` | Center vignette depth |
| `hover` | `'boost'` | `boost` · `speed` · `reveal` · `none` |
## Behavior & performance
- **One rAF loop** drives every instance of all three effects, targeting ~60fps —
one tick per frame on a 60Hz display; higher-refresh displays are gated
down to the target. It stops entirely when the last instance unmounts.
- **The frame rate is tunable** via `setTickRate(fps)` — pass `0` to tick at
the display's native refresh rate, or `30` to halve the paint cost on
hero-size effects. Speed is unaffected: every effect integrates against
wall-clock `dt` rather than counting frames.
```ts
import { setTickRate, getTickRate } from "ethereal-glow";
setTickRate(30); // large effect, tight budget
```
It is one process-wide setting, not per-instance — every effect shares the
loop, so the last caller wins. `getTickRate()` reads it back if a component
needs to restore the previous value on unmount.
`` redraws its canvas every tick rather than only writing
CSS custom properties, so it can be the most expensive renderer on a large
host — consider `setTickRate(30)` when using it.
- **Off-screen instances pause** (IntersectionObserver, 160px margin) and
the loop clamps `dt` after background-tab pauses so clocks never jump.
- **Layout reads are cached** by ResizeObserver — the per-frame code only
writes CSS custom properties, so there is no layout thrash.
- **`prefers-reduced-motion: reduce`** renders a static glow with no
animation loop.
- **Theming**: honors `html[data-theme]`, `.light`/`.dark` classes, or the
OS scheme — see [Theme resolution](#theme-resolution). One shared
MutationObserver and one `matchMedia` listener serve every mounted
instance, regardless of how deep in the tree they sit. Use `themes.light` to
tune any of the three effects for light backgrounds.
- **State transitions**: re-render with a different config and the rebuilt
layers fade in over ~320ms (e.g. idle → thinking on a chat composer).
## Clipping caveat
With `place: "external"` / `"ext-border"` / `"both"` (and Event Horizon's
halo always) the glow paints OUTSIDE the host, so any ancestor that clips
will cut it off: `overflow` hidden/auto/scroll/clip, `contain: paint`,
`clip-path`, transformed ancestors with overflow. Give the nearest scroll
container enough padding for the glow reach (~`glowBlur×2 + 30px`), lift the
element out of the clipping wrapper, or use `place: "internal"`.
Also: `place: "internal"` draws INSIDE the element — on a solid bright fill
it barely reads. Use external placement there, or keep ethereal elements
dark/outlined.
## License
MIT
---
# Complete prop reference
Every prop below is an optional override of the exported defaults
(`ETHEREAL`, `EVENT_HORIZON`, `ETHEREAL_DITHER`).
## Getting started
```sh
npm i ethereal-glow
```
Or via the shadcn/ui registry — this drops an `EtherealButton` you own into
`components/ui/` and adds the package as a dependency, so fixes still arrive
over npm rather than being stranded in your tree:
```sh
npx shadcn@latest add https://ethereal.ale.design/r/ethereal.json
```
```tsx
import { Ethereal, EventHorizon } from 'ethereal-glow'
// Ethereal — a comet travels the border behind a spotlight mask
// EventHorizon — a black-hole accretion disk
```
### The host contract
```css
/* Every effect needs a positioned, isolated host. Tailwind: */
/* Plain CSS: */
.host { position: relative; isolation: isolate; }
```
### Wrap components
```tsx
import { EtherealWrap, EventHorizonWrap, EtherealDitherWrap } from 'ethereal-glow'
// Replaced elements (inputs, textareas) can't contain the effect span —
// wrap them instead. The wrapper span becomes the positioned, isolated host.
// EventHorizonWrap and EtherealDitherWrap provide the same host contract.
```
## `` props
#### Colors, path & timing
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `colors` | `string[]` | `9-color rainbow` | Palette cycled across blobs, needles and cores. Any CSS-parseable color. |
| `path` | `'bottom' \| 'around' \| 'breathe' \| 'static'` | `'bottom'` | bottom = light sweeps the bottom edge · around = full perimeter · breathe = stationary pulse · static = fixed bottom band with waveform needles. |
| `duration` | `number` | `3.1` | Seconds for one full travel cycle (clamped to 0.1–600). |
| `repeatDelay` | `number` | `0` | Dead time (s) after each cycle before the next begins — less frequent, not slower. |
| `heads` | `1 \| 2` | `1` | One travelling comet head, or two. |
| `spin` | `'same' \| 'counter'` | `'same'` | With heads = 2, the two heads co-rotate or counter-rotate. |
| `travelEase` | `'linear' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'back' \| 'bounce'` | `'linear'` | Easing applied to the head's progress along the path. |
| `wander` | `number` | `0` | Slow sine warp of progress — the head hesitates and hurries (0 = steady). |
| `breatheAmp` | `number` | `0.25` | Amplitude of the breathe-mode pulse; also drives subtle size pulsing. |
#### Spot geometry & placement
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `place` | `'internal' \| 'external' \| 'ext-border' \| 'both'` | `'internal'` | Where the glow paints: inside the host, outside, only outside the border, or both. External placements can be clipped by ancestors (see caveats). |
| `spotShape` | `'adaptive' \| 'round'` | `'round'` | adaptive stretches the spotlight along the travel direction; round is a chain of equal circles that each follow the path — never a stretched ellipse. |
| `spotSamples` | `number` | `0` | round only: circles in the chain (0 = auto from spotW/spotH ratio, capped at 9). |
| `trail` | `number` | `1` | Comet-tail length multiplier for the spot chain (clamped to 0.2–4). |
| `lead` | `number` | `0` | Leading trail — the chain extends AHEAD of the head too (0 = none, 1 = as long as the tail). |
| `trailFade` | `number` | `0.45` | How strongly trail circles shrink and dim toward the tail end (0–1). |
| `spotW` | `number` | `78` | Spotlight width in px (clamped to 1–4000). |
| `spotH` | `number` | `60` | Height of the light in px (clamped to 1–4000). Scales the glow's paint — blobs, needles, edge-relight bands — as well as the reveal window. |
| `heightMin` | `number` | `0.8` | Lower bound of the height breathing, ×spotH (travel paths; breathe keeps breatheAmp). |
| `heightMax` | `number` | `1.3` | Upper bound of the height breathing, ×spotH — raise it to let the light surge tall. |
| `spotBlur` | `number` | `0` | Blur (px) of the masked-in spot content, softening the mask cutoff. |
| `spotOffset` | `number` | `0` | Pushes the spotlight anchor outward past the border along the head's normal (px). Ignored in breathe. |
| `strokeWidth` | `number` | `1` | Thickness (px) of the lit border ring. |
| `blendSoftness` | `number` | `0.5` | Softness of gradient stops across the spot and needles (0–1). |
| `reveal` | `number` | `1` | Scales the bloom spotlight mask (internal and external) — smaller = tighter reveal. |
| `hotspots` | `number` | `1` | Extra white-hot cores fanned along the path, each walking it individually. |
| `hotSpread` | `number` | `22` | Spacing (px) between fanned hotspot cores. |
#### Needles
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `needles` | `number` | `7` | Count of thin light needles emitted along the edge. |
| `needleHeight` | `number` | `1` | Length multiplier for the needles. |
| `needleJitter` | `boolean` | `false` | breathe only: needles wobble and reshuffle onto new perimeter spots each cycle. |
#### Bloom & opacities
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `glowBlur` | `number` | `8` | Gaussian blur (px) of the bloom / needle halo layer. |
| `strokeOpacity` | `number` | `1.14` | Opacity multiplier of the lit border ring. |
| `innerOpacity` | `number` | `0.7` | Opacity multiplier of the interior color wash (internal / both). |
| `bloomOpacity` | `number` | `0.8` | Opacity multiplier of the needle bloom layer. |
| `strength` | `number` | `1` | Master opacity multiplier over all layers. |
| `saturation` | `number` | `1` | saturate() multiplier applied to every layer. |
| `brightness` | `number` | `1` | brightness() multiplier applied to every layer. |
| `hueRange` | `number` | `13` | Degrees of hue oscillation over time (0 = fixed hue; breathe does a full-circle rotation). |
| `gamut` | `'srgb' \| 'p3'` | `'srgb'` | Color space for emitted gradients — p3 uses color(display-p3 …). |
#### Hover
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hover` | `'none' \| 'boost' \| 'speed' \| 'boost-speed' \| 'reveal'` | `'none'` | Pointer response: none · boost (brighter + larger) · speed (faster travel) · boost-speed (both at once) · reveal (fades in from hidden on hover). |
| `hoverAmount` | `number` | `1` | Strength of the hover response. |
| `hoverEase` | `number` | `8` | Smoothing rate of the hover transition (higher = snappier). |
#### Misc & theming
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `flicker` | `number` | `0` | Candle-like irregular intensity jitter (0 = steady). |
| `pulseMin` | `number` | `0.8` | Lower bound of the size / intensity pulse oscillators. |
| `pulseMax` | `number` | `1.4` | Upper bound of the size / intensity pulse oscillators. |
## State, theme & interaction props
Accepted by all three components. `Cfg` in the type column is the component's
own config type: `EtherealCfg`, `EventHorizonCfg` or `EtherealDitherCfg`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `state` | `string \| null` | `'idle'` | Named state to apply: a built-in ('idle' \| 'thinking') or any key of `states`. Unknown names warn once and fall back to the base config; `null` suppresses state resolution entirely. |
| `states` | `Record>` | `—` | Custom / overriding state configs, merged over the built-ins per key. |
| `themes` | `Partial>>` | `—` | Per-theme base config, merged over your flat props and UNDER any named state — so a state can still override it. Note the shape: a themes branch is flat, unlike a states branch, so `themes.light.base.duration` is a silent no-op. It is the only per-theme mechanism on all three effects — a glow that should be dimmer on white is `themes={{ light: { strength: 0.6 } }}`. |
| `transitionMs` | `number` | `320` | Cross-fade duration (ms) when the config or state changes; 0 disables the cross-fade. |
| `whileHover` | `Partial` | `—` | Merged while the host is hovered (mouse only), with or without a named state. Theme-independent — for a light/dark split use a state's light.whileHover instead. |
| `whilePressed` | `Partial` | `—` | Merged while pressed (pointer or keyboard). Layers over whileHover, never directly over base — a real press is hovered and pressed at once. A quick click pulses for ~0.6s. |
| `theme` | `'light' \| 'dark'` | `auto` | Pins the theme and skips detection entirely. Omit it and the effect resolves: host data-theme → host .dark/.light class → the same two checks on every ancestor → prefers-color-scheme. Tailwind's class strategy, shadcn/ui and next-themes all work with no configuration. |
| `themeDetector` | `(host: HTMLElement \| null) => 'light' \| 'dark'` | `—` | Replaces the whole resolution chain — use it when the theme lives somewhere the DOM does not show it (React context, a cookie, a custom attribute). Like `theme`, it opts out of watching: you own re-rendering when the value changes. |
### Tuning this in the playground
The playground edits the same cascade, one cell at a time. The `Base` row is
the flat config every state inherits; each named state below it is a `states`
entry. Inside a row, two pill strips pick the cell you are editing: theme, then
interaction slot.
On the `Base` row the theme pills read `base` and `dark ^` rather than
light/dark, because the base config is not symmetric: `base` edits the shared
flat props, and `dark ^` writes a `themes.dark` override on top — the same
shape as Tailwind's base styles plus `dark:` variants. On a named state the
pills are plain `light` / `dark`, because a state does have two symmetric
branches.
A green dot on a pill means that cell holds overrides; an amber dot beside a
control means that cell sets it, and clicking it clears the override. The
expanded control section persists across theme, slot and state switches.
## `` props
#### Colors & timing
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `colors` | `string[]` | `['#ffb46b', '#ff8a3d', '#b58cff']` | Doppler palette cycled down the accretion tail. |
| `duration` | `number` | `6` | Seconds for one orbit (clamped to 0.5–600). |
| `repeatDelay` | `number` | `0` | Dead time (s) after each orbit before the next begins — less frequent, not slower. |
| `dir` | `1 \| -1` | `1` | Orbit direction. |
#### Disk geometry
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `ring` | `number` | `2` | Accretion-ring thickness (px). |
| `tail` | `number` | `1.2` | Tail length multiplier. |
| `nodes` | `number` | `9` | Tail micro-spot pairs — stream density (chain capped at 32 spots). |
| `node` | `number` | `1` | Node size multiplier. |
| `shimmer` | `number` | `0.45` | How much the tail surges and pulses. |
| `shape` | `'adaptive' \| 'round'` | `'adaptive'` | round collapses oriented ellipses to fixed circles — the silhouette never swells or squashes through corners. |
| `corner` | `number` | `0.3` | Superellipse corner exponent for the orbit path (lower = squarer). |
#### Lens, halo & shadow
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `lens` | `number` | `4` | Graduated gravitational-lens backdrop-blur strength (0 = off). |
| `halo` | `number` | `0.9` | Halo opacity. |
| `blur` | `number` | `14` | Halo gaussian blur (px). |
| `dist` | `number` | `0` | Halo distance off the border (px). |
| `shadow` | `number` | `0.35` | Center vignette depth. |
#### Hover
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hover` | `'none' \| 'boost' \| 'speed' \| 'reveal'` | `'boost'` | Pointer response — same modes as Ethereal. |
| `hoverAmount` | `number` | `1.2` | Strength of the hover response. |
| `hoverEase` | `number` | `8` | Smoothing rate of the hover transition. |
## Event Horizon presets
Exported as `EVENT_HORIZON_PRESETS`:
- `Gargantua`
- `Blue giant`
- `Ember disk`
- `Violet quasar`
- `Neutron`
```tsx
import { EventHorizon, EVENT_HORIZON_PRESETS } from 'ethereal-glow'
```
## `` props
The same travelling comet rendered as ordered-dithered blocks on a canvas. It
repaints every tick instead of writing CSS variables, so consider
`setTickRate(30)` when you use it.
#### Colors, path & timing
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `colors` | `string[]` | `['#4285f4', '#9b72cb', '#d96570', '#9b72cb']` | Palette cycled through the quantized blocks. |
| `path` | `'around' \| 'bottom'` | `'around'` | Full perimeter, or a bottom-edge sweep. |
| `place` | `'both' \| 'internal' \| 'external'` | `'both'` | Which side of the border the blocks paint on. |
| `duration` | `number` | `7` | Seconds for one full travel cycle (clamped to 0.1–600). |
| `heads` | `1 \| 2` | `1` | One travelling head, or two. |
| `spin` | `'same' \| 'counter'` | `'same'` | With heads = 2, the two heads co-rotate or counter-rotate. |
| `travelEase` | `'linear' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'back' \| 'bounce'` | `'linear'` | Easing applied to the head's progress along the path. |
| `repeatDelay` | `number` | `0` | Seconds the head rests at the end of the path before the next cycle. |
| `wander` | `number` | `0` | Slow sine warp of progress — the head hesitates and hurries. |
#### Dither grid
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `block` | `number` | `2` | Size of one block cell in px (clamped to 2–64) — the pixel size of the whole effect. |
| `levels` | `number` | `4` | Intensity quantization steps, 2–16. Fewer = chunkier, more posterized. |
| `reach` | `number` | `75` | Glow radius around the head (px, 8–2000). |
| `band` | `number` | `6` | Thickness of the border band the glow hugs (px, 2–400). |
| `bleed` | `number` | `0` | How far the grid extends past the host (px). 0 = auto, wide enough for the edge band's tail to fade out. |
| `corner` | `number` | `0.3` | Superellipse corner exponent for the path (lower = squarer). |
#### Intensity & color
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `strength` | `number` | `2` | Master intensity multiplier (0–4). |
| `saturation` | `number` | `1.6` | Saturation multiplier applied to the palette. |
| `brightness` | `number` | `1` | Brightness multiplier applied to the palette. |
| `hueRange` | `number` | `6` | Degrees of hue oscillation over time (0 = fixed hue). |
| `flicker` | `number` | `0` | Candle-like irregular intensity jitter (0 = steady). |
| `pulseMin` | `number` | `0.9` | Lower bound of the reach pulse. |
| `pulseMax` | `number` | `1.15` | Upper bound of the reach pulse. |
| `hotspots` | `number` | `1` | Extra white-hot cores fanned along the path (1–8). |
| `hotSpread` | `number` | `22` | Spacing (px) between fanned cores. |
#### Hover
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hover` | `'none' \| 'boost'` | `'boost'` | Pointer response — boost brightens and enlarges the glow. |
| `hoverAmount` | `number` | `1` | Strength of the hover response. |
## Notes for coding agents
- The host **must** be `position: relative` and `isolation: isolate`, and the
effect **must** be a child of it. Without the stacking context the glow paints
over your content instead of behind it.
- Keep your own content on a higher layer (`relative z-10`) so it sits above
the glow.
- Replaced elements (``, ``, `