Zum Inhalt springen
UI Kit
Accent

Scharf werden

Eine ganze Zeile wird von unscharf zu scharf. Sie bleibt echter Text, nur Deckkraft, Filter und Transformation bewegen sich.

Eine von 5 Components in Text Effects

3 Variants

Variants

Weich

Ein leichter Schleier, der etwas aufsteigt, die Einleitung folgt einen Takt später.

Kräftig

Tiefe Unschärfe und ein leichter Zoom für eine kurze Markenzeile.

Folge beim Scrollen

Drei Zeilen werden beim Scrollen nacheinander scharf. Die letzte trägt den Akzent.

Details

Was Scharf werden mitbringt, aus Registry und Quellcode gelesen.

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

Props

Die Schnittstelle BlurInProps aus components/ui/blur-in.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • childrenPflicht
    React.ReactNode

    The line. It stays real text, so screen readers and search read it as it is.

  • asoptional
    Tag

    h1h2h3h4pspandiv

    Element to render, keeps the heading level. Default p.

  • amountoptional
    'soft' | 'strong'

    soft: a light haze (default). strong: deep blur and a small zoom.

  • riseoptional
    boolean

    Rise a little while it sharpens

  • triggeroptional
    BlurInTrigger

    loadviewin-viewhoverscroll

    load: right away. view (or in-view): when it scrolls in (default). hover: again on every hover and focus. scroll: sharpens with the scroll position, from the bottom of the window to about its middle.

  • scrollRangeoptional
    [number, number]

    With trigger scroll: the part of the way through the window that sharpens it, 0 to 1. Default [0.06, 0.46].

  • durationoptional
    number

    Milliseconds the line takes. Default 1000.

  • delayoptional
    number

    Milliseconds before it starts, to follow another line. Default 0.

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, 149 Zeilen
components/ui/blur-in.tsx
'use client';

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

/** in-view is the same as view */
export type BlurInTrigger = 'load' | 'view' | 'in-view' | 'hover' | 'scroll';
type Tag = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'span' | 'div';

export interface BlurInProps {
  /** The line. It stays real text, so screen readers and search read it as it is. */
  children: React.ReactNode;
  /** Element to render, keeps the heading level. Default p. */
  as?: Tag;
  /** soft: a light haze (default). strong: deep blur and a small zoom. */
  amount?: 'soft' | 'strong';
  /** Rise a little while it sharpens */
  rise?: boolean;
  /**
   * load: right away. view (or in-view): when it scrolls in (default).
   * hover: again on every hover and focus. scroll: sharpens with the scroll
   * position, from the bottom of the window to about its middle.
   */
  trigger?: BlurInTrigger;
  /** With trigger scroll: the part of the way through the window that sharpens it, 0 to 1. Default [0.06, 0.46]. */
  scrollRange?: [number, number];
  /** Milliseconds the line takes. Default 1000. */
  duration?: number;
  /** Milliseconds before it starts, to follow another line. Default 0. */
  delay?: number;
  /** Classes for the element, e.g. text-display, text-h1, text-h2, text-lead */
  className?: string;
}

/*
 * Scroll: --kitfx-p comes from the scroll link and is eased once (p * (2 - p)),
 * so the line sharpens quickly at first and settles softly. Without the
 * variable the line stands sharp.
 */
const CSS = `
.kitfx-bi-in{display:block}
span.kitfx-bi>.kitfx-bi-in{display:inline-block}
.kitfx-bi[data-mode=play]>.kitfx-bi-in{animation:var(--kitfx-bi-name) var(--kitfx-bi-dur) cubic-bezier(.2,.7,.2,1) var(--kitfx-bi-delay) both}
.kitfx-bi[data-mode=wait]>.kitfx-bi-in{opacity:0}
.kitfx-bi[data-amount=soft]{--kitfx-bi-name:kitfx-bi-soft;--kitfx-bi-blur:10px;--kitfx-bi-s:0}
.kitfx-bi[data-amount=strong]{--kitfx-bi-name:kitfx-bi-strong;--kitfx-bi-blur:22px;--kitfx-bi-s:.05}
.kitfx-bi[data-amount=soft][data-rise]{--kitfx-bi-name:kitfx-bi-soft-rise}
.kitfx-bi[data-amount=strong][data-rise]{--kitfx-bi-name:kitfx-bi-strong-rise}
.kitfx-bi[data-rise]{--kitfx-bi-y:.3em}
.kitfx-bi[data-mode=scroll]>.kitfx-bi-in{--e:calc(var(--kitfx-p,1) * (2 - var(--kitfx-p,1)));opacity:var(--e);filter:blur(calc((1 - var(--e)) * var(--kitfx-bi-blur)));transform:translateY(calc((1 - var(--e)) * var(--kitfx-bi-y,0em))) scale(calc(1 + (1 - var(--e)) * var(--kitfx-bi-s)));transition:opacity .2s linear,filter .2s linear,transform .2s linear}
@keyframes kitfx-bi-soft{from{opacity:0;filter:blur(10px)}}
@keyframes kitfx-bi-strong{from{opacity:0;filter:blur(22px);transform:scale(1.05)}}
@keyframes kitfx-bi-soft-rise{from{opacity:0;filter:blur(10px);transform:translateY(.3em)}}
@keyframes kitfx-bi-strong-rise{from{opacity:0;filter:blur(22px);transform:translateY(.3em) scale(1.05)}}
@keyframes kitfx-bi-fade{from{opacity:0}}
@media (prefers-reduced-motion:reduce){.kitfx-bi[data-mode]>.kitfx-bi-in{animation:kitfx-bi-fade .5s ease both;opacity:1;filter:none;transform:none}}`;

