---
title: "PrismGlow"
description: "Spectral light cones that rise from an edge and flow left to right."
category: Effects
status: beta
since: 0.6.0
tags: [glow, prism, loading, effect, spectrum]
syncStatus: reviewed
verified: true
---

## Installation

:::TuffCodeBlock{lang="bash"}
---
code: |
  pnpm add @talex-touch/tuffex
---
:::

:::TuffCodeBlock{lang="typescript"}
---
code: |
  import { TxPrismGlow } from '@talex-touch/tuffex/prism-glow'
  import '@talex-touch/tuffex/prism-glow/style.css'
  import '@talex-touch/tuffex/base.css' // tokens + resets, once per app
---
:::

## Usage

### Card
The root is the card: put the background and radius on the root, and the light paints over that background and under the slot.
:::TuffDemoWrapper{demo="PrismGlowShowcaseDemo" code-lang="vue"}
---
code: |
  <template>
    <TxPrismGlow
      class="canvas-card"
      :active="active"
      palette="spectrum"
      placement="bottom"
      :intensity="1"
      :duration="6"
    >
      <strong>Generating canvas</strong>
      <span>Building 3 layers from your prompt</span>
    </TxPrismGlow>
  </template>

  <style scoped>
  /* Background and radius on the root: the light paints over it and under the slot */
  .canvas-card {
    border-radius: 16px;
    background: var(--tx-bg-color-overlay);
  }
  </style>
---
:::

