Zum Inhalt springen
UI Kit
Accent

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

3 Variants

Variants

Weiches Licht

Reines Licht ohne Muster, ruhig genug für eine Kanzlei.

Raster unter dem Licht

Das Licht legt ein Raster in der Akzentfarbe frei.

Punkte unter dem Licht

Ein breiteres Licht über einem Punktfeld, etwas höher in Ruhe.

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

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.

  • sizeoptional
    number

    Diameter of the light in px, default 640

  • toneoptional
    SpotlightTone

    accentneutral

    accent tints the light with the accent color, neutral is plain light

  • revealoptional
    SpotlightReveal

    nonegriddots

    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

  • childrenoptional
    React.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
components/ui/spotlight-follow.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert