Color Field Loader
Color field loading animation: a small canvas of two color fields on a ground, breathing under grain and crossfading through any palettes you pass. Three sizes and a grisaille option; client component, reduced motion keeps only the fade.
pnpm dlx shadcn add @01orbital/color-field-loader"use client"
import { useEffect, useState } from "react"
import { cn } from "cn"
import { ORBITAL_PALETTES } from "@/components/orbs/orbital-palettes"
import { PAPER_GRAIN } from "@/components/texture/grain-recipes"
// A breathing color-field canvas that crossfades through palettes.
// Opacity/transform only; reduced motion keeps the opacity breath alone.
const BREATHE_CSS = `
@keyframes color-field-breathe {
0%, 100% { opacity: 0.78; transform: scale(0.985); }
50% { opacity: 1; transform: scale(1); }
}
[data-slot="color-field-loader"] [data-field] {
animation: color-field-breathe 4s ease-in-out infinite;
}
@keyframes color-field-breathe-still {
0%, 100% { opacity: 0.78; }
50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
[data-slot="color-field-loader"] [data-field] { animation-name: color-field-breathe-still; }
}
`
const SIZES = {
sm: { width: 60, height: 80, box: "gap-1 p-1.5" },
md: { width: 120, height: 160, box: "gap-1.5 p-2.5" },
lg: { width: 200, height: 260, box: "gap-2.5 p-4" },
} as const
type ColorFieldPalette = { ground: string; upper: string; lower: string }
// The palette is the knob: the ground and the two fields painted on it, each
// a flat color. By default the canvas steps through the four palettes it
// shipped with: gold (once called apogee), vesper, meridian and plaster.
// Gold, vesper and plaster come from the shared Orbital Palettes; meridian
// lives here. Only each field's core color is used, so the fields read as
// paint, not glows.
const core = (field: string, fallback: string) =>
field.match(/oklch\([^)]+\)/)?.[0] ?? fallback
const fromOrbital = (name: "gold" | "vesper" | "plaster") => {
const { ground, upper, lower } = ORBITAL_PALETTES[name]
return { ground, upper: core(upper, ground), lower: core(lower, ground) }
}
const DEFAULT_PALETTES: ColorFieldPalette[] = [
fromOrbital("gold"),
fromOrbital("vesper"),
{
ground: "oklch(0.436 0.110 259.5)",
upper: "oklch(0.627 0.214 26.0)",
lower: "oklch(0.477 0.080 189.0)",
},
fromOrbital("plaster"),
]
/**
* A small color-field canvas, two soft fields on a ground, that breathes
* while content loads and can crossfade through a list of palettes.
*
* Exposes `role="status"` with the label "Loading"; pass `aria-label` to
* change it. The fields breathe in opacity and scale; with reduced motion
* only the opacity changes.
*/
function ColorFieldLoader({
size = "md",
cyclePalettes = true,
paletteOrder = DEFAULT_PALETTES,
cycleSpeed = 6000,
colored = true,
className,
style,
...props
}: React.ComponentProps<"div"> & {
/** Canvas size: `sm` is 60 by 80px, `md` 120 by 160px, `lg` 200 by 260px. */
size?: keyof typeof SIZES
/** Steps through `paletteOrder` on a timer. Needs at least two palettes. */
cyclePalettes?: boolean
/**
* The palettes to show, in order, each a `ground` plus `upper` and `lower`
* field colors. Defaults to gold, vesper, meridian and plaster.
*/
paletteOrder?: ColorFieldPalette[]
/** Milliseconds per palette, 6000 by default. */
cycleSpeed?: number
/**
* When false, paints the palette in grays of the same lightness, a
* grisaille underpainting. Color changes fade over 1.5s.
*/
colored?: boolean
}) {
const [current, setCurrent] = useState(0)
useEffect(() => {
if (!cyclePalettes || paletteOrder.length < 2) return
const interval = setInterval(() => {
setCurrent((i) => (i + 1) % paletteOrder.length)
}, cycleSpeed)
return () => clearInterval(interval)
}, [cyclePalettes, paletteOrder.length, cycleSpeed])
const palette = paletteOrder[current] ?? DEFAULT_PALETTES[0]
// Grisaille keeps each color's lightness and drops its chroma.
const paint = (color: string) =>
colored ? color : `oklch(from ${color} l 0 h)`
const fields = {
ground: paint(palette.ground),
upper: paint(palette.upper),
lower: paint(palette.lower),
}
const s = SIZES[size]
return (
<div
role="status"
aria-label="Loading"
className={cn(
"relative flex flex-col overflow-hidden rounded-sm",
s.box,
className
)}
style={{
width: s.width,
height: s.height,
background: fields.ground,
transition: "background 1.5s ease",
...style,
}}
// After the spread: the breathing CSS above selects on this slot.
data-slot="color-field-loader"
{...props}
>
<style>{BREATHE_CSS}</style>
{[fields.upper, fields.lower].map((color, i) => (
<div
key={i}
data-field
className={cn("rounded-[3px]", i === 0 ? "flex-1" : "flex-2")}
style={{
background: color,
transition: "background 1.5s ease",
animationDelay: `${i * 1.2}s`,
}}
/>
))}
<div
aria-hidden
className="absolute inset-0 opacity-30 mix-blend-overlay"
style={{ backgroundImage: PAPER_GRAIN }}
/>
</div>
)
}
export { ColorFieldLoader }
// The grain tiles the kit paints with, as data URIs, each held once. Every
// recipe is a tiny SVG turbulence filter; the component using it chooses
// the tile size, opacity and blend. `*_URI` is the bare image for callers
// that wrap it themselves; the unsuffixed name is ready for `background-image`.
/** Stylized paper: coarse turbulence pushed to high contrast, the grain under most of the painted scenes and the Grain `paper` variant. */
export const PAPER_GRAIN_URI =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='250' viewBox='0 0 100 100'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='turbulence' baseFrequency='1.4' numOctaves='1' seed='2' stitchTiles='stitch' result='n'/%3E%3CfeComponentTransfer result='g'%3E%3CfeFuncR type='linear' slope='4' intercept='1'/%3E%3CfeFuncG type='linear' slope='4' intercept='1'/%3E%3CfeFuncB type='linear' slope='4' intercept='1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='saturate' values='0' in='g'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const PAPER_GRAIN = `url("${PAPER_GRAIN_URI}")`
/** Sandpaper: finer, two-octave turbulence, the Grain `sandpaper` variant and the pours and pools. */
export const SANDPAPER_GRAIN_URI =
"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 100 100'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='turbulence' baseFrequency='2.2' numOctaves='2' seed='5' stitchTiles='stitch' result='n'/%3E%3CfeComponentTransfer result='g'%3E%3CfeFuncR type='linear' slope='2.5' intercept='0.8'/%3E%3CfeFuncG type='linear' slope='2.5' intercept='0.8'/%3E%3CfeFuncB type='linear' slope='2.5' intercept='0.8'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='saturate' values='0' in='g'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const SANDPAPER_GRAIN = `url("${SANDPAPER_GRAIN_URI}")`
/** Square: three-octave turbulence at high contrast, the Grain `square` variant. */
export const SQUARE_GRAIN_URI =
"data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22200%22%20height=%22200%22%20viewBox=%220%200%20100%20100%22%3E%3Cfilter%20id=%22n%22%3E%3CfeTurbulence%20type=%22turbulence%22%20baseFrequency=%222.1%22%20numOctaves=%223%22%20seed=%2211%22%20stitchTiles=%22stitch%22%20result=%22n%22/%3E%3CfeComponentTransfer%20result=%22g%22%3E%3CfeFuncR%20type=%22linear%22%20slope=%225.5%22%20intercept=%221.4%22/%3E%3CfeFuncG%20type=%22linear%22%20slope=%225.5%22%20intercept=%221.4%22/%3E%3CfeFuncB%20type=%22linear%22%20slope=%225.5%22%20intercept=%221.4%22/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix%20type=%22saturate%22%20values=%220%22%20in=%22g%22/%3E%3C/filter%3E%3Crect%20width=%22100%%22%20height=%22100%%22%20filter=%22url(%23n)%22/%3E%3C/svg%3E"
export const SQUARE_GRAIN = `url("${SQUARE_GRAIN_URI}")`
/** Plain fractal noise on a 200px tile, the Grain `single` variant. */
export const SINGLE_NOISE_URI =
"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const SINGLE_NOISE = `url("${SINGLE_NOISE_URI}")`
/** Fine fractal noise on a 512px tile, SoftGrain and the Limb scene. */
export const SOFT_NOISE_URI =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='5' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const SOFT_NOISE = `url("${SOFT_NOISE_URI}")`
/** Fine fractal noise the field scenes lay over their art: Fracture Field, Gestural Zip, Solar Flare and Underwater Shaft. */
export const FIELD_NOISE_URI =
"data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const FIELD_NOISE = `url("${FIELD_NOISE_URI}")`
/** Fine fractal noise on a 256px tile, the mist over the radial charts. */
export const CHART_NOISE_URI =
"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const CHART_NOISE = `url("${CHART_NOISE_URI}")`
/** A one-octave dither on a 128px tile, the glass of the neon signs. */
export const DITHER_NOISE_URI =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"
export const DITHER_NOISE = `url("${DITHER_NOISE_URI}")`
/** A thin film: paper turbulence with its alpha cut to 0.6, the grain GlowSection lays over its light. */
export const GLOW_FILM_URI =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='250'%3E%3Cfilter id='sf'%3E%3CfeTurbulence type='turbulence' baseFrequency='1.4' numOctaves='1' seed='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.6'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23sf)'/%3E%3C/svg%3E"
export const GLOW_FILM = `url("${GLOW_FILM_URI}")`
/** Plain fractal noise on a 400px tile, the canvas finish of ColorField and FeltOrb. */
export const CANVAS_NOISE_URI =
"data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"
export const CANVAS_NOISE = `url("${CANVAS_NOISE_URI}")`
import { ColorFieldLoader } from "@/components/loader/color-field-loader"
export default function ColorFieldLoaderDemo() {
return <ColorFieldLoader size="md" />
}
// Named palettes for the painted fields: a ground with an upper and a lower
// field blurred over it, the way Orbital paints a ball and ColorWash paints
// a panel. Plain data with no imports, so any component can read it. Pass a
// name to ColorWash as `color`, or an entry (or your own object of the same
// shape) to Orbital or ColorWash as `palette`.
type OrbitalPalette = {
/** The base color under everything else, any CSS color. */
ground: string
/** A CSS background, usually a radial gradient, blurred over the top. */
upper: string
/** A CSS background blurred over the bottom. */
lower: string
/** The orb's glow: an inner bloom and a halo. ColorWash ignores it. */
glow?: string
/** The orb's preferred shading tier. ColorWash ignores it. */
form?: "off" | "medium" | "strong"
/** Read by nothing today; kept so older palettes still type-check. */
period?: "nocturne"
}
type OrbitalPaletteName =
| "gold"
| "delft"
| "warmth"
| "marigold"
| "vesper"
| "aurora"
| "gesso"
| "plaster"
| "moss"
| "tidepool"
| "vermillion"
| "cocoa"
| "linen"
| "tarn"
| "gold-dark"
| "mint"
| "rose"
| "sky"
| "lilac"
const ORBITAL_PALETTES: Record<OrbitalPaletteName, OrbitalPalette> = {
gold: {
ground: "oklch(0.315 0.064 270.3)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.875 0.097 100.4) 0%, oklch(0.850 0.101 100.7 / 0.7) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.365 0.073 269.3) 0%, oklch(0.365 0.073 269.3 / 0.8) 56%, transparent 100%)",
glow: "oklch(0.936 0.055 102.9)",
form: "strong",
},
delft: {
ground: "oklch(0.376 0.087 261.9)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.905 0.025 104.4) 0%, oklch(0.887 0.026 104.5 / 0.72) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.467 0.121 261.9) 0%, oklch(0.467 0.121 261.9 / 0.8) 55%, transparent 100%)",
glow: "oklch(0.978 0.010 261.8)",
form: "strong",
},
warmth: {
ground: "oklch(0.652 0.147 52.4)",
upper:
"radial-gradient(ellipse 130% 100% at 50% 35%, oklch(0.836 0.159 96.2) 0%, oklch(0.814 0.157 92.4 / 0.7) 55%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 90% at 50% 80%, oklch(0.518 0.176 28.9) 0%, oklch(0.487 0.167 27.7 / 0.8) 55%, transparent 100%)",
glow: "oklch(0.836 0.159 96.2)",
},
marigold: {
ground: "oklch(0.379 0.076 35.6)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.805 0.160 71.9) 0%, oklch(0.805 0.160 71.9 / 0.75) 53%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.243 0.023 32.4) 0%, oklch(0.243 0.023 32.4 / 0.88) 56%, transparent 100%)",
glow: "oklch(0.829 0.154 76.5)",
form: "strong",
},
vesper: {
ground: "oklch(0.28 0.06 300)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.90 0.04 55) 0%, oklch(0.90 0.04 55 / 0.7) 53%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.42 0.13 305) 0%, oklch(0.42 0.13 305 / 0.85) 56%, transparent 100%)",
glow: "oklch(0.82 0.07 50)",
form: "strong",
},
aurora: {
ground: "oklch(0.318 0.057 158.2)",
upper:
"radial-gradient(ellipse 140% 110% at 50% 45%, oklch(0.890 0.129 146.1) 0%, oklch(0.890 0.129 146.1 / 0.5) 55%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 100% at 50% 55%, oklch(0.236 0.055 296.3) 0%, oklch(0.236 0.055 296.3 / 0.5) 55%, transparent 100%)",
},
gesso: {
ground: "oklch(0.563 0.167 30.8)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.919 0.014 53.3) 0%, oklch(0.907 0.011 45.7 / 0.75) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.621 0.161 28.1) 0%, oklch(0.621 0.161 28.1 / 0.8) 55%, transparent 100%)",
glow: "oklch(0.949 0.014 53.3)",
form: "strong",
},
plaster: {
ground: "oklch(0.728 0.030 93.8)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.787 0.035 68.7) 0%, oklch(0.787 0.035 68.7 / 0.65) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.547 0.181 27.1) 0%, oklch(0.516 0.172 26.6 / 0.75) 55%, transparent 100%)",
glow: "oklch(0.561 0.158 29.1)",
form: "strong",
},
moss: {
ground: "oklch(0.313 0.006 34.3)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.561 0.039 200.7) 0%, oklch(0.507 0.038 203.1 / 0.7) 55%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.373 0.047 214.5) 0%, oklch(0.395 0.046 210.5 / 0.8) 56%, transparent 100%)",
glow: "oklch(0.659 0.131 237.7)",
form: "medium",
},
tidepool: {
ground: "oklch(0.179 0.029 240.3)",
upper:
"radial-gradient(ellipse 140% 110% at 50% 45%, oklch(0.413 0.082 166.3) 0%, oklch(0.413 0.082 166.3 / 0.5) 55%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 100% at 50% 55%, oklch(0.962 0.077 113.3) 0%, oklch(0.962 0.077 113.3 / 0.5) 55%, transparent 100%)",
},
vermillion: {
ground: "oklch(0.235 0.06 18.06)",
upper:
"radial-gradient(ellipse 130% 100% at 50% 38%, oklch(0.66 0.205 30.97) 0%, oklch(0.619 0.205 30.96 / 60%) 55%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 90% at 50% 82%, oklch(0.28 0.136 266.87) 0%, oklch(0.25 0.136 267.22 / 70%) 55%, transparent 100%)",
},
cocoa: {
ground: "oklch(0.306 0.066 45.1)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.794 0.054 64.1) 0%, oklch(0.794 0.054 64.1 / 0.75) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.457 0.115 42.0) 0%, oklch(0.457 0.115 42.0 / 0.84) 56%, transparent 100%)",
glow: "oklch(0.762 0.120 49.2)",
form: "strong",
},
linen: {
ground: "oklch(0.267 0.021 54.1)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.782 0.021 58.0) 0%, oklch(0.782 0.021 58.0 / 0.75) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.363 0.028 53.6) 0%, oklch(0.363 0.028 53.6 / 0.85) 56%, transparent 100%)",
glow: "oklch(0.994 0.000 89.9)",
form: "strong",
},
tarn: {
ground: "oklch(0.286 0.036 189.5)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.757 0.155 95.6) 0%, oklch(0.757 0.155 95.6 / 0.72) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.468 0.065 190.9) 0%, oklch(0.468 0.065 190.9 / 0.82) 56%, transparent 100%)",
glow: "oklch(0.831 0.167 96.2)",
form: "strong",
},
"gold-dark": {
ground: "oklch(0.16 0.04 270)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.29 0.06 100) 0%, oklch(0.27 0.06 101 / 0.7) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.23 0.07 269) 0%, oklch(0.23 0.07 269 / 0.8) 56%, transparent 100%)",
},
mint: {
ground: "oklch(0.315 0.06368 314.9)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.875 0.09651 145.0) 0%, oklch(0.850 0.10049 145.3 / 0.7) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.365 0.07263 313.9) 0%, oklch(0.365 0.07263 313.9 / 0.8) 56%, transparent 100%)",
},
rose: {
ground: "oklch(0.315 0.04852 149.9)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.875 0.07355 340.0) 0%, oklch(0.850 0.07658 340.3 / 0.7) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.365 0.05535 148.9) 0%, oklch(0.365 0.05535 148.9 / 0.8) 56%, transparent 100%)",
},
sky: {
ground: "oklch(0.315 0.04989 29.9)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.875 0.07562 220.0) 0%, oklch(0.850 0.07874 220.3 / 0.7) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.365 0.05691 28.9) 0%, oklch(0.365 0.05691 28.9 / 0.8) 56%, transparent 100%)",
},
lilac: {
ground: "oklch(0.315 0.03869 99.9)",
upper:
"radial-gradient(ellipse 135% 105% at 50% 35%, oklch(0.875 0.05864 290.0) 0%, oklch(0.850 0.06106 290.3 / 0.7) 52%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 95% at 50% 80%, oklch(0.365 0.04413 98.9) 0%, oklch(0.365 0.04413 98.9 / 0.8) 56%, transparent 100%)",
},
}
// One line per palette, for pickers and captions.
const ORBITAL_PALETTE_NOTES: Record<OrbitalPaletteName, string> = {
gold: "pale gold over indigo, the Orbital default",
delft: "cream over blue",
warmth: "gold over terracotta",
marigold: "gold over rust",
vesper: "pale peach over violet",
aurora: "mint over deep green",
gesso: "warm white over red-orange",
plaster: "rose over plaster",
moss: "sky over warm grey",
tidepool: "teal over deep blue",
vermillion: "cream over oxblood",
cocoa: "pale sand over cocoa",
linen: "pale sand over umber",
tarn: "gold over deep teal",
"gold-dark": "dim gold over near-black blue",
mint: "pale mint over plum",
rose: "pale pink over deep green",
sky: "pale blue over dark brown",
lilac: "pale lilac over dark olive",
}
export { ORBITAL_PALETTES, ORBITAL_PALETTE_NOTES }
export type { OrbitalPalette, OrbitalPaletteName }
Usage
import { ColorFieldLoader } from "@/components/loader/color-field-loader"
export default function ColorFieldLoaderDemo() {
return <ColorFieldLoader size="md" />
}Examples
New
color-field-loader-framed-color-studies
API reference
A small color-field canvas, two soft fields on a ground, that breathes while content loads and can crossfade through a list of palettes.
Exposes role="status" with the label "Loading"; pass aria-label to change it. The fields breathe in opacity and scale; with reduced motion only the opacity changes.
Prop
Type
It also accepts the HTML attributes of the element it renders.
Dependencies
npm: cn. The CLI installs them for you.
Registry pieces it installs with it: