← Playground

Haze background

A glow that follows the time of day and season

Hand it to your AI assistant

A ready-made skill: a Markdown file with instructions, the options, the full source and the wrappers for every framework. Save it as SKILL.md inside a folder named nikuma-haze in your assistant's skills folder (for Claude Code: .claude/skills/), or paste it into any chat, then say "add a glow that follows the time of day and season to my site".

Download SKILL.md

SKILL.mdpreview
---
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} />;
}
```

Get the code

One small file does all the work and has no dependencies. React, Vue, Svelte and Solid only add a few lines around it. Pick your tool below.

1. Copy the engine (same for every framework)

haze.js4 KB
// 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(); };
}

Save it in your project (for example src/haze/). You never need to edit it: everything is an option.

2. Use it in your project

Plain HTML, Astro, Eleventy, WordPress, anything. No build step needed: a `<script type="module">` is enough.

Use it:

<div id="stage" style="height:260px"></div>

<script type="module">
  import { mountHaze } from './haze.js';

  const stop = mountHaze(document.getElementById('stage'), { mood: 'auto', season: 'auto' });
  // when you are done (for example when a page closes): stop();
</script>

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.

How it works

Options

Every option is optional. These are the defaults.

OptionDefaultWhat it does
mood'auto'‘auto’ (visitor’s clock) or dawn / morning / noon / evening / dusk / night
season'auto'‘auto’ (calendar) or spring / summer / autumn / winter
southfalsetrue flips the seasons for the southern hemisphere
strength.24glow strength from 0 to 1
fixedfalsetrue = fixed to the window (used automatically for document.body)

Tips

Nature does not hurry, yet everything is accomplished.
Lao Tzu