Wechselnde Wörter
Ein Wort im Satz wechselt, der Platz gleitet auf die Breite des nächsten Worts, nichts springt. Pausiert außerhalb des Bildes und bei Hover.
Eine von 5 Components in Textakzente
Details
Was Wechselnde Wörter mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Textakzente
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/rotating-words.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 RotatingWordsProps aus components/ui/rotating-words.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
wordsPflichtstring[]The words that take turns, the first one is the one without JavaScript
effectoptionalRotatingWordsEffectslideblurflip
slide: up and out, up and in (default). blur: crossfade through a blur. flip: a short turn over the x axis.
intervaloptionalnumberMilliseconds a word stays. Default 2400.
toneoptional'inherit' | 'accent'inherit: the text color (default). accent: kit-accent-text.
pilloptionalbooleanA soft accent pill behind the word, its width follows the word
triggeroptional'load' | 'view' | 'in-view' | 'scroll'view (or in-view, default) and load: the words take turns on a clock, paused off screen. scroll: the scroll position picks the word, every word gets its share of the way through the window and the first one returns at the end.
scrollRangeoptional[number, number]With trigger scroll: the part of the way through the window that runs through the words, 0 to 1. Default [0.04, 0.6].
loopoptionalbooleanStart over after the last word. Default true. false stops on the last word.
suffixoptionalstringText glued right after the word that must not wrap away from it, e.g. a comma
labeloptionalstringWhat screen readers hear in place of the words. Default: the words joined with commas.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- 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, 191 Zeilen
'use client';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { cn, prefersReducedMotion, round } from '@/lib/kit';
export type RotatingWordsEffect = 'slide' | 'blur' | 'flip';
export interface RotatingWordsProps {
/** The words that take turns, the first one is the one without JavaScript */
words: string[];
/** slide: up and out, up and in (default). blur: crossfade through a blur. flip: a short turn over the x axis. */
effect?: RotatingWordsEffect;
/** Milliseconds a word stays. Default 2400. */
interval?: number;
/** inherit: the text color (default). accent: kit-accent-text. */
tone?: 'inherit' | 'accent';
/** A soft accent pill behind the word, its width follows the word */
pill?: boolean;
/**
* view (or in-view, default) and load: the words take turns on a clock,
* paused off screen. scroll: the scroll position picks the word, every word
* gets its share of the way through the window and the first one returns
* at the end.
*/
trigger?: 'load' | 'view' | 'in-view' | 'scroll';
/** With trigger scroll: the part of the way through the window that runs through the words, 0 to 1. Default [0.04, 0.6]. */
scrollRange?: [number, number];
/** Start over after the last word. Default true. false stops on the last word. */
loop?: boolean;
/** Text glued right after the word that must not wrap away from it, e.g. a comma */
suffix?: string;
/** What screen readers hear in place of the words. Default: the words joined with commas. */
label?: string;
className?: string;
}
const CSS = `
.kitfx-rw{white-space:nowrap}
.kitfx-rw-slot{display:inline-grid;justify-items:start;align-items:baseline;box-sizing:content-box;overflow:clip;overflow-clip-margin:.25em;transition:width .55s cubic-bezier(.3,.7,.2,1);text-align:start}
.kitfx-rw[data-pill] .kitfx-rw-slot{padding-inline:.28em;margin-inline:.04em;border-radius:.24em;background:color-mix(in oklch,var(--primary) 13%,transparent);box-shadow:inset 0 0 0 1px color-mix(in oklch,var(--primary) 18%,transparent);overflow-clip-margin:0}
.kitfx-rw-word{grid-area:1/1;padding-block:.1em;margin-block:-.1em;transition:transform .6s cubic-bezier(.3,.7,.2,1),opacity .45s ease,filter .5s ease}
.kitfx-rw-word:is([data-state=out],[data-state=next]){opacity:0;pointer-events:none}
.kitfx-rw:not([data-ready]) .kitfx-rw-word:not([data-state=in]){position:absolute;left:0;top:0}
.kitfx-rw[data-effect=slide] .kitfx-rw-word[data-state=out]{transform:translateY(-.75em)}
.kitfx-rw[data-effect=slide] .kitfx-rw-word[data-state=next]{transform:translateY(.75em)}
.kitfx-rw[data-effect=blur] .kitfx-rw-word:is([data-state=out],[data-state=next]){filter:blur(8px)}
.kitfx-rw[data-effect=blur] .kitfx-rw-word[data-state=next]{transform:scale(.96)}
.kitfx-rw[data-effect=flip] .kitfx-rw-slot{perspective:4em}
.kitfx-rw[data-effect=flip] .kitfx-rw-word{transform-origin:50% 50% -.4em;backface-visibility:hidden}
.kitfx-rw[data-effect=flip] .kitfx-rw-word[data-state=out]{transform:rotateX(80deg)}
.kitfx-rw[data-effect=flip] .kitfx-rw-word[data-state=next]{transform:rotateX(-80deg)}
.kitfx-rw-word[data-state=next]{transition:none}
@media (prefers-reduced-motion:reduce){.kitfx-rw-slot,.kitfx-rw-word{transition:none}}`;
/**
* 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], onChange?: (p: number) => void) {
const [start, end] = range;
const listener = useRef(onChange);
listener.current = onChange;
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));
listener.current?.(p);
};
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]);
}
/** Measuring before paint in the browser, a no-op warning free effect on the server */
const useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
/**
* One word in a sentence takes turns with others. The slot animates its
* width to the next word, so the sentence glides instead of jumping. The
* cycle pauses off screen and while the pointer rests on it. Screen readers
* hear all words once; without JavaScript and with reduced motion the first
* word stands still.
* <h1>Websites for <RotatingWords words={['law firms', 'cafés', 'studios']} /></h1>
*/
export function RotatingWords({ words, effect = 'slide', interval = 2400, trigger = 'view', scrollRange = [0.04, 0.6], tone = 'inherit', pill, loop = true, suffix, label, className }: RotatingWordsProps) {
const root = useRef<HTMLSpanElement>(null);
const refs = useRef<(HTMLSpanElement | null)[]>([]);
const [index, setIndex] = useState(0);
const [prev, setPrev] = useState<number | null>(null);
const [widths, setWidths] = useState<number[] | null>(null);
const [onScreen, setOnScreen] = useState(false);
const [hovered, setHovered] = useState(false);
const [still, setStill] = useState(true);
const scroll = trigger === 'scroll';
const shown = useRef(0);
shown.current = index;
// Scroll: the position picks the word, the first one stands again at the end
useScrollLink(root, scroll && words.length > 1, scrollRange, (p) => {
const next = p >= 1 ? 0 : Math.min(words.length - 1, Math.floor(p * words.length));
if (next === shown.current) return;
setPrev(shown.current);
setIndex(next);
shown.current = next;
});
// Measure every word, again when fonts load or the size changes
useIsoLayoutEffect(() => {
// offsetWidth ignores transforms, so a scaled or flipped word never measures too narrow
const measure = () => setWidths(refs.current.map((el) => (el ? round(el.offsetWidth, 1) : 0)));
measure();
const ro = new ResizeObserver(measure);
if (root.current) ro.observe(root.current);
document.fonts?.ready.then(measure).catch(() => {});
return () => ro.disconnect();
}, [words.join('|')]);
useEffect(() => {
if (prefersReducedMotion() || words.length < 2 || scroll) return;
setStill(false);
const el = root.current;
if (!el) return;
const io = new IntersectionObserver(([e]) => setOnScreen(!!e?.isIntersecting));
io.observe(el);
return () => io.disconnect();
}, [words.length, scroll]);
useEffect(() => {
if (still || !onScreen || hovered) return;
if (!loop && index >= words.length - 1) return;
const timer = window.setTimeout(() => {
setPrev(index);
setIndex((index + 1) % words.length);
}, interval);
return () => window.clearTimeout(timer);
}, [still, onScreen, hovered, index, loop, interval, words.length]);
const width = widths?.[index];
return (
<span
ref={root}
className={cn('kitfx-rw', className)}
data-effect={effect}
data-pill={pill ? '' : undefined}
data-ready={widths ? '' : undefined}
onPointerEnter={() => setHovered(true)}
onPointerLeave={() => setHovered(false)}
>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
<span className="sr-only">{label ?? words.join(', ')}</span>
<span aria-hidden data-clip-ok className="kitfx-rw-slot" style={width ? { width: `${width}px` } : undefined}>
{words.map((w, i) => (
<span
key={i}
ref={(el) => {
refs.current[i] = el;
}}
className={cn('kitfx-rw-word', tone === 'accent' && 'kit-accent-text')}
data-state={i === index ? 'in' : i === prev ? 'out' : 'next'}
>
{w}
</span>
))}
</span>
{suffix}
</span>
);
}Häufige Fragen
Wie installiere ich Wechselnde Wörter?
Mit aiw add @ai-website/rotating-words. Der Befehl legt die Datei components/ui/rotating-words.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Wechselnde Wörter?
3 Varianten, Schieben, Pille und Unschärfe und Kippen beim Scrollen.
Kann ein KI-Assistent Wechselnde Wörter einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Wechselnde Wörter mit aiw_components und installieren sie mit aiw_add @ai-website/rotating-words.