backgrounds
Conic Glow
An ambient rotating conic-gradient glow background layer. Token colour stops (--primary / --accent / --background) form a soft aurora that rotates slowly behind content. A radial-mask + 52 px blur dissolve the hard conic wedge into a tasteful glow field. Decorative (aria-hidden, pointer-events:none, z-0). Place sibling content at z-10. Reduced-motion users receive a static soft glow — same colours, zero motion.
conic-glow
Install
1. Register the namespace (once per project):
json
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
bash
npx shadcn add @bottega/conic-glowUsage
Usagetsx
<ConicGlow />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| speed | number | 20 | Seconds per full rotation. Larger = slower, more meditative. Default 20. |
| intensity | number | 0.6 | Glow opacity 0–1. Controls how vivid the ambient colour field reads. Default 0.6. |
Source
conic-glow.tsxtsx
"use client";
import { useEffect, useRef, useState } from "react";
import { useReducedMotion } from "motion/react";
import type React from "react";
import styles from "./conic-glow.module.css";
export interface ConicGlowProps extends React.HTMLAttributes<HTMLDivElement> {
/** Seconds per full rotation. Larger = slower, more meditative. Default 20. */
speed?: number;
/** Glow opacity 0–1. Controls how vivid the ambient colour field reads. Default 0.6. */
intensity?: number;
}
/**
* Ambient rotating conic-gradient glow background layer.
*
* A large conic gradient (--primary / --accent / --background stops) is centered
* behind content, rotating slowly behind a radial-mask + 52 px blur so it reads
* as a soft aurora/glow, not a hard pinwheel. Token-driven: themes automatically.
*
* Decorative: root carries role="presentation" + aria-hidden + pointer-events:none.
* Place sibling content at z-10 above this z-0 layer.
* Reduced-motion users get a static (non-rotating) conic glow — same look, no motion.
*/
export function ConicGlow({
speed = 20,
intensity = 0.6,
className = "",
style,
"aria-hidden": ariaHidden = true,
...props
}: ConicGlowProps) {
// Mount-gate + null-safe reduce check prevents React #418 hydration mismatch.
// Server + first client render are byte-identical (mounted=false → animation paused).
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const prefersReducedMotion = useReducedMotion();
const reduce = prefersReducedMotion === null || prefersReducedMotion; // null => reduced
const animate = mounted && !reduce;
const glowRef = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(true);
// Pause rotation when the layer is scrolled fully out of view (GPU savings).
useEffect(() => {
const el = glowRef.current;
if (!el || typeof IntersectionObserver === "undefined") return;
const io = new IntersectionObserver(
([entry]) => setVisible(entry.isIntersecting),
{ rootMargin: "0px" },
);
io.observe(el);
return () => io.disconnect();
}, []);
return (
<div
role="presentation"
aria-hidden={ariaHidden}
className={[styles.root, className].filter(Boolean).join(" ")}
style={style}
{...props}
>
{/* Single glow element — always in DOM; animation VALUE (--cg-play) branches,
not the DOM structure, so server + client first render stay byte-identical. */}
<div
ref={glowRef}
className={styles.glow}
style={
{
"--cg-speed": `${speed}s`,
"--cg-intensity": intensity,
// Drives animation-play-state via CSS custom prop — branches VALUES not DOM.
"--cg-play": animate && visible ? "running" : "paused",
} as React.CSSProperties
}
/>
</div>
);
}
Dependencies
- motion
- @bottega/tokens