Farbverlauf
Geschichtete Radialverläufe in Akzenttönen, ruhend oder langsam ziehend.
Eine von 6 Components in Backgrounds
Variants
Details
Was Farbverlauf mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Backgrounds
- Bereich
- Elements
- Variants
- 4
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/gradient-mesh.tsx
- Bringt mit
- utilskit
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle GradientMeshProps aus components/ui/gradient-mesh.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalGradientMeshVariantsoftvividcornershorizon
soft: a pale cloud of accent tones behind the middle. vivid: stronger, overlapping tones that fill the stage. corners: light gathers in two opposite corners, the middle stays clear. horizon: the tones rise from the bottom edge.
animateoptionalbooleanLet the tones drift slowly, default true
speedoptionalnumberSeconds for one slow drift, default 26
parallaxoptionalnumberPixels the deepest tone drifts against the page over a full scroll pass, the others less. Default 40, 0 is off.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bewegt sich mit dem Scrollen, der Fortschritt folgt der Position auf der Seite.
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 176 Zeilen
'use client';
import { useEffect, useRef } from 'react';
import { cn, prefersReducedMotion } from '@/lib/kit';
export type GradientMeshVariant = 'soft' | 'vivid' | 'corners' | 'horizon';
export interface GradientMeshProps {
/**
* soft: a pale cloud of accent tones behind the middle.
* vivid: stronger, overlapping tones that fill the stage.
* corners: light gathers in two opposite corners, the middle stays clear.
* horizon: the tones rise from the bottom edge.
*/
variant?: GradientMeshVariant;
/** Let the tones drift slowly, default true */
animate?: boolean;
/** Seconds for one slow drift, default 26 */
speed?: number;
/** Pixels the deepest tone drifts against the page over a full scroll pass, the others less. Default 40, 0 is off. */
parallax?: number;
className?: string;
}
/*
* Tones from the accent. The second tone turns the hue a little where the
* browser supports relative colors, otherwise it is a lighter accent. Light
* mode keeps every tone thin so a dark accent never turns into grey fog.
*/
const CSS = `
.kitfx-mesh {
--kitfx-m1: color-mix(in oklab, var(--primary) 20%, transparent);
--kitfx-m2: color-mix(in oklab, var(--primary) 12%, transparent);
--kitfx-m3: color-mix(in oklab, var(--primary) 9%, transparent);
--kitfx-m4: color-mix(in oklch, var(--background) 90%, transparent);
}
:where(.dark) .kitfx-mesh, .kitfx-mesh:where(.dark *) {
--kitfx-m1: color-mix(in oklab, var(--primary) 46%, transparent);
--kitfx-m2: color-mix(in oklch, color-mix(in oklab, var(--primary) 60%, var(--foreground)) 26%, transparent);
--kitfx-m3: color-mix(in oklab, var(--primary) 30%, transparent);
--kitfx-m4: color-mix(in oklch, var(--background) 45%, transparent);
}
@supports (color: oklch(from red l c h)) {
/*
* Lightness is clamped, so a near-black accent never paints dark fog in light
* mode and a near-white one never turns to milk in dark mode. --kitfx-n says
* how neutral the accent is (1 grey default, 0 real color): a neutral accent
* gets a breath of chroma and its tones fan out in hue, a pearl sheen of
* lavender, rose and sky instead of grey clouds. The second tone turns away from
* yellow: warm accents (orange, rose) lean toward red, cool ones toward violet, so a
* dark orange never turns olive.
*/
.kitfx-mesh {
--kitfx-n: clamp(0, 1 - c / 0.06, 1);
--kitfx-m1: oklch(from var(--primary) calc(max(l, 0.64) + 0.06 * var(--kitfx-n)) max(c, 0.085) h / calc(0.22 + 0.1 * var(--kitfx-n)));
--kitfx-m2: oklch(from var(--primary) calc(max(l, 0.68) + 0.06 * var(--kitfx-n)) max(c, 0.075) calc(h - 38 * clamp(-1, cos((h - 75) * 1deg) * 3, 1) + 26 * var(--kitfx-n)) / calc(0.16 + 0.12 * var(--kitfx-n)));
--kitfx-m3: oklch(from var(--primary) calc(max(l, 0.72) + 0.04 * var(--kitfx-n)) max(c, 0.07) calc(h - 80 * var(--kitfx-n)) / calc(0.14 + 0.12 * var(--kitfx-n)));
}
:where(.dark) .kitfx-mesh, .kitfx-mesh:where(.dark *) {
--kitfx-m1: oklch(from var(--primary) calc(min(l, 0.7) - 0.02 * var(--kitfx-n)) max(c, 0.08) h / calc(0.42 + 0.04 * var(--kitfx-n)));
--kitfx-m2: oklch(from var(--primary) min(l, 0.66) max(c, 0.075) calc(h - 38 * clamp(-1, cos((h - 75) * 1deg) * 3, 1) + 26 * var(--kitfx-n)) / 0.3);
--kitfx-m3: oklch(from var(--primary) calc(min(l, 0.6) + 0.06 * var(--kitfx-n)) max(c, 0.07) calc(h - 80 * var(--kitfx-n)) / calc(0.3 + 0.04 * var(--kitfx-n)));
}
}
@keyframes kitfx-mesh-drift {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
33% { transform: translate3d(var(--kitfx-dx), var(--kitfx-dy), 0) scale(1.08); }
66% { transform: translate3d(calc(var(--kitfx-dx) * -0.6), calc(var(--kitfx-dy) * 0.5), 0) scale(0.96); }
}
.kitfx-mesh * { animation-play-state: var(--kitfx-play, running); }
@media (prefers-reduced-motion: reduce) { .kitfx-mesh * { animation: none !important; } .kitfx-mesh-depth { transform: none !important; } }
`;
interface Blob { x: number; y: number; w: number; h: number; tone: 1 | 2 | 3 | 4; dx: number; dy: number; t: number }
const LAYOUT: Record<GradientMeshVariant, Blob[]> = {
soft: [
{ x: 30, y: 38, w: 55, h: 70, tone: 1, dx: 6, dy: 4, t: 1 },
{ x: 68, y: 46, w: 50, h: 64, tone: 2, dx: -5, dy: 5, t: 1.3 },
{ x: 50, y: 70, w: 60, h: 50, tone: 3, dx: 4, dy: -4, t: 1.6 },
{ x: 50, y: 42, w: 34, h: 40, tone: 4, dx: -3, dy: 3, t: 1.2 },
],
vivid: [
{ x: 18, y: 28, w: 62, h: 80, tone: 1, dx: 8, dy: 6, t: 1 },
{ x: 80, y: 30, w: 58, h: 76, tone: 2, dx: -7, dy: 6, t: 1.25 },
{ x: 60, y: 82, w: 70, h: 70, tone: 1, dx: -5, dy: -6, t: 1.5 },
{ x: 26, y: 84, w: 54, h: 60, tone: 3, dx: 6, dy: -4, t: 1.15 },
{ x: 50, y: 48, w: 40, h: 46, tone: 4, dx: 3, dy: 3, t: 1.4 },
],
corners: [
{ x: 4, y: 0, w: 58, h: 80, tone: 1, dx: 5, dy: 5, t: 1 },
{ x: 18, y: 10, w: 36, h: 50, tone: 2, dx: 4, dy: 3, t: 1.3 },
{ x: 96, y: 100, w: 58, h: 80, tone: 1, dx: -5, dy: -5, t: 1.15 },
{ x: 82, y: 92, w: 36, h: 50, tone: 2, dx: -4, dy: -3, t: 1.45 },
],
horizon: [
{ x: 22, y: 104, w: 64, h: 70, tone: 1, dx: 6, dy: -4, t: 1 },
{ x: 54, y: 108, w: 70, h: 64, tone: 2, dx: -6, dy: -5, t: 1.3 },
{ x: 84, y: 102, w: 60, h: 70, tone: 3, dx: -4, dy: -4, t: 1.6 },
{ x: 50, y: 96, w: 46, h: 30, tone: 1, dx: 4, dy: -2, t: 1.15 },
],
};
/**
* Scroll pass. While `on`, writes --kitfx-p on the element: 0 when its top
* meets the bottom of the window, 1 when its bottom leaves at the top. Never
* with reduced motion, the CSS then falls back to the resting value.
*/
function useScrollPass(ref: { current: HTMLElement | null }, on: boolean) {
useEffect(() => {
const el = ref.current;
if (!el || !on || prefersReducedMotion()) return;
let frame = 0;
const update = () => {
frame = 0;
const r = el.getBoundingClientRect();
const p = Math.min(1, Math.max(0, (window.innerHeight - r.top) / (r.height + window.innerHeight)));
el.style.setProperty('--kitfx-p', p.toFixed(4));
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(update);
};
update();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
el.style.removeProperty('--kitfx-p');
};
}, [ref, on]);
}
/**
* Layered radial gradients in accent tones, still or drifting slowly.
* Fills its parent (parent `relative isolate overflow-hidden`, content after
* it with `relative`). Only transforms move, paused out of view, still with
* reduced motion.
*/
export function GradientMesh({ variant = 'soft', animate = true, speed = 26, parallax = 40, className }: GradientMeshProps) {
const ref = useRef<HTMLDivElement>(null);
useScrollPass(ref, parallax !== 0);
const blobs = LAYOUT[variant];
useEffect(() => {
const el = ref.current;
if (!el || !animate || typeof IntersectionObserver === 'undefined') return;
const io = new IntersectionObserver(([e]) => el.style.setProperty('--kitfx-play', e.isIntersecting ? 'running' : 'paused'));
io.observe(el);
return () => io.disconnect();
}, [animate]);
return (
<div ref={ref} aria-hidden data-clip-ok className={cn('kitfx-mesh pointer-events-none absolute inset-0 overflow-hidden', className)}>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
{blobs.map((b, i) => (
// Every tone on its own depth, the later ones nearer, so the cloud gains a little volume when the page moves
<div key={i} className="kitfx-mesh-depth absolute inset-0" style={parallax ? { transform: `translate3d(0, calc((var(--kitfx-p, 0.5) - 0.5) * ${Math.round(parallax * (0.45 + (0.55 * i) / Math.max(1, blobs.length - 1)))}px), 0)` } : undefined}>
<div
className="absolute rounded-full"
style={{
left: `${b.x - b.w / 2}%`,
top: `${b.y - b.h / 2}%`,
width: `${b.w}%`,
height: `${b.h}%`,
background: `radial-gradient(closest-side, var(--kitfx-m${b.tone}), transparent)`,
['--kitfx-dx' as string]: `${b.dx}%`,
['--kitfx-dy' as string]: `${b.dy}%`,
animation: animate ? `kitfx-mesh-drift ${Math.round(speed * b.t)}s ease-in-out ${-i * 4}s infinite` : undefined,
}}
/>
</div>
))}
</div>
);
}Häufige Fragen
Wie installiere ich Farbverlauf?
Mit aiw add @ai-website/gradient-mesh. Der Befehl legt die Datei components/ui/gradient-mesh.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Farbverlauf?
4 Varianten, Weiche Wolke, Kräftig, Ecken und Horizont. Du wählst sie mit der Prop variant, zum Beispiel variant="vivid".
Kann ein KI-Assistent Farbverlauf einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Farbverlauf mit aiw_components und installieren sie mit aiw_add @ai-website/gradient-mesh. Nenn im Prompt die Variante, etwa variant="soft", dann schreibt der Assistent deine Inhalte direkt in die Props.