---
name: nikuma-haze
description: Add a soft coloured haze behind content that changes with the visitor's time of day and the season. Use when the user asks for this effect or component in any framework (vanilla JS, Astro, React, Vue, Svelte, Solid).
---

# Haze background

A soft three-colour glow behind your content. In the morning it is gold and pale blue, at golden hour amber and violet, at night indigo with a hint of aurora, and the season tints it (pink in spring, ice-blue in winter). Pick a mood below to preview.

One dependency-free file. `mountHaze(element, options)` builds everything inside `element` and **returns a `stop()` function** that removes it and cleans up all listeners. The framework wrappers below only call it in the right lifecycle hook.

## When to use

The user wants: a glow that follows the time of day and season. Works in plain HTML, Astro, React (incl. Next.js), Vue (incl. Nuxt), Svelte (incl. SvelteKit) and Solid.

## How to add it

1. Save the file(s) under **Source** into the project (for example `src/lib/haze/`), keeping their names. They need no installation.
2. Vanilla / Astro / plain HTML: import the function in a `<script type="module">` and call it with the container element. Other frameworks: create the wrapper from **Framework wrappers**, then render it.
3. Give the container a size where the rules below say so, then check it in both light and dark theme.

```js
import { mountHaze } from './haze.js';
const stop = mountHaze(document.getElementById('stage'), { mood: 'auto', season: 'auto' });
// stop();  // when the element is removed
```

## Options (all optional)