### Collapse on Grow
When the host grows, the light sinks into its edge in about 140ms instead of fading, and stays off until `active` is switched off and on again.
:::TuffDemoWrapper{demo="PrismGlowGrowDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { nextTick, ref } from 'vue'

  const active = ref(true)
  const loaded = ref(false)

  // A collapse holds until active is switched off and on again
  async function reset() {
    loaded.value = false
    active.value = false
    await nextTick()
    active.value = true
  }
  </script>

  <template>
    <TxPrismGlow :active="active">
      <strong>Meeting summary</strong>
      <span role="status">{{ loaded ? '3 key points' : 'Writing the summary…' }}</span>
      <ul v-if="loaded">…</ul>
    </TxPrismGlow>
    <TxButton :disabled="loaded" @click="loaded = true">Load content</TxButton>
    <TxButton :disabled="!loaded" @click="reset">Reset</TxButton>
  </template>
---
:::

### Overlay
Without a slot, it sits behind a host's content, as in the CoreBox search bar. The host must be a stacking context; position the root absolutely at `z-index: -1`.
:::TuffDemoWrapper{demo="PrismGlowSearchDemo" code-lang="vue"}
---
code: |
  <template>
    <form class="search-bar" role="search" @submit.prevent="search">
      <TxPrismGlow class="search-bar__glow" :active="searching" />
      <input aria-label="Search apps, files and commands">
      <TxButton native-type="submit">Search</TxButton>
      <!-- The component announces nothing; the host provides the status text -->
      <span class="sr-only" role="status">{{ searching ? 'Searching' : '' }}</span>
    </form>
  </template>

  <style scoped>
  /* The host is a stacking context: the glow's z-index: -1 lands on its background */
  .search-bar {
    position: relative;
    z-index: 0;
  }

  /* The root's own position: relative has zero specificity, so this wins */
  .search-bar__glow {
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
  }
  </style>
---
:::

### Best Practices

- Turn it on only while work is in progress: set `active` to `true` when it starts and back to `false` when it ends. Don't use it as permanent decoration.
- When the glow sits in a fixed-height part, point `growTarget` at the outer container that grows; turn `collapseOnGrow` off only for hosts that grow while loading.
- The component renders no text and announces nothing; pair it with visible status text or a `role="status"` region (sr-only is fine).
- Put the background on the root or the host, never on the slot, or it covers the light.
- Keep the main text away from the edge the light rises from, and use one per view.

## API Reference

### Props

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `active` | `boolean` | `true` | On / off; fades in over 0.3s, fades out over 0.45s, then unmounts. |
| `palette` | `'spectrum' \| 'accent'` | `'spectrum'` | `spectrum` spans the whole wheel; `accent` uses neighbouring hues of `--tx-color-primary`. |
| `placement` | `'bottom' \| 'top'` | `'bottom'` | The edge the light rises from; it flows left to right either way. |
| `intensity` | `number` | `1` | Light-layer opacity, clamped to 0–1; non-finite counts as `1`. The slot is unaffected. |
| `duration` | `number` | `6` | Base flow period in seconds; smaller is faster. Falls back to `6` unless positive and finite. |
| `collapseOnGrow` | `boolean` | `true` | Sinks the light into its edge in about 140ms when the watched element grows over 8px. |
| `growTarget` | `HTMLElement \| null` | `null` | The element `collapseOnGrow` watches; `null` means the component root. |

### Slots

| Slot | Description |
|------|-------------|
| `default` | Content rendered above the light layer; leave it out to use the component as an overlay. |

### CSS Variables

The props write these into the root's inline style; change them through the props:

| Variable | Source | Description |
|----------|--------|-------------|
| `--tx-prism-glow-intensity` | `intensity` | Light-layer opacity, clamped to 0–1. |
| `--tx-prism-glow-duration` | `duration` | Base flow period, with its `s` unit. |

These are declared on the root, with defaults for light and dark surfaces:

| Variable | Light | Dark | Description |
|----------|-------|------|-------------|
| `--tx-prism-glow-l` / `--tx-prism-glow-c` | `0.76` / `0.19` | `0.74` / `0.19` | oklch lightness and chroma of the cone colour. |
| `--tx-prism-glow-l-core` / `-c-core` / `-a-core` | `0.78` / `0.18` / `0.5` | `0.95` / `0.05` / `0.78` | Lightness, chroma, and alpha of the core along the edge. |
| `--tx-prism-glow-a-halo` | `0.22` | `0.3` | Alpha of the halo. |
| `--tx-prism-glow-a-fringe` | `0.3` | `0.44` | Alpha of the two dispersion fringes. |
| `--tx-prism-glow-a-ray` | `0.05` | `0.07` | Alpha of the vertical rays. |
| `--tx-prism-glow-blend` | `normal` | `plus-lighter` | Blend mode between cones. |
| `--tx-prism-glow-reach` | `1` | `1` | Cone height factor; `0.45` in high contrast, `0` on the leaving layer during a collapse. |

- To override them, use a selector more specific than the component's theme blocks (light is `.tx-prism-glow`, dark is `:is([data-theme='dark'], .dark) .tx-prism-glow`), and give dark its own rule.
- `--tx-pg-*` are internal and not part of the API.

## Overview

- The root, `div.tx-prism-glow`, carries `tx-prism-glow--{palette}` and `tx-prism-glow--{placement}` (plus `is-collapsing` during a collapse) and `isolation: isolate`, making it its own stacking context; its `position: relative` sits in `:where()`, at zero specificity.
- The light layer, `.tx-prism-glow__field`, is `absolute; inset: 0; z-index: -1`: it paints over the root's background and under the slot, is clipped to the root's radius, and is `aria-hidden="true"` with no pointer events.
- While the light is on or fading out, it collapses once the watched element is more than 8px taller than its lowest height since the light came on, pinning the leaving layer to the pre-growth height. Shrinking and changes within 8px never trigger it, and after a collapse it stays off until `active` turns on again.
- Dark is detected from an ancestor: under `[data-theme='dark']` or `.dark` the dark tuning applies (near-white core, `plus-lighter` blending); otherwise the light tuning. There is no theme prop.
- Under reduced motion, every animation and transition stops, the six cones hold one finished frame spread edge to edge, and the layer appears and disappears with `active` at once.
- In high contrast (`html.contrast`, `html[data-tx-contrast='high']`, or `prefers-contrast: more` without `html[data-tx-contrast='normal']`), the cones shrink to a band along the edge and the rays are hidden.

## Technologies

- Motion is compositor-only (`translate`, `scale`, `opacity`); no keyframe reads a `var()` and there is no JS frame loop, so the flow stays smooth while the main thread is busy.
- The look is inspired by Unicorn Studio's "New AI canvas loading state" clip, posted by George Hastings (@soulegit) on 2026-09-23. Only the look was borrowed; no code came from it.
- Source: `packages/tuffex/packages/components/src/prism-glow/`, with the cone parameters in `cones.ts`.

<TuffDocSourceLink label="View source" />

## Use cases

- A loading or working state on a card, input, search bar, or message composer.
- The wait during a long AI generation (a canvas, an image, a long answer).
- The "searching" state of the CoreBox search bar.

## Related components

- [BorderBeam](/docs/dev/components/border-beam): a beam that travels along or breathes around a border; it draws the border, not cones rising from an edge.
- [ThinkingOrb](/docs/dev/components/thinking-orb): a standalone orb with its own `label`; to light up a card or input that is already there, use PrismGlow.
- [GlowText](/docs/dev/components/glow-text): a shine that sweeps across text or a card.
