Zum Inhalt springen
UI Kit
Accent

Polarlicht

Weiche Lichtschleier in der Akzentfarbe, die langsam ziehen. Hell zurückhaltend, dunkel leuchtend.

Eine von 6 Components in Backgrounds

3 Variants

Variants

Sanft von oben

Ein leises Licht hängt von der Oberkante, gemacht für Ankündigungen.

Kräftig in der Mitte

Kräftigere Schleier hinter der Überschrift für einen Produkt-Teaser.

Horizont

Das Licht steigt von der Unterkante auf wie ein Morgenhimmel.

Details

Was Polarlicht mitbringt, aus Registry und Quellcode gelesen.

Kategorie
Backgrounds
Bereich
Elements
Variants
3
Version
1.0.0 · seit 1.0.0
Datei
components/ui/aurora.tsx
Bringt mit
utilskit
npm-Pakete
Keine
Sprachen
Texte kommen über Props
Studio
Im Code anpassen

Props

Die Schnittstelle AuroraProps aus components/ui/aurora.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • intensityoptional
    AuroraIntensity

    softmediumvivid

    How strong the veils are. Light mode always stays quieter than dark.

  • placementoptional
    AuroraPlacement

    topcenterbottom

    Where the light hangs: from the top edge, behind the middle or rising from the bottom

  • speedoptional
    number

    Seconds for one slow sway of the veils, default 22

  • parallaxoptional
    number

    Pixels the veils drift against the page over a full scroll pass, a quiet depth. Default 36, 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, 174 Zeilen
components/ui/aurora.tsx
'use client';

import { useEffect, useRef } from 'react';
import { cn, prefersReducedMotion } from '@/lib/kit';

export type AuroraIntensity = 'soft' | 'medium' | 'vivid';
export type AuroraPlacement = 'top' | 'center' | 'bottom';

export interface AuroraProps {
  /** How strong the veils are. Light mode always stays quieter than dark. */
  intensity?: AuroraIntensity;
  /** Where the light hangs: from the top edge, behind the middle or rising from the bottom */
  placement?: AuroraPlacement;
  /** Seconds for one slow sway of the veils, default 22 */
  speed?: number;
  /** Pixels the veils drift against the page over a full scroll pass, a quiet depth. Default 36, 0 is off. */
  parallax?: number;
  className?: string;
}

const CSS = `
@keyframes kitfx-aurora-sway {
  0%, 100% { transform: translate3d(-5%, 0, 0) rotate(var(--kitfx-r, 0deg)) scaleY(1); }
  50% { transform: translate3d(5%, var(--kitfx-y, 8%), 0) rotate(calc(var(--kitfx-r, 0deg) * -0.6)) scaleY(1.25); }
}
@keyframes kitfx-aurora-breathe { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
.kitfx-aurora * { animation-play-state: var(--kitfx-play, running); }
@media (prefers-reduced-motion: reduce) { .kitfx-aurora * { animation: none !important; } .kitfx-aurora-depth { transform: none !important; } }

/* Tones. Fallback without relative colors: thin mixes of the accent */
.kitfx-aurora {
  --kitfx-a1: color-mix(in oklab, var(--primary) 22%, transparent);
  --kitfx-a2: color-mix(in oklab, var(--primary) 11%, transparent);
  --kitfx-a3: color-mix(in oklab, var(--primary) 16%, transparent);
}
.dark .kitfx-aurora {
  --kitfx-a1: color-mix(in oklab, var(--primary) 50%, transparent);
  --kitfx-a2: color-mix(in oklab, var(--primary) 26%, transparent);
  --kitfx-a3: color-mix(in oklch, color-mix(in oklab, var(--primary) 70%, var(--foreground)) 36%, transparent);
}
/*
 * --kitfx-n says how neutral the accent is: 1 for the grey default, 0 for any
 * real color. Grey veils on white look like smudges, so a neutral accent turns
 * into a pearl light: mid lightness, a breath of chroma, and the veils fan out
 * in hue (lavender, rose, sky). A colored accent keeps its hue and strength.
 */
@supports (color: oklch(from red l c h)) {
  .kitfx-aurora {
    --kitfx-n: clamp(0, 1 - c / 0.06, 1);
    --kitfx-a1: oklch(from var(--primary) calc(l + (0.72 - l) * var(--kitfx-n)) max(c, 0.08) h / calc(0.22 + 0.16 * var(--kitfx-n)));
    --kitfx-a2: oklch(from var(--primary) calc(l + (0.78 - l) * var(--kitfx-n)) max(c, 0.075) calc(h + 60 * var(--kitfx-n)) / calc(0.11 + 0.2 * var(--kitfx-n)));
    --kitfx-a3: oklch(from var(--primary) calc(l + (0.78 - l) * var(--kitfx-n)) max(c, 0.07) calc(h - 78 * var(--kitfx-n)) / calc(0.16 + 0.17 * var(--kitfx-n)));
  }
  .dark .kitfx-aurora {
    --kitfx-a1: oklch(from var(--primary) calc(l + (0.8 - l) * var(--kitfx-n)) max(c, 0.07) h / 0.5);
    --kitfx-a2: oklch(from var(--primary) calc(l + (0.76 - l) * var(--kitfx-n)) max(c, 0.07) calc(h + 60 * var(--kitfx-n)) / calc(0.26 + 0.08 * var(--kitfx-n)));
    --kitfx-a3: oklch(from var(--primary) calc(l + (0.88 - l) * 0.45) max(c * 0.75, 0.06) calc(h - 78 * var(--kitfx-n)) / 0.38);
  }
}
/* In dark mode the veils add light instead of laying a grey film over the page */
.dark .kitfx-aurora { mix-blend-mode: plus-lighter; }
`;

