Animierte Icons
Sechzehn Icons, die sich mit Sinn bewegen, wenn du auf sie oder ihren Knopf zeigst. Gleiches Raster und gleicher Strich wie die Kit-Icons.
Eine von 4 Components in Mikroanimationen
Variants
Details
Was Animierte Icons mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Mikroanimationen
- Bereich
- Visuals
- Variants
- 6
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/animated-icon.tsx
- Bringt mit
- utilskitmotion-scene
- 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 AnimatedIconProps aus components/ui/animated-icon.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
namePflichtAnimatedIconNamebelluploaddownloadsendheartcheckrefreshsearchmailstartrashcopylockeyecartsparkle
sizeoptionalnumberSize in px. Default 24.
triggeroptional'hover' | 'loop' | 'in-view'hover: plays when the icon or its parent is hovered or focused (default). loop: plays on its own with a rest. in-view: once when it appears.
activeoptionalbooleanFilled state for heart, star and sparkle, and the accent color for all
speedoptionalnumber1 is the designed tempo
strokeWidthoptionalnumberlabeloptionalstringRead to screen readers. Without it the icon is decorative, which is right inside a labelled button.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle label.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 242 Zeilen
'use client';
import { cn, round } from '@/lib/kit';
import { SCENE_CSS, keyframes, useSceneId, useScenePlay } from '@/components/ui/motion-scene';
/*
* Icons that move with meaning: the bell swings, the arrow leaves the tray,
* the plane takes off, the heart beats. Drawn on the 24 px grid of the kit
* icons (1.6 px stroke, round caps), animated with transform, opacity and
* stroke-dashoffset only. Hover plays them on the icon itself or on its
* parent (a button or link), focus-visible too, so keyboard users see the
* same. A still icon is always complete.
*/
export type AnimatedIconName = 'bell' | 'upload' | 'download' | 'send' | 'heart' | 'check' | 'refresh' | 'search' | 'mail' | 'star' | 'trash' | 'copy' | 'lock' | 'eye' | 'cart' | 'sparkle';
interface Part {
d: string;
/** Which motion this part follows */
m?: string;
/** Filled when active (heart, star) */
fill?: boolean;
origin?: string;
/** Draw on with stroke-dashoffset */
draw?: boolean;
/** Hidden when still, only shows while the motion plays */
hidden?: boolean;
}
interface Def {
parts: Part[];
/** Motions in percent of one play: name -> frames */
motions: Record<string, [number, string][]>;
/** Seconds of one play */
dur: number;
}
const ICONS: Record<AnimatedIconName, Def> = {
bell: {
dur: 1,
parts: [
{ d: 'M6.5 16.5V11a5.5 5.5 0 0 1 11 0v5.5l1.5 2h-14Z', m: 'swing', origin: '12px 3.5px' },
{ d: 'M10 20.5a2 2 0 0 0 4 0', m: 'clap', origin: '12px 3.5px' },
{ d: 'M12 3.5v2', m: 'swing', origin: '12px 3.5px' },
],
motions: {
swing: [[0, 'transform:rotate(0)'], [15, 'transform:rotate(16deg)'], [32, 'transform:rotate(-13deg)'], [48, 'transform:rotate(9deg)'], [64, 'transform:rotate(-5deg)'], [80, 'transform:rotate(2deg)'], [100, 'transform:rotate(0)']],
clap: [[0, 'transform:rotate(0)'], [20, 'transform:rotate(-12deg)'], [38, 'transform:rotate(16deg)'], [55, 'transform:rotate(-9deg)'], [72, 'transform:rotate(5deg)'], [100, 'transform:rotate(0)']],
},
},
upload: {
dur: 0.9,
parts: [
{ d: 'M4.5 14.5v3a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-3' },
{ d: 'M12 15V4.5M7.5 9 12 4.5 16.5 9', m: 'up' },
],
motions: { up: [[0, 'transform:translateY(0);opacity:1'], [40, 'transform:translateY(-7px);opacity:0'], [41, 'transform:translateY(7px);opacity:0'], [100, 'transform:translateY(0);opacity:1']] },
},
download: {
dur: 0.9,
parts: [
{ d: 'M4.5 14.5v3a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-3' },
{ d: 'M12 4.5V15M7.5 10.5 12 15l4.5-4.5', m: 'down' },
],
motions: { down: [[0, 'transform:translateY(0);opacity:1'], [40, 'transform:translateY(7px);opacity:0'], [41, 'transform:translateY(-7px);opacity:0'], [100, 'transform:translateY(0);opacity:1']] },
},
send: {
dur: 1,
parts: [{ d: 'M20.5 3.5 3.5 10.5l7 3 3 7Zm0 0-10 10', m: 'fly' }],
motions: {
fly: [[0, 'transform:translate(0,0) scale(1);opacity:1'], [12, 'transform:translate(-1.5px,1.5px) scale(.95);opacity:1'], [42, 'transform:translate(12px,-12px) scale(.8);opacity:0'], [43, 'transform:translate(-12px,12px) scale(.8);opacity:0'], [100, 'transform:translate(0,0) scale(1);opacity:1']],
},
},
heart: {
dur: 0.9,
parts: [{ d: 'M12 19.5s-7.5-4.3-7.5-9.6A4.1 4.1 0 0 1 12 7.6a4.1 4.1 0 0 1 7.5 2.3c0 5.3-7.5 9.6-7.5 9.6Z', m: 'beat', fill: true, origin: '12px 13px' }],
motions: { beat: [[0, 'transform:scale(1)'], [18, 'transform:scale(1.22)'], [34, 'transform:scale(.94)'], [52, 'transform:scale(1.1)'], [72, 'transform:scale(.98)'], [100, 'transform:scale(1)']] },
},
check: {
dur: 0.7,
parts: [{ d: 'M5 12.5 10 17.5 19.5 7', draw: true, m: 'draw' }],
motions: { draw: [[0, 'stroke-dashoffset:1'], [100, 'stroke-dashoffset:0']] },
},
refresh: {
dur: 0.9,
parts: [{ d: 'M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4v4.5H15', m: 'turn', origin: '12px 12px' }],
motions: { turn: [[0, 'transform:rotate(0)'], [100, 'transform:rotate(360deg)']] },
},
search: {
dur: 1.1,
parts: [{ d: 'M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Zm4.6-1.9 4.4 4.4', m: 'scan' }],
motions: { scan: [[0, 'transform:translate(0,0)'], [25, 'transform:translate(-1.5px,-1px)'], [50, 'transform:translate(1px,-2px)'], [75, 'transform:translate(1.5px,.5px)'], [100, 'transform:translate(0,0)']] },
},
mail: {
dur: 1,
parts: [
{ d: 'M4 7.5h16v11H4Z' },
{ d: 'M4.5 8 12 13.5 19.5 8', m: 'flap', origin: '12px 8px' },
{ d: 'M8 11.5V5h8v6.5', m: 'letter', hidden: true },
],
motions: {
flap: [[0, 'transform:scaleY(1)'], [30, 'transform:scaleY(-1)'], [70, 'transform:scaleY(-1)'], [100, 'transform:scaleY(1)']],
letter: [[0, 'transform:translateY(4px);opacity:0'], [30, 'transform:translateY(4px);opacity:0'], [50, 'transform:translateY(-1px);opacity:1'], [70, 'transform:translateY(-1px);opacity:1'], [90, 'transform:translateY(4px);opacity:0'], [100, 'transform:translateY(4px);opacity:0']],
},
},
star: {
dur: 0.9,
parts: [{ d: 'm12 4 2.4 5 5.4.7-4 3.7 1 5.4L12 16.2l-4.8 2.6 1-5.4-4-3.7 5.4-.7Z', m: 'spin', fill: true, origin: '12px 12px' }],
motions: { spin: [[0, 'transform:rotate(0) scale(1)'], [45, 'transform:rotate(72deg) scale(1.2)'], [100, 'transform:rotate(72deg) scale(1)']] },
},
trash: {
dur: 0.9,
parts: [
{ d: 'M6.5 8.5h11l-.8 10.2a1.5 1.5 0 0 1-1.5 1.3H8.8a1.5 1.5 0 0 1-1.5-1.3Z' },
{ d: 'M4.5 6.5h15M9.5 6.5V4.5h5v2', m: 'lid', origin: '19.5px 6.5px' },
{ d: 'M10.5 11.5v5M13.5 11.5v5' },
],
motions: { lid: [[0, 'transform:rotate(0) translateY(0)'], [35, 'transform:rotate(14deg) translateY(-2px)'], [65, 'transform:rotate(14deg) translateY(-2px)'], [100, 'transform:rotate(0) translateY(0)']] },
},
copy: {
dur: 0.8,
parts: [
{ d: 'M8.5 8.5h10v11h-10Z', m: 'front' },
{ d: 'M15.5 8.5V4.5h-10v11h3', m: 'back' },
],
motions: {
front: [[0, 'transform:translate(0,0)'], [45, 'transform:translate(1.5px,1.5px)'], [100, 'transform:translate(0,0)']],
back: [[0, 'transform:translate(0,0)'], [45, 'transform:translate(-1.5px,-1.5px)'], [100, 'transform:translate(0,0)']],
},
},
lock: {
dur: 1,
parts: [
{ d: 'M6 11h12v9H6Z' },
{ d: 'M8.5 11V8a3.5 3.5 0 0 1 7 0v3', m: 'open', origin: '15.5px 11px' },
{ d: 'M12 14.5v2' },
],
motions: { open: [[0, 'transform:translateY(0) rotate(0)'], [30, 'transform:translateY(-2px) rotate(0)'], [50, 'transform:translateY(-2px) rotate(25deg)'], [75, 'transform:translateY(-2px) rotate(25deg)'], [100, 'transform:translateY(0) rotate(0)']] },
},
eye: {
dur: 0.8,
parts: [
{ d: 'M2.5 12s3.5-6.5 9.5-6.5S21.5 12 21.5 12s-3.5 6.5-9.5 6.5S2.5 12 2.5 12Z', m: 'blink', origin: '12px 12px' },
{ d: 'M12 14.6a2.6 2.6 0 1 0 0-5.2 2.6 2.6 0 0 0 0 5.2Z', m: 'blink', origin: '12px 12px' },
],
motions: { blink: [[0, 'transform:scaleY(1)'], [30, 'transform:scaleY(.08)'], [55, 'transform:scaleY(1)'], [100, 'transform:scaleY(1)']] },
},
cart: {
dur: 0.9,
parts: [
{ d: 'M3.5 4.5h2l2 10.5h10l2-7.5H7', m: 'roll' },
{ d: 'M9 19.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z', m: 'roll' },
{ d: 'M12.5 3v4M10.5 5h4', m: 'add' },
],
motions: {
roll: [[0, 'transform:translateX(0)'], [30, 'transform:translateX(2.5px)'], [55, 'transform:translateX(-1px)'], [100, 'transform:translateX(0)']],
add: [[0, 'transform:translateY(-4px);opacity:0'], [40, 'transform:translateY(1px);opacity:1'], [60, 'transform:translateY(0);opacity:1'], [100, 'transform:translateY(0);opacity:1']],
},
},
sparkle: {
dur: 1.1,
parts: [
{ d: 'M11 3.5c.6 4.2 2.3 5.9 6.5 6.5-4.2.6-5.9 2.3-6.5 6.5-.6-4.2-2.3-5.9-6.5-6.5 4.2-.6 5.9-2.3 6.5-6.5Z', m: 'big', fill: true, origin: '11px 10px' },
{ d: 'M18 15.5c.3 1.8 1 2.5 2.8 2.8-1.8.3-2.5 1-2.8 2.8-.3-1.8-1-2.5-2.8-2.8 1.8-.3 2.5-1 2.8-2.8Z', m: 'small', origin: '18px 18.3px' },
],
motions: {
big: [[0, 'transform:scale(1) rotate(0)'], [35, 'transform:scale(1.15) rotate(15deg)'], [70, 'transform:scale(.95) rotate(0)'], [100, 'transform:scale(1) rotate(0)']],
small: [[0, 'transform:scale(1);opacity:1'], [25, 'transform:scale(0);opacity:0'], [55, 'transform:scale(1.3);opacity:1'], [100, 'transform:scale(1);opacity:1']],
},
},
};
export const ANIMATED_ICON_NAMES = Object.keys(ICONS) as AnimatedIconName[];
export interface AnimatedIconProps {
name: AnimatedIconName;
/** Size in px. Default 24. */
size?: number;
/** hover: plays when the icon or its parent is hovered or focused (default). loop: plays on its own with a rest. in-view: once when it appears. */
trigger?: 'hover' | 'loop' | 'in-view';
/** Filled state for heart, star and sparkle, and the accent color for all */
active?: boolean;
/** 1 is the designed tempo */
speed?: number;
strokeWidth?: number;
/** Read to screen readers. Without it the icon is decorative, which is right inside a labelled button. */
label?: string;
className?: string;
}
/**
* <button className="…"><AnimatedIcon name="bell" /> Notify me</button>
* <AnimatedIcon name="heart" active trigger="in-view" />
*/
export function AnimatedIcon({ name, size = 24, trigger = 'hover', active = false, speed = 1, strokeWidth = 1.6, label, className }: AnimatedIconProps) {
const sid = useSceneId('kvai');
const play = useScenePlay<HTMLSpanElement>(trigger === 'hover' ? 'off' : 'in-view');
const def = ICONS[name];
const sp = Math.max(0.1, speed);
const dur = round(def.dur / sp, 3);
const cycle = round(dur + 1.6 / sp, 3);
const share = dur / cycle;
const css = [
SCENE_CSS,
...Object.entries(def.motions).map(([m, frames]) => keyframes(`${sid}-${m}`, frames)),
...Object.entries(def.motions).map(([m, frames]) => keyframes(`${sid}-${m}-l`, [...frames.map(([p, d]) => [round(p * share, 2), d] as [number, string]), [100, frames[frames.length - 1][1]]])),
...Object.keys(def.motions).map((m) => {
const on = trigger === 'hover'
? `.${sid}:hover .${sid}-${m}, :hover > .${sid} .${sid}-${m}, :focus-visible > .${sid} .${sid}-${m}, [data-icon-host]:hover .${sid} .${sid}-${m}, [data-icon-host]:focus-visible .${sid} .${sid}-${m}`
: `.${sid}:not([data-kv="off"]) .${sid}-${m}`;
const value = trigger === 'loop' ? `${sid}-${m}-l ${cycle}s cubic-bezier(.4,0,.2,1) infinite both` : `${sid}-${m} ${dur}s cubic-bezier(.4,0,.2,1) 1 both`;
return `${on} { animation: ${value}; }`;
}),
`@media (prefers-reduced-motion: reduce) { .${sid} * { animation: none !important; } }`,
].join('\n');
const fillOn = active ? 'var(--primary)' : 'none';
return (
<span
ref={play.ref}
data-kv={trigger === 'hover' ? undefined : play.state}
role={label ? 'img' : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
className={cn(sid, 'inline-grid shrink-0 place-items-center', active && 'text-primary', className)}
style={{ width: size, height: size }}
>
<style dangerouslySetInnerHTML={{ __html: css }} />
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" className="overflow-visible">
{def.parts.map((p, i) => (
<path
key={i}
d={p.d}
className={p.m ? `${sid}-${p.m}` : undefined}
fill={p.fill ? fillOn : 'none'}
{...(p.draw ? { pathLength: 1, strokeDasharray: '1 1' } : null)}
style={{ transformOrigin: p.origin ?? '12px 12px', transformBox: 'view-box', transition: 'fill .3s', opacity: p.hidden ? 0 : undefined }}
/>
))}
</svg>
</span>
);
}Häufige Fragen
Wie installiere ich Animierte Icons?
Mit aiw add @ai-website/animated-icon. Der Befehl legt die Datei components/ui/animated-icon.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind motion-scene. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Animierte Icons?
6 Varianten, Glocke, Hoch- und Herunterladen, Senden, Liken und merken, Alle Icons und In einer Liste.
Kann ein KI-Assistent Animierte Icons einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Animierte Icons mit aiw_components und installieren sie mit aiw_add @ai-website/animated-icon.