Search docs

Jump to any table example

Discord
ShadTable

A collection of composable table components built on shadcn/ui and TanStack Table.

Animated Icons Table

Row actions built with Iconimate's motion-driven icons instead of static glyphs. Hover, focus, or tap a row action to preview its animation; click to apply it.

npx shadcn add https://shad-table.dev/r/animated-icons-table.json

Hover a row action to preview its motion; click to apply it.

NameEmailRoleActions
Amelia Frostamelia@acme.devEngineering
Noah Parknoah@acme.devDesign
Sofia Reyessofia@acme.devProduct
Liam Chenliam@acme.devEngineering
Maya Okaformaya@acme.devSupport

Installing the icons individually

The icons themselves are published on Iconimate's own registry, not this one — installing animated-icons-table above already pulls all five in, but you can add any of them to another project on their own:

Star

npx shadcn add https://iconimate.app/r/star.json

Bell Ringing

npx shadcn add https://iconimate.app/r/bell-ringing.json

Archive

npx shadcn add https://iconimate.app/r/archive.json

Trash

npx shadcn add https://iconimate.app/r/trash.json

Arrows Clockwise

npx shadcn add https://iconimate.app/r/arrows-clockwise.json

How it works

1.Every icon exposes an imperative handle, not just `:hover`

Each icon forwards a ref shaped like { startAnimation, stopAnimation }, because `:hover` never fires on touch. The Reset button in the toolbar calls startAnimation() directly on click, so the refresh spin plays even for someone who tapped the button without ever hovering it.

src/components/animated-icons/data-table.tsx
const refreshRef = useRef<IconHandle>(null)
function handleReset() {
refreshRef.current?.startAnimation()
onReset()
}
<ArrowsClockwiseIcon ref={refreshRef} size={16} />

2.A single useHover() drives motion, focus, and touch together

Every hover-driven icon (star, bell, archive, trash) shares one useHover() hook from #/lib/animated-icon.ts. It wires onMouseEnter/onMouseLeave AND onFocus/onBlur to the same controls, so tabbing to a row action previews its motion exactly like hovering it does — no separate keyboard path to maintain.

src/lib/animated-icon.ts
return {
controls,
reduced,
ambient,
start,
stop,
bind: { onMouseEnter: start, onMouseLeave: stop, onFocus: start, onBlur: stop },
}

3.The animation loops for as long as the pointer stays

start() doesn't just play the "animate" variant once — it replays it end-to-end while the pointer or focus is still on the icon, snapping back to "normal" between cycles (invisible, since keyframes end where they start) so the next replay actually restarts instead of resolving instantly.

src/lib/animated-icon.ts
void controls.start('animate').then(() => {
if (!looping.current) return
controls.set('normal')
if (!ambient) { looping.current = false; return }
const elapsed = performance.now() - t0
replayTimer.current = window.setTimeout(run, elapsed < 100 ? 300 : elapsed * 0.3)
})

4.Row state drives which icon variant renders, not a second icon set

There's no separate "filled star" icon component. The same StarIcon just gets fill: 'currentColor' when member.favorite is true, and its wrapper button picks up an amber text color. Toggling favorite/notify/archived is a plain array map — the same pattern the Editable Table uses for its optimistic updates.

src/components/animated-icons/data-table.tsx
function toggle(id: string, key: 'favorite' | 'notify' | 'archived') {
onDataChange((prev) =>
prev.map((row) => (row.id === id ? { ...row, [key]: !row[key] } : row)),
)
}
<StarIcon size={18} style={{ fill: member.favorite ? 'currentColor' : 'none' }} />