Folgendes Licht
Eine Bühne, deren Licht dem Zeiger folgt und auf Touch-Geräten in der Mitte ruht.
Eine von 6 Components in Backgrounds
Variants
Details
Was Folgendes Licht mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Backgrounds
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/spotlight-follow.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 SpotlightFollowProps aus components/ui/spotlight-follow.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von React.HTMLAttributes<HTMLDivElement> an.
sizeoptionalnumberDiameter of the light in px, default 640
toneoptionalSpotlightToneaccentneutral
accent tints the light with the accent color, neutral is plain light
revealoptionalSpotlightRevealnonegriddots
A pattern that only shows where the light falls
intensityoptional'soft' | 'strong'soft for long reading sections, strong for a hero
restoptional{ x: number; y: number }Where the light rests without a pointer (touch, reduced motion, before the first move), in % of the stage
childrenoptionalReact.ReactNode
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, 142 Zeilen
'use client';
import { useEffect, useRef } from 'react';
import { cn, prefersReducedMotion } from '@/lib/kit';
export type SpotlightTone = 'accent' | 'neutral';
export type SpotlightReveal = 'none' | 'grid' | 'dots';
export interface SpotlightFollowProps extends React.HTMLAttributes<HTMLDivElement> {
/** Diameter of the light in px, default 640 */
size?: number;
/** accent tints the light with the accent color, neutral is plain light */
tone?: SpotlightTone;
/** A pattern that only shows where the light falls */
reveal?: SpotlightReveal;
/** soft for long reading sections, strong for a hero */
intensity?: 'soft' | 'strong';
/** Where the light rests without a pointer (touch, reduced motion, before the first move), in % of the stage */
rest?: { x: number; y: number };
children?: React.ReactNode;
}
/* Strength of the glow in dark mode. Light mode always uses white light on the surface. */
const STRENGTH = { soft: 14, strong: 22 };
/**
* A stage whose light follows the pointer. Wraps its children. On touch
* screens the light travels down the stage with the scroll. With reduced
* motion and before the pointer arrives it rests in a fixed place, so the section always looks finished. The light
* moves with transforms only and eases toward the pointer.
*/
export function SpotlightFollow({ size = 640, tone = 'accent', reveal = 'none', intensity = 'soft', rest = { x: 50, y: 38 }, className, children, ...props }: SpotlightFollowProps) {
const stage = useRef<HTMLDivElement>(null);
const light = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = stage.current;
const lamp = light.current;
if (!el || !lamp) return;
if (prefersReducedMotion()) return;
const place = (dx: number, dy: number) => {
lamp.style.transform = `translate3d(${dx.toFixed(1)}px, ${dy.toFixed(1)}px, 0)`;
if (reveal !== 'none') {
const r = el.getBoundingClientRect();
el.style.setProperty('--kitfx-sx', `${((rest.x / 100) * r.width + dx).toFixed(1)}px`);
el.style.setProperty('--kitfx-sy', `${((rest.y / 100) * r.height + dy).toFixed(1)}px`);
}
};
// Touch screens: no pointer to follow, the light travels down the stage with the scroll instead
if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
let raf = 0;
const update = () => {
raf = 0;
const r = el.getBoundingClientRect();
const p = Math.min(1, Math.max(0, (window.innerHeight - r.top) / (r.height + window.innerHeight)));
place(0, (p - 0.5) * r.height * 0.5);
};
const onScroll = () => {
if (!raf) raf = requestAnimationFrame(update);
};
update();
window.addEventListener('scroll', onScroll, { passive: true });
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('scroll', onScroll);
};
}
let frame = 0;
let x = 0, y = 0, tx = 0, ty = 0; // offsets from the resting point in px
const tick = () => {
x += (tx - x) * 0.14;
y += (ty - y) * 0.14;
place(x, y);
frame = Math.abs(tx - x) + Math.abs(ty - y) > 0.4 ? requestAnimationFrame(tick) : 0;
};
const go = () => { if (!frame) frame = requestAnimationFrame(tick); };
const move = (e: PointerEvent) => {
if (e.pointerType === 'touch') return;
const r = el.getBoundingClientRect();
tx = e.clientX - r.left - (rest.x / 100) * r.width;
ty = e.clientY - r.top - (rest.y / 100) * r.height;
go();
};
const leave = () => {
tx = 0;
ty = 0;
go();
};
el.addEventListener('pointermove', move);
el.addEventListener('pointerleave', leave);
return () => {
el.removeEventListener('pointermove', move);
el.removeEventListener('pointerleave', leave);
cancelAnimationFrame(frame);
};
}, [rest.x, rest.y, reveal]);
const s = STRENGTH[intensity];
const color = tone === 'accent' ? 'var(--primary)' : 'var(--foreground)';
// The revealed pattern takes the accent where the light falls
const ink = tone === 'accent' ? 'color-mix(in oklab, var(--primary) 70%, var(--foreground))' : 'var(--foreground)';
const patternOf = (a: number) =>
reveal === 'grid'
? `linear-gradient(to right, color-mix(in oklch, ${ink} ${a}%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in oklch, ${ink} ${a}%, transparent) 1px, transparent 1px)`
: `radial-gradient(color-mix(in oklch, ${ink} ${a * 2}%, transparent) 1px, transparent 1.5px)`;
const tile = reveal === 'grid' ? '48px 48px' : '20px 20px';
const revealMask = `radial-gradient(${Math.round(size * 0.4)}px circle at var(--kitfx-sx, ${rest.x}%) var(--kitfx-sy, ${rest.y}%), #000, transparent)`;
const baseMask = 'radial-gradient(ellipse 80% 80% at 50% 40%, #000, transparent)';
return (
<div ref={stage} {...props} className={cn('relative isolate overflow-hidden bg-surface-2 dark:bg-background', className)}>
<div aria-hidden data-clip-ok className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
{reveal !== 'none' && (
<>
{/* The faint pattern everywhere, and the bright one under the light */}
<div className="absolute inset-0" style={{ backgroundImage: patternOf(6), backgroundSize: tile, backgroundPosition: 'center', maskImage: baseMask, WebkitMaskImage: baseMask }} />
<div className="absolute inset-0" style={{ backgroundImage: patternOf(30), backgroundSize: tile, backgroundPosition: 'center', maskImage: revealMask, WebkitMaskImage: revealMask }} />
</>
)}
<div
ref={light}
className="absolute rounded-full will-change-transform"
style={{ left: `${rest.x}%`, top: `${rest.y}%`, width: size, height: size, marginLeft: -size / 2, marginTop: -size / 2 }}
>
{/* Light mode: white light on a quiet surface. Dark mode: the accent glows. */}
<div
className="absolute inset-0 rounded-full [--kitfx-c:var(--kitfx-cl)] [--kitfx-e:var(--kitfx-el)] dark:[--kitfx-c:var(--kitfx-cd)] dark:[--kitfx-e:var(--kitfx-ed)]"
style={{
['--kitfx-cl' as string]: 'var(--background)',
['--kitfx-el' as string]: 'color-mix(in oklch, var(--background) 55%, transparent)',
['--kitfx-cd' as string]: `color-mix(in oklch, ${color} ${s}%, transparent)`,
['--kitfx-ed' as string]: `color-mix(in oklch, ${color} ${Math.round(s * 0.35)}%, transparent)`,
background: 'radial-gradient(closest-side, var(--kitfx-c), var(--kitfx-e) 50%, transparent)',
}}
/>
</div>
</div>
{children}
</div>
);
}Häufige Fragen
Wie installiere ich Folgendes Licht?
Mit aiw add @ai-website/spotlight-follow. Der Befehl legt die Datei components/ui/spotlight-follow.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Folgendes Licht?
3 Varianten, Weiches Licht, Raster unter dem Licht und Punkte unter dem Licht.
Kann ein KI-Assistent Folgendes Licht einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Folgendes Licht mit aiw_components und installieren sie mit aiw_add @ai-website/spotlight-follow.