The Future of Web Dev
The Future of Web Dev
Sliding Tabs UI Component for Shadcn/ui – slidytabs
A shadcn/ui Tabs utility for animated tab movement, sticky sliders, and two point range selection.

slidytabs is a Shadcn Tabs utility that adds animated tab movement, discrete slider selection, and discrete range selection across small fixed option sets.
It keeps the normal Tabs structure and attaches at the root so that you can reuse your existing TabsList, TabsTrigger, and TabsContent markup.
More Features
🎯 Uses the existing shadcn Tabs structure.
✨ Adds an animated active state for the standard tab component.
📌 Supports a fixed endpoint with sticky in slider mode.
🔁 Supports push behavior in range mode when handles meet
🧩 Works with shadcn/ui, shadcn-svelte, and shadcn-vue.
📐 Works with horizontal layouts and vertical layouts.
Use Cases
- Animate navigation menus to create clear visual feedback during view changes.
- Discrete rating selectors for customer feedback forms.
- Price range filters for e-commerce search interfaces.
- Clothing size selectors with tactile sliding mechanics.
How to Use It
Table Of Contents
Installation
Install the package with npm.
npm i slidytabsYou also need a Tabs component from your chosen shadcn stack. slidytabs does not render tabs on its own. It augments the Tabs root that already exists in your project.
Use it in React
import { tabs } from "slidytabs";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
export function BillingTabs() {
return (
<Tabs
ref={tabs()}
defaultValue="team"
className="w-full max-w-md"
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="solo">Solo</TabsTrigger>
<TabsTrigger value="team">Team</TabsTrigger>
<TabsTrigger value="agency">Agency</TabsTrigger>
</TabsList>
<TabsContent value="solo">Single seat plan details.</TabsContent>
<TabsContent value="team">Small team plan details.</TabsContent>
<TabsContent value="agency">Multi client plan details.</TabsContent>
</Tabs>
);
}Use it in Svelte
<script lang="ts">
import { tabs } from "slidytabs";
import * as Tabs from "$lib/components/ui/tabs/index.js";
</script>
<Tabs.Root {@attach tabs()} defaultValue="grid" class="w-full max-w-md">
<Tabs.List class="grid w-full grid-cols-3">
<Tabs.Trigger value="grid">Grid</Tabs.Trigger>
<Tabs.Trigger value="stack">Stack</Tabs.Trigger>
<Tabs.Trigger value="split">Split</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="grid">Grid layout settings.</Tabs.Content>
<Tabs.Content value="stack">Stack layout settings.</Tabs.Content>
<Tabs.Content value="split">Split layout settings.</Tabs.Content>
</Tabs.Root>Use it in Vue
<script setup lang="ts">
import { tabs } from "slidytabs";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
</script>
<template>
<Tabs :ref="tabs()" default-value="draft" class="w-full max-w-md">
<TabsList class="grid w-full grid-cols-3">
<TabsTrigger value="draft">Draft</TabsTrigger>
<TabsTrigger value="review">Review</TabsTrigger>
<TabsTrigger value="publish">Publish</TabsTrigger>
</TabsList>
<TabsContent value="draft">Draft stage settings.</TabsContent>
<TabsContent value="review">Review stage settings.</TabsContent>
<TabsContent value="publish">Publish stage settings.</TabsContent>
</Tabs>
</template>Tabs, slider, and range modes
tabs()
Use tabs() when you want classic tab panels plus animated movement on the trigger row.
What it does:
- keeps the normal Tabs interaction model
- works with standard shadcn
valueandonValueChangeprops - can also use slidytabs index based options when that fits your state model better
Example with standard shadcn controlled state in React:
import { useState } from "react";
import { tabs } from "slidytabs";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
export function ControlledTabs() {
const [value, setValue] = useState("medium");
return (
<Tabs
ref={tabs()}
value={value}
onValueChange={setValue}
className="w-full max-w-sm"
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="small">Small</TabsTrigger>
<TabsTrigger value="medium">Medium</TabsTrigger>
<TabsTrigger value="large">Large</TabsTrigger>
</TabsList>
<TabsContent value="small">Small size notes.</TabsContent>
<TabsContent value="medium">Medium size notes.</TabsContent>
<TabsContent value="large">Large size notes.</TabsContent>
</Tabs>
);
}slider()
Use slider() when the tab row should behave like a draggable single value selector instead of a panel switcher.
Available options:
value?: number
Current selected indexonValueChange?: (value: number) => void
Receives the next selected indexsticky?: number
Locks one endpoint at a fixed index
Example with a fixed starting point:
import { useState } from "react";
import { slider } from "slidytabs";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
export function SeatsSlider() {
const [index, setIndex] = useState(2);
return (
<Tabs
ref={slider({
value: index,
onValueChange: setIndex,
sticky: 0, // Keep the first stop fixed
})}
className="w-full max-w-md"
>
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="1">1</TabsTrigger>
<TabsTrigger value="2">2</TabsTrigger>
<TabsTrigger value="4">4</TabsTrigger>
<TabsTrigger value="8">8</TabsTrigger>
<TabsTrigger value="12">12</TabsTrigger>
</TabsList>
</Tabs>
);
}Use sticky for controls where the lower or upper bound should not move. Good examples include minimum seat counts, fixed starting dates, or base plan tiers.
range()
Use range() when users need to pick two indices from the same trigger row.
Available options:
value: [number, number]
Current start and end indicesonValueChange?: (value: [number, number]) => void
Receives the updated rangepush?: boolean
Lets one handle push the other when they collide
Example with push behavior:
import { useState } from "react";
import { range } from "slidytabs";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
export function DeliveryWindowRange() {
const [value, setValue] = useState<[number, number]>([1, 3]);
return (
<Tabs
ref={range({
value,
onValueChange: setValue,
push: true,
})}
className="w-full max-w-lg"
>
<TabsList className="grid w-full grid-cols-6">
<TabsTrigger value="mon">Mon</TabsTrigger>
<TabsTrigger value="tue">Tue</TabsTrigger>
<TabsTrigger value="wed">Wed</TabsTrigger>
<TabsTrigger value="thu">Thu</TabsTrigger>
<TabsTrigger value="fri">Fri</TabsTrigger>
<TabsTrigger value="sat">Sat</TabsTrigger>
</TabsList>
</Tabs>
);
}Related resources
- shadcn/ui Tabs: Shadcn/ui Tabs primitive.
- shadcn-svelte Tabs: Svelte Tabs component.
- shadcn-vue Tabs: Vue Tabs component.
- Radix UI Tabs: Underlying Tabs primitive behind the React shadcn implementation.
FAQs
Q: Does slidytabs replace the Tabs component?
A: No. It augments an existing shadcn Tabs root. You still render the same Tabs parts in your app.
Q: Can I use normal tab panels with slidytabs?
A: Yes. tabs() keeps the normal panel pattern and adds animated trigger movement.
Q: How does the utility handle dynamic tab additions?
A: The script uses a MutationObserver to monitor data-state attribute changes. It recalculates indicator positions when the DOM updates.
Q: Does slidytabs support vertical layouts?
A: Yes, if your underlying shadcn Tabs implementation supports vertical orientation.

