Expressive kinetic typography engine where words and characters animate with coordinated rotation, scale, optical blur, and 3D position trajectories across four presets.
Preview
Customize
7 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add kinetic-typeWrites components/ui/kinetic-type.tsx and components/styles/kinetic-type.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { KineticType } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <KineticType />6 );7}Props
Accessibility
Exposes the full unsplit sentence via aria-label on the semantic heading while marking animated glyph wrappers aria-hidden='true'. Renders static text immediately when prefers-reduced-motion is active.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/kinetic-type.module.css";6 7export interface KineticTypeProps {8 /** Text string to animate */9 text: string;10 /** Coordinated motion preset */11 preset?: "elastic" | "wave" | "orbit" | "snap";12 /** Split granularity */13 splitBy?: "words" | "chars";14 /** Stagger interval in seconds */15 stagger?: number;16 /** Duration per token in seconds */17 duration?: number;18 /** Initial rotation angle in degrees */19 rotationIntensity?: number;20 /** Initial Gaussian blur in pixels */21 blurStrength?: number;22 /** Optional className */23 className?: string;24}25 26export function KineticType({27 text,28 preset = "elastic",29 splitBy = "words",30 stagger = 0.055,31 duration = 0.65,32 rotationIntensity = 14,33 blurStrength = 8,34 className = "",35}: KineticTypeProps) {36 const prefersReducedMotion = useReducedMotion();37 const tokens = React.useMemo(38 () => (splitBy === "chars" ? Array.from(text) : text.split(" ")),39 [text, splitBy]40 );41 42 if (prefersReducedMotion) {43 return <h2 className={`${styles.root} ${className}`.trim()}>{text}</h2>;44 }45 46 const getInitialState = (index: number) => {47 const sign = index % 2 === 0 ? 1 : -1;48 switch (preset) {49 case "wave":50 return {51 opacity: 0,52 y: 26 * sign,53 rotate: rotationIntensity * sign,54 scale: 0.86,55 filter: `blur(${blurStrength}px)`,56 };57 case "orbit":58 return {59 opacity: 0,60 x: -18 * sign,61 y: 22,62 rotate: rotationIntensity * 1.4,63 scale: 0.72,64 filter: `blur(${blurStrength}px)`,65 };66 case "snap":67 return {68 opacity: 0,69 y: 34,70 rotate: -rotationIntensity * 0.5,71 scale: 1.18,72 filter: `blur(${blurStrength}px)`,73 };74 case "elastic":75 default:76 return {77 opacity: 0,78 y: 28,79 rotate: rotationIntensity,80 scale: 0.8,81 filter: `blur(${blurStrength}px)`,82 };83 }84 };85 86 return (87 <h2 aria-label={text} className={`${styles.root} ${className}`.trim()}>88 <span aria-hidden="true" className={styles.stage}>89 {tokens.map((token, idx) => (90 <motion.span91 key={`${token}-${idx}`}92 initial={getInitialState(idx)}93 animate={{94 opacity: 1,95 x: 0,96 y: 0,97 rotate: 0,98 scale: 1,99 filter: "blur(0px)",100 }}101 transition={102 preset === "elastic"103 ? {104 type: "spring",105 stiffness: 260,106 damping: 18,107 delay: idx * stagger,108 }109 : {110 duration,111 delay: idx * stagger,112 ease: [0.16, 1, 0.3, 1],113 }114 }115 className={styles.token}116 >117 {token === " " ? "\u00A0" : token}118 {splitBy === "words" && idx < tokens.length - 1 ? "\u00A0" : ""}119 </motion.span>120 ))}121 </span>122 </h2>123 );124}