| Option | Default | What it does |
|---|---|---|
| `mood` | `'auto'` | 'auto' (visitor's clock) or dawn / morning / noon / evening / dusk / night |
| `season` | `'auto'` | 'auto' (calendar) or spring / summer / autumn / winter |
| `south` | `false` | true flips the seasons for the southern hemisphere |
| `strength` | `.24` | glow strength from 0 to 1 |
| `fixed` | `false` | true = fixed to the window (used automatically for `document.body`) |

## Rules

- Pass `document.body` for a page background, or any element for a boxed one.
- The glow is behind your content (`z-index: -1`); give opaque cards their own background.
- Call `stop()` before mounting again on the same element (the framework wrappers do this on unmount).
- Do not edit the engine to change behaviour; use options. Respect `prefers-reduced-motion` (the engine does).

## Source

### `haze.js`

```js
// Haze: a soft coloured glow behind your content that follows the time of day and the season.
// mountHaze(el, options) -> stop()   Paints behind the content of `el`. For a whole page use document.body (it is fixed behind everything).
//
// options (all optional):
//   mood      'auto' | 'dawn' | 'morning' | 'noon' | 'evening' | 'dusk' | 'night'   'auto' = the visitor's clock (redrawn every minute)
//   season    'auto' | 'spring' | 'summer' | 'autumn' | 'winter'                  'auto' = the calendar
//   south     true flips the seasons for the southern hemisphere
//   strength  glow strength 0..1 (default .24)
//   fixed     true = fixed to the window (use with document.body)
const css = (id, t) => { if (!document.getElementById(id)) { const s = document.createElement('style'); s.id = id; s.textContent = t; document.head.append(s); } };
// glows are [top-left, top-right, bottom] as r,g,b
const MOODS = {
  dawn: [[255, 160, 140], [255, 205, 150], [190, 150, 255]], morning: [[255, 214, 130], [140, 205, 255], [175, 235, 205]],
  noon: [[110, 185, 255], [170, 220, 255], [210, 238, 255]], evening: [[255, 160, 90], [255, 120, 150], [170, 130, 255]],
  dusk: [[170, 110, 255], [255, 110, 170], [90, 120, 255]], night: [[90, 90, 230], [70, 140, 255], [60, 200, 220]],
};
const SEASONS = { spring: { t: [255, 170, 205], m: .22, k: 1 }, summer: { t: [255, 200, 70], m: .2, k: 1.12 }, autumn: { t: [255, 140, 50], m: .24, k: 1 }, winter: { t: [175, 215, 255], m: .3, k: .88 } };
const moodAt = (h) => (h >= 5 && h < 8 ? 'dawn' : h >= 8 && h < 11 ? 'morning' : h >= 11 && h < 16 ? 'noon' : h >= 16 && h < 19 ? 'evening' : h >= 19 && h < 21 ? 'dusk' : 'night');
const seasonAt = (m, south) => ['spring', 'summer', 'autumn', 'winter'][((south ? 2 : 0) + [3, 3, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3][m]) % 4];

export function mountHaze(el, o = {}) {
  css('hzx-css', `@property --hzx1{syntax:'<color>';inherits:true;initial-value:rgb(170 130 255)}@property --hzx2{syntax:'<color>';inherits:true;initial-value:rgb(255 160 200)}@property --hzx3{syntax:'<color>';inherits:true;initial-value:rgb(120 200 255)}
.hzx{position:absolute;inset:0;z-index:-1;pointer-events:none;overflow:hidden;transition:--hzx1 4s ease,--hzx2 4s ease,--hzx3 4s ease;
  background:radial-gradient(70% 46% at 10% -6%,color-mix(in srgb,var(--hzx1) var(--hzs),transparent),transparent 70%),radial-gradient(60% 42% at 92% -4%,color-mix(in srgb,var(--hzx2) var(--hzs),transparent),transparent 70%),radial-gradient(80% 50% at 50% 112%,color-mix(in srgb,var(--hzx3) var(--hzs),transparent),transparent 72%)}
.hzx.fixed{position:fixed}`);
  if (el !== document.body && getComputedStyle(el).position === 'static') el.style.position = 'relative';
  if (el !== document.body) el.style.isolation = 'isolate';     // so the glow stays behind the content but inside the box
  const box = document.createElement('div'); box.className = 'hzx' + (o.fixed || el === document.body ? ' fixed' : ''); box.setAttribute('aria-hidden', 'true');
  box.style.setProperty('--hzs', Math.round((o.strength ?? .24) * 100) + '%');
  el.prepend(box);
  const paint = () => {
    const now = new Date(), m = o.mood && o.mood !== 'auto' ? o.mood : moodAt(now.getHours() + now.getMinutes() / 60);
    const s = o.season && o.season !== 'auto' ? o.season : seasonAt(now.getMonth(), !!o.south), S = SEASONS[s];
    MOODS[m].map((c) => c.map((v, i) => Math.min(255, Math.round((v * (1 - S.m) + S.t[i] * S.m) * S.k)))).forEach((c, i) => box.style.setProperty('--hzx' + (i + 1), `rgb(${c.join(' ')})`));
    box.dataset.mood = m; box.dataset.season = s;
  };
  paint();
  const timer = !o.mood || o.mood === 'auto' ? setInterval(paint, 60000) : 0;    // drifts on as the day goes by
  return () => { clearInterval(timer); box.remove(); };
}
```

## Framework wrappers

### React: `Haze.jsx`

```jsx
'use client'; // only needed in Next.js
import { useEffect, useRef } from 'react';
import { mountHaze } from './haze.js';

export default function Haze({ options = {}, className, style }) {
  const ref = useRef(null);
  useEffect(() => mountHaze(ref.current, options), []);   // the engine returns its own cleanup
  return <div ref={ref} className={className} style={style} />;
}
```

### Vue: `Haze.vue`

```vue
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { mountHaze } from './haze.js';

const props = defineProps({ options: { type: Object, default: () => ({}) } });
const root = ref(null);
let stop;
onMounted(() => { stop = mountHaze(root.value, props.options); });
onBeforeUnmount(() => stop && stop());
</script>

<template><div ref="root"></div></template>
```

### Svelte: `Haze.svelte`

```svelte
<script>
  import { onMount } from 'svelte';
  import { mountHaze } from './haze.js';

  export let options = {};
  export let style = '';
  let klass = '';
  export { klass as class };
  let root;
  onMount(() => mountHaze(root, options));   // returning the cleanup from onMount stops it on destroy
</script>

<div bind:this={root} class={klass} {style}></div>
```

### Solid: `Haze.jsx`

```jsx
import { onMount, onCleanup } from 'solid-js';
import { mountHaze } from './haze.js';

export default function Haze(props) {
  let root;
  onMount(() => onCleanup(mountHaze(root, props.options || {})));
  return <div ref={root} class={props.class} style={props.style} />;
}
```