/* Peak strength per intensity, light and dark. Light stays a sheen, never a cloud. */
const INTENSITY: Record<AuroraIntensity, string> = {
  soft: 'opacity-[0.55] dark:opacity-[0.55]',
  medium: 'opacity-[0.8] dark:opacity-[0.78]',
  vivid: 'opacity-[0.9] dark:opacity-[0.88]',
};

interface Ribbon { at: number; h: number; w: number; x: number; r: number; tone: 'a1' | 'a2' | 'a3'; blur: number; t: number; y: number }

/* The veils, measured from the anchored edge (top, middle or bottom) */
const RIBBONS: Ribbon[] = [
  { at: 4, h: 34, w: 120, x: -14, r: -7, tone: 'a2', blur: 46, t: 1, y: 10 },
  { at: 16, h: 18, w: 96, x: -4, r: -4, tone: 'a1', blur: 30, t: 1.35, y: -8 },
  { at: 10, h: 26, w: 80, x: 34, r: 6, tone: 'a3', blur: 40, t: 1.7, y: 12 },
  { at: 26, h: 10, w: 70, x: 10, r: -3, tone: 'a1', blur: 18, t: 1.15, y: 6 },
];

/* Fine vertical rays, like the curtain of a real aurora */
const RAYS = 'repeating-linear-gradient(90deg, #000 0px, rgb(0 0 0 / 0.66) 13px, #000 27px, rgb(0 0 0 / 0.8) 38px, #000 50px, rgb(0 0 0 / 0.58) 61px, #000 74px)';

const EDGE: Record<AuroraPlacement, string> = {
  top: 'radial-gradient(ellipse 80% 90% at 50% 0%, #000 30%, transparent 78%)',
  center: 'radial-gradient(ellipse 72% 52% at 50% 50%, #000 40%, transparent 100%), radial-gradient(ellipse 34% 30% at 50% 50%, rgb(0 0 0 / 0.22), #000)',
  bottom: 'radial-gradient(ellipse 80% 90% at 50% 100%, #000 30%, transparent 78%)',
};

/**
 * 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]);
}

/**
 * Soft moving veils of light in the accent color. Fills its parent
 * (give the parent `relative isolate overflow-hidden`, content after it with
 * `relative`). Decorative, pauses out of view, still image with reduced motion.
 */
export function Aurora({ intensity = 'medium', placement = 'top', speed = 22, parallax = 36, className }: AuroraProps) {
  const ref = useRef<HTMLDivElement>(null);
  useScrollPass(ref, parallax !== 0);
  useEffect(() => {
    const el = ref.current;
    if (!el || typeof IntersectionObserver === 'undefined') return;
    const io = new IntersectionObserver(([e]) => el.style.setProperty('--kitfx-play', e.isIntersecting ? 'running' : 'paused'));
    io.observe(el);
    return () => io.disconnect();
  }, []);
  const edge = EDGE[placement];
  return (
    <div
      ref={ref}
      aria-hidden
      data-clip-ok
      className={cn('kitfx-aurora pointer-events-none absolute inset-0 overflow-hidden', INTENSITY[intensity], placement === 'center' && 'max-sm:opacity-[0.6] max-sm:dark:opacity-[0.55]', className)}
      style={{ maskImage: edge, WebkitMaskImage: edge, maskComposite: 'intersect', WebkitMaskComposite: 'source-in' }}
    >
      <style dangerouslySetInnerHTML={{ __html: CSS }} />
      {/* The veils sit a little deeper than the page: they drift against the scroll */}
      <div className="kitfx-aurora-depth absolute inset-0" style={parallax ? { transform: `translate3d(0, calc((var(--kitfx-p, 0.5) - 0.5) * ${parallax}px), 0)` } : undefined}>
      <div className="absolute inset-x-[-10%] inset-y-0" style={{ maskImage: RAYS, WebkitMaskImage: RAYS, transform: 'skewX(-12deg)' }}>
        {RIBBONS.map((r, i) => {
          const at = placement === 'center' ? { top: `${18 + r.at * 1.3}%` } : placement === 'bottom' ? { bottom: `${r.at}%` } : { top: `${r.at}%` };
          return (
            <div
              key={i}
              className="absolute rounded-[50%]"
              style={{
                ...at,
                left: `${r.x}%`,
                width: `${r.w}%`,
                height: `${r.h}%`,
                background: `linear-gradient(90deg, transparent, var(--kitfx-${r.tone}) 28%, var(--kitfx-${r.tone}) 62%, transparent)`,
                filter: `blur(${r.blur}px)`,
                ['--kitfx-r' as string]: `${r.r}deg`,
                ['--kitfx-y' as string]: `${placement === 'bottom' ? -r.y : r.y}%`,
                transform: `rotate(${r.r}deg)`,
                animation: `kitfx-aurora-sway ${Math.round(speed * r.t)}s ease-in-out ${-i * 5}s infinite, kitfx-aurora-breathe ${Math.round(speed * r.t * 0.8)}s ease-in-out ${-i * 3}s infinite`,
              }}
            />
          );
        })}
      </div>
      </div>
    </div>
  );
}

Häufige Fragen

Wie installiere ich Polarlicht?

Mit aiw add @ai-website/aurora. Der Befehl legt die Datei components/ui/aurora.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Polarlicht?

3 Varianten, Sanft von oben, Kräftig in der Mitte und Horizont.

Kann ein KI-Assistent Polarlicht einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Polarlicht mit aiw_components und installieren sie mit aiw_add @ai-website/aurora.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert