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".
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>Works in Vite, Create React App, Next.js (App Router needs the `'use client'` line, already included), Remix. The engine only touches the page inside `useEffect`, so server rendering is safe.
Create Haze.jsx next to the engine:
'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} />;
}Then use it:
import Haze from './Haze';
export default function Page() {
return <Haze options={{ mood: 'auto', season: 'auto' }} style={{ height: '260px' }} />;
}Works in Vite + Vue 3 and Nuxt (wrap it in `<ClientOnly>` if you want to be extra safe; it only touches the page in `onMounted`).
Create Haze.vue next to the engine:
<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>Then use it:
<script setup>
import Haze from './Haze.vue';
</script>
<template>
<Haze :options="{ mood: 'auto', season: 'auto' }" style="height:260px" />
</template>Works in Svelte 4 and 5 (the `export let` style still works in 5) and SvelteKit. Starts in `onMount`, so it never runs on the server.
Create Haze.svelte next to the engine:
<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>Then use it:
<script>
import Haze from './Haze.svelte';
</script>
<Haze options={{ mood: 'auto', season: 'auto' }} style="height:260px" />Works in Solid and SolidStart. Starts in `onMount`, so it never runs on the server.
Create Haze.jsx next to the engine:
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} />;
}Then use it:
import Haze from './Haze';
export default function Page() {
return <Haze options={{ mood: 'auto', season: 'auto' }} style={{ height: '260px' }} />;
}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
- Six moods (dawn, morning, noon, evening, dusk, night) each define three glow colours; four seasons mix a tint into them.
- The colours are CSS variables registered with
@property, so changes cross-fade slowly over 4 seconds instead of jumping. - With
mood: "auto"it re-checks the clock every minute and drifts on by itself. - It is only three radial gradients on one element: very cheap.
Options
Every option is optional. These are the defaults.
| 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) |
Tips
- Whole page:
mountHaze(document.body)once in your layout. - Your content sits above it: the haze uses
z-index: -1inside an isolated box. - Make it subtle:
strength: .15on light themes,.3on dark ones.