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

Installation

Install the package with npm.

npm i slidytabs

You 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 value and onValueChange props
  • 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 index
  • onValueChange?: (value: number) => void
    Receives the next selected index
  • sticky?: 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 indices
  • onValueChange?: (value: [number, number]) => void
    Receives the updated range
  • push?: 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

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.

Justin Francos

Justin Francos

Leave a Reply

Your email address will not be published. Required fields are marked *