Leuchtende Punkte
Ein Punktfeld, dessen Punkte in der Akzentfarbe aufleuchten, am Zeiger oder in einer wandernden Welle.
Eine von 6 Components in Backgrounds
Variants
Details
Was Leuchtende Punkte mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Backgrounds
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/dot-glow.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 DotGlowProps aus components/ui/dot-glow.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
modeoptionalDotGlowModepointerwaveripple
pointer: dots near the pointer light up (on touch screens the light wanders on its own). wave: a diagonal band of light sweeps across the field. ripple: rings of light spread from the middle.
gapoptionalnumberDistance between dots in px, default 24
radiusoptionalnumberReach of the glow in px, default 170
speedoptionalnumber1 is calm, 2 is twice as fast
clearCenteroptionalbooleanDims the glow behind the middle so a headline stays readable, default true
fadeoptional'edges' | 'none'How far the field fades out toward the edges
scrollLinkoptionalbooleanThe light also moves with the scroll: the wave travels on, the rings spread, the wandering light keeps going. Default false.
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, 200 Zeilen
'use client';
import { useEffect, useRef } from 'react';
import { cn, prefersReducedMotion } from '@/lib/kit';
export type DotGlowMode = 'pointer' | 'wave' | 'ripple';
export interface DotGlowProps {
/**
* pointer: dots near the pointer light up (on touch screens the light wanders on its own).
* wave: a diagonal band of light sweeps across the field.
* ripple: rings of light spread from the middle.
*/
mode?: DotGlowMode;
/** Distance between dots in px, default 24 */
gap?: number;
/** Reach of the glow in px, default 170 */
radius?: number;
/** 1 is calm, 2 is twice as fast */
speed?: number;
/** Dims the glow behind the middle so a headline stays readable, default true */
clearCenter?: boolean;
/** How far the field fades out toward the edges */
fade?: 'edges' | 'none';
/** The light also moves with the scroll: the wave travels on, the rings spread, the wandering light keeps going. Default false. */
scrollLink?: boolean;
className?: string;
}
interface Colors { dot: string; glow: string; dark: boolean }
/** Reads the current accent as a color string the canvas understands */
function readColors(el: HTMLElement): Colors {
const probe = document.createElement('span');
probe.style.cssText = 'position:absolute;visibility:hidden;color:var(--primary)';
el.appendChild(probe);
const glow = getComputedStyle(probe).color;
probe.style.color = 'color-mix(in oklab, var(--primary) 75%, var(--foreground))';
const dot = getComputedStyle(probe).color;
probe.remove();
return { dot, glow, dark: !!el.closest('.dark') };
}
/**
* A dot pattern whose dots glow in the accent color, near the pointer or in
* a moving wave. The plain dots are CSS and render on the server, a canvas
* on top draws only the glowing ones. DPR aware, sized by ResizeObserver,
* paused out of view and in hidden tabs, a still frame with reduced motion.
* Fills its parent and listens to the pointer on the parent, so it never
* blocks clicks.
*/
export function DotGlow({ mode = 'pointer', gap = 24, radius = 170, speed = 1, clearCenter = true, fade = 'edges', scrollLink = false, className }: DotGlowProps) {
const wrap = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const host = wrap.current;
const cv = canvas.current;
const ctx = cv?.getContext('2d');
if (!host || !cv || !ctx) return;
const reduced = prefersReducedMotion();
const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
let colors = readColors(host);
let w = 0, h = 0, dpr = 1;
let frame = 0;
let visible = true;
// Start mid-motion, so the first frame already shows the effect
const t0 = performance.now() - 2600 / speed;
// Pointer state: target and eased position, null while no pointer is over the parent
let target: { x: number; y: number } | null = null;
const pos = { x: 0, y: 0 };
// Scroll adds up to five seconds of motion over a full pass
let pass = 0;
const onScroll = () => {
const r = host.getBoundingClientRect();
pass = Math.min(1, Math.max(0, (window.innerHeight - r.top) / (r.height + window.innerHeight)));
};
if (scrollLink && !reduced) {
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
}
const draw = (time: number) => {
const t = (time / 1000) * speed + (reduced ? 2.6 : 0) + pass * 5;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
const narrow = w < 640;
let px = w / 2, py = h * 0.45;
if (mode === 'pointer') {
const auto = { x: w / 2 + Math.sin(t * 0.45) * w * 0.3, y: h * 0.45 + Math.sin(t * 0.31 + 1) * h * 0.22 };
const goal = target ?? (reduced ? { x: w / 2, y: h * 0.42 } : auto);
pos.x += (goal.x - pos.x) * (target ? 0.16 : 0.05);
pos.y += (goal.y - pos.y) * (target ? 0.16 : 0.05);
px = pos.x;
py = pos.y;
// a soft halo under the dots
const halo = ctx.createRadialGradient(px, py, 0, px, py, radius * 1.3);
halo.addColorStop(0, colors.glow);
halo.addColorStop(1, 'transparent');
ctx.globalAlpha = colors.dark ? 0.1 : 0;
ctx.fillStyle = halo;
ctx.fillRect(px - radius * 1.3, py - radius * 1.3, radius * 2.6, radius * 2.6);
}
const ringR = ((t * 160) % (Math.hypot(w, h) * 0.6 + radius)) - radius / 2;
const band = ((t * 170 + (w + h) * 0.3) % (w * 1.8 + radius * 4)) - radius * 2;
ctx.fillStyle = colors.dot;
for (let y = gap / 2; y < h; y += gap) {
for (let x = gap / 2; x < w; x += gap) {
let v = 0;
if (mode === 'pointer') {
const d = Math.hypot(x - px, y - py);
v = d < radius ? (1 - d / radius) ** 2 : 0;
} else if (mode === 'wave') {
const u = (x + y * 0.8) - band;
v = Math.exp(-((u / (radius * 0.7)) ** 2)) * (0.6 + 0.4 * Math.sin(x * 0.015 + t));
} else {
const d = Math.hypot(x - w / 2, y - h / 2);
const a = Math.exp(-(((d - ringR) / (radius * 0.28)) ** 2));
const b = Math.exp(-(((d - ringR + radius * 1.6) / (radius * 0.28)) ** 2)) * 0.55;
v = Math.max(a, b) * Math.max(0, 1 - d / (Math.hypot(w, h) * 0.55));
}
// On phones the copy fills most of the width, so the quiet middle grows with it
if (clearCenter) v *= 1 - (narrow ? 0.88 : 0.7) * Math.exp(-(((x - w / 2) / (w * (narrow ? 0.46 : 0.24))) ** 2 + ((y - h / 2) / (h * (narrow ? 0.3 : 0.26))) ** 2));
if (v < 0.04) continue;
ctx.globalAlpha = Math.min(1, v * 1.1);
ctx.beginPath();
ctx.arc(x, y, 1 + v * 1.25, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.globalAlpha = 1;
};
const loop = (time: number) => {
draw(time - t0);
frame = requestAnimationFrame(loop);
};
const start = () => {
if (reduced) { draw(0); return; }
if (!frame && visible && !document.hidden) frame = requestAnimationFrame(loop);
};
const stop = () => { cancelAnimationFrame(frame); frame = 0; };
const resize = () => {
const r = host.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio || 1, 2);
w = Math.max(1, Math.round(r.width));
h = Math.max(1, Math.round(r.height));
cv.width = Math.round(w * dpr);
cv.height = Math.round(h * dpr);
if (!pos.x) { pos.x = w / 2; pos.y = h * 0.42; }
if (reduced || !frame) draw(reduced ? 0 : performance.now() - t0);
};
const ro = new ResizeObserver(resize);
ro.observe(host);
resize();
const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; if (visible) start(); else stop(); });
io.observe(host);
const onVis = () => (document.hidden ? stop() : start());
document.addEventListener('visibilitychange', onVis);
// Theme or accent switches change the colors
const mo = new MutationObserver(() => { colors = readColors(host); if (reduced) start(); });
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-accent', 'style'] });
const parent = host.parentElement;
const move = (e: PointerEvent) => {
if (e.pointerType === 'touch' || !finePointer) return;
const r = host.getBoundingClientRect();
target = { x: e.clientX - r.left, y: e.clientY - r.top };
};
const leave = () => { target = null; };
if (mode === 'pointer' && parent && !reduced) {
parent.addEventListener('pointermove', move);
parent.addEventListener('pointerleave', leave);
}
start();
return () => {
stop();
ro.disconnect();
io.disconnect();
mo.disconnect();
document.removeEventListener('visibilitychange', onVis);
window.removeEventListener('scroll', onScroll);
parent?.removeEventListener('pointermove', move);
parent?.removeEventListener('pointerleave', leave);
};
}, [mode, gap, radius, speed, clearCenter, scrollLink]);
const mask = fade === 'edges' ? 'radial-gradient(ellipse 78% 74% at 50% 46%, #000 35%, transparent 82%)' : undefined;
return (
<div ref={wrap} aria-hidden className={cn('pointer-events-none absolute inset-0 overflow-hidden', className)} style={mask ? { maskImage: mask, WebkitMaskImage: mask } : undefined}>
<div
className="absolute inset-0 [--kitfx-dot:color-mix(in_oklch,var(--foreground)_15%,transparent)] dark:[--kitfx-dot:color-mix(in_oklch,var(--foreground)_17%,transparent)]"
style={{ backgroundImage: 'radial-gradient(circle, var(--kitfx-dot) 1px, transparent 1.5px)', backgroundSize: `${gap}px ${gap}px` }}
/>
<canvas ref={canvas} className="absolute inset-0 size-full" />
</div>
);
}Häufige Fragen
Wie installiere ich Leuchtende Punkte?
Mit aiw add @ai-website/dot-glow. Der Befehl legt die Datei components/ui/dot-glow.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Leuchtende Punkte?
3 Varianten, Folgt dem Zeiger, Welle und Kreise.
Kann ein KI-Assistent Leuchtende Punkte einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Leuchtende Punkte mit aiw_components und installieren sie mit aiw_add @ai-website/dot-glow.