logo

Animated Tabs

0 installs

A drop-in replacement for shadcn-svelte Tabs powered by svelte-motion and bits-ui. A spring-based sliding indicator animates between triggers using layoutId — no extra config needed.

Installation

Install via the shadcn-svelte CLI from our registry:

npx shadcn-svelte@latest add https://motion.svelte.page/r/animated-tabs.json
npx shadcn-svelte@latest add https://motion.svelte.page/r/animated-tabs.json

This installs AnimatedTabs alongside your existing components. It automatically pulls in @humanspeak/svelte-motion and bits-ui as dependencies.

Then use it:

<script>
  import * as Tabs from '$lib/components/ui/animated-tabs'
</script>

<Tabs.Root value="account">
  <Tabs.List>
    <Tabs.Trigger value="account">Account</Tabs.Trigger>
    <Tabs.Trigger value="password">Password</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="account">Account content</Tabs.Content>
  <Tabs.Content value="password">Password content</Tabs.Content>
</Tabs.Root>
<script>
  import * as Tabs from '$lib/components/ui/animated-tabs'
</script>

<Tabs.Root value="account">
  <Tabs.List>
    <Tabs.Trigger value="account">Account</Tabs.Trigger>
    <Tabs.Trigger value="password">Password</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="account">Account content</Tabs.Content>
  <Tabs.Content value="password">Password content</Tabs.Content>
</Tabs.Root>

Manual Installation

Alternatively, copy the component source directly from GitHub.

Live Demo

Click the tabs to see the sliding indicator:

Default (Animated)

Account

Make changes to your account here. Click save when you're done.

Without Animation

Set animated={false} on Root to get vanilla shadcn behavior with no motion:

Standard CSS background swap — no animation.

Tabs Only (No Content Animation)

Set animated={false} on Content to keep the sliding indicator but skip the content entrance animation:

The tab indicator slides, but this content appears instantly.

Multiple Groups (Unique layoutIds)

Each tab group automatically gets a unique layoutId, so multiple groups on the same page don’t conflict:

Animation Details

PropertyValueDescription
layoutIdAuto-generated per groupShared layout animation between triggers
TransitionSpring (500/30)stiffness: 500, damping: 30
Indicatorbg-backgroundAbsolute-positioned div behind trigger text

The sliding indicator uses AnimatePresence + motion.div with layoutId to animate smoothly between the active trigger. Each tab group instance gets its own unique layoutId via a module-level counter, so multiple groups on the same page animate independently.

Accessibility

Full ARIA compliance via bits-ui Tabs primitives:

  • role="tablist", role="tab", role="tabpanel"
  • aria-selected on triggers
  • Roving tabindex (only active trigger is in tab order)
  • Arrow keys cycle between triggers
  • Home/End jump to first/last trigger
  • Enter/Space activate a focused trigger

API

Root

All standard bits-ui Tabs.Root props are supported, plus:

PropTypeDefaultDescription
valuestring''Active tab value (bindable)
onValueChange(value: string) => voidCallback when selection changes
animatedbooleantrueEnable/disable all animation (indicator + content)

List

Styled wrapper for the trigger container. Accepts all standard HTML div attributes.

Trigger

PropTypeDefaultDescription
valuestringTab value (required)
disabledbooleanfalseDisable this trigger
animatedbooleanInherits from RootOverride animation for this trigger
transitionobject{ type: 'spring', stiffness: 500, damping: 30 }Override the indicator spring transition

Content

PropTypeDefaultDescription
valuestringTab value (required)
animatedbooleanInherits from RootOverride animation for this panel
initialobject{ opacity: 0, y: 8 }Override the content entrance initial state
animateobject{ opacity: 1, y: 0 }Override the content entrance animate target
transitionobject{ duration: 0.3, ease: 'easeOut' }Override the content entrance transition