# Heat haze

> A mirage warps the air around the type.

- Type: Component (distort)
- Page: https://textlab.dev/components/heat-haze
- Markdown: https://textlab.dev/components/heat-haze.md
- Registry item: https://textlab.dev/r/heat-haze.json
- Source file: `registry/textlab/heat-haze/heat-haze.tsx`
- Dependencies: none
- Variants: haze, boil
- Keywords: heat, haze, mirage, shimmer, turbulence, desert

## When to use

- Travel, summer and desert themes.
- A single atmospheric headline.

## When not to use

- Text people must read closely.
- Multiple instances on one page; each runs a live filter.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/heat-haze
pnpm dlx shadcn@latest add @textlab/heat-haze
```

The `@textlab` name needs `"registries": { "@textlab": "https://textlab.dev/r/{name}.json" }` in `components.json`. Without it, use the full URL:

```bash
npx shadcn@latest add https://textlab.dev/r/heat-haze.json
```

Import the tokens once in your root layout: `import "@/components/textlab/tokens.css";`

### Manual

1. Install the dependencies: `npm install motion@12`
2. Copy `text-fx` (https://textlab.dev/r/text-fx.json) and this component into `components/textlab/`.

## Usage

```tsx
import { HeatHaze } from "@/components/textlab/heat-haze/heat-haze";

export function Example() {
  return <HeatHaze as="h2" text="Midday on the salt flats" />;
}
```

## API reference

### HeatHaze

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"haze" \| "boil"` | `"haze"` | A gentle shimmer, or fast rising air. |
| `strength` | `number` | `6` | Distortion in px. |
| `as` | `ElementType` | `"span"` | Rendered element. |
| `reducedMotion` | `"user" \| "always" \| "never"` | `"user"` | user follows the OS setting; always shows the still version. |
| `className` | `string` | – | Merged onto the root. |

## Accessibility

- The distorted layer is decorative; the text is read from a hidden copy.
- Distortion is a few pixels, so the word stays legible.

## Motion

- Noise rises continuously while its seed steps, so the shimmer never repeats exactly.

## Reduced motion

Plain, still text.

## Performance

- SVG SMIL animation on one filter; no JavaScript loop.
- Filters repaint every frame; use one per screen.

## Notes for AI

- Use only the props documented above. Do not invent props.
- Keep the screen-reader text and the reduced-motion branch when editing the source.
- Pick a variant from: haze, boil.

## Related

- [Liquid text](https://textlab.dev/components/liquid-text.md): Ripples run through the letters under the cursor.
- [Aurora text](https://textlab.dev/components/aurora-text.md): Northern lights drift inside the letters.
- [Ink bleed](https://textlab.dev/components/ink-bleed.md): Text soaks in like ink on paper.

## Also in distort

- [Gooey text](https://textlab.dev/components/gooey-text.md): Words melt into each other.
- [Chromatic text](https://textlab.dev/components/chromatic-text.md): The color channels split and rejoin.
- [Glitch text](https://textlab.dev/components/glitch-text.md): A controlled digital glitch on cue.

## Source

### components/textlab/heat-haze/heat-haze.tsx

```tsx
"use client";

import type { CSSProperties, ElementType } from "react";
import { srOnly } from "../lib/a11y";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useSvgId } from "../lib/use-svg-id";

export type HeatHazeVariant = "haze" | "boil";

export interface HeatHazeProps {
  text: string;
  /** haze shimmers gently, boil rises fast like air over hot asphalt. */
  variant?: HeatHazeVariant;
  /** Distortion strength in px. */
  strength?: number;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

/**
 * A mirage warps the type. Rising turbulence bends the letters most at the top and leaves the
 * baseline nearly still, the way heat shimmers above a road.
 */
export function HeatHaze({ text, variant = "haze", strength = 6, as: Tag = "span", reducedMotion = "user", className, style }: HeatHazeProps) {
  const id = useSvgId("tl-haze");
  const reduced = useReduced(reducedMotion);
  if (reduced) {
    return (
      <Tag className={className} style={style}>
        {text}
      </Tag>
    );
  }
  const boil = variant === "boil";
  return (
    <Tag className={className} style={{ position: "relative", display: "inline-block", ...style }}>
      <svg aria-hidden="true" width="0" height="0" style={{ position: "absolute" }}>
        <defs>
          <filter id={id} x="-5%" y="-20%" width="110%" height="140%">
            <feTurbulence type="fractalNoise" baseFrequency={boil ? "0.02 0.09" : "0.01 0.06"} numOctaves="2" seed="11" result="noise">
              <animate attributeName="seed" values="11;12;13;14;15;16;17;18" dur={boil ? "0.8s" : "2.4s"} repeatCount="indefinite" calcMode="discrete" />
            </feTurbulence>
            <feOffset in="noise" dy="0" result="rising">
              <animate attributeName="dy" from="40" to="-40" dur={boil ? "1.2s" : "4s"} repeatCount="indefinite" />
            </feOffset>
            <feDisplacementMap in="SourceGraphic" in2="rising" scale={boil ? strength * 1.8 : strength} xChannelSelector="R" yChannelSelector="G" />
          </filter>
        </defs>
      </svg>
      <span style={srOnly}>{text}</span>
      <span aria-hidden="true" style={{ display: "inline-block", filter: `url(#${id})`, padding: "0.1em 0.1em", margin: "-0.1em -0.1em" }}>
        {text}
      </span>
    </Tag>
  );
}

export default HeatHaze;
```

Full library index: https://textlab.dev/llms.txt