/** Plays a CSS entrance from the first paint, holds lines further down until they scroll in. */
function usePlay<T extends HTMLElement>(trigger: BlurInTrigger) {
  const ref = useRef<T>(null);
  const [run, setRun] = useState(0);
  const [mode, setMode] = useState<'play' | 'wait' | 'idle' | 'scroll'>(trigger === 'hover' ? 'idle' : trigger === 'scroll' ? 'scroll' : 'play');
  useEffect(() => {
    const el = ref.current;
    if (!el || (trigger !== 'view' && trigger !== 'in-view') || prefersReducedMotion()) return;
    if (el.getBoundingClientRect().top < window.innerHeight * 0.9) return;
    setMode('wait');
    const io = new IntersectionObserver(
      ([e]) => {
        if (!e?.isIntersecting) return;
        setMode('play');
        setRun((r) => r + 1);
        io.disconnect();
      },
      { rootMargin: '0px 0px -10% 0px' },
    );
    io.observe(el);
    return () => io.disconnect();
  }, [trigger]);
  const replay = useCallback(() => {
    if (prefersReducedMotion()) return;
    setMode('play');
    setRun((r) => r + 1);
  }, []);
  return { ref, run, mode, replay };
}

/**
 * Scroll link. Writes --kitfx-p (0 to 1) on the element: 0 when it has
 * travelled `start` of its way through the window, 1 at `end` (0 is its top
 * at the bottom edge, 1 its bottom at the top edge). The value is scaled to
 * the part of that way the page can still scroll, so a line at the end of a
 * short page still finishes. Never with reduced motion, then the CSS
 * fallback 1 keeps the final state.
 */
function useScrollLink(ref: { current: HTMLElement | null }, on: boolean, range: readonly [number, number]) {
  const [start, end] = range;
  useEffect(() => {
    const el = ref.current;
    if (!el || !on || prefersReducedMotion()) return;
    let frame = 0;
    const span = Math.max(0.0001, end - start);
    const at = (top: number, h: number, vh: number) => Math.min(1, Math.max(0, ((vh - top) / (h + vh) - start) / span));
    const update = () => {
      frame = 0;
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight;
      const rest = Math.max(0, document.documentElement.scrollHeight - vh - window.scrollY);
      const most = at(r.top - rest, r.height, vh);
      const p = most < 0.02 ? 1 : Math.min(1, at(r.top, r.height, vh) / most);
      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, start, end]);
}

/**
 * The whole line goes from blurred to sharp. Only opacity, filter and
 * transform move, so nothing around it shifts. Without JavaScript and with
 * reduced motion the line stands sharp (a short fade at most).
 *   <BlurIn as="h1" className="text-display">Café Mira</BlurIn>
 */
export function BlurIn({ children, as: Tag = 'p', amount = 'soft', rise, trigger = 'view', scrollRange = [0.06, 0.46], duration = 1000, delay = 0, className }: BlurInProps) {
  const { ref, run, mode, replay } = usePlay<HTMLElement>(trigger);
  useScrollLink(ref, trigger === 'scroll', scrollRange);
  const vars = { '--kitfx-bi-dur': `${duration}ms`, '--kitfx-bi-delay': `${delay}ms` } as React.CSSProperties;
  const hover = trigger === 'hover' ? { onPointerEnter: replay, onFocus: replay } : {};
  return (
    <>
      <style dangerouslySetInnerHTML={{ __html: CSS }} />
      <Tag ref={ref as never} className={cn('kitfx-bi', className)} data-mode={mode} data-amount={amount} data-rise={rise ? '' : undefined} style={vars} {...hover}>
        <span key={run} className="kitfx-bi-in">
          {children}
        </span>
      </Tag>
    </>
  );
}

Häufige Fragen

Wie installiere ich Scharf werden?

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

Welche Varianten hat Scharf werden?

3 Varianten, Weich, Kräftig und Folge beim Scrollen.

Kann ein KI-Assistent Scharf werden einbauen?

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

Oft gesucht

Direkt zu

In die Zwischenablage kopiert