Components
Tabs
shadcn's Tabs with goo. The active tab is a blob that slides between triggers and lands with a jelly overshoot.
Customize
GoopTabs wraps the same reka-ui Tabs primitive as shadcn-vue's Tabs, with the same API. Keyboard navigation and ARIA work the same. Swap the imports and you're done.
Installation
Nuxt
pnpm add @goopui/nuxt
export default defineNuxtConfig({
modules: ['@goopui/nuxt'],
})
The module auto-imports every GoopTabs* part, adds goop.css, and points Tailwind at the component classes. It needs Tailwind CSS v4 and the shadcn tokens (--muted, --background, --foreground, --input, --ring, --radius).
Vue
pnpm add @goopui/vue
@import "tailwindcss";
@import "@goopui/vue/goop.css";
Usage
<script setup lang="ts">
// Nuxt auto-imports these. In plain Vue, import them:
import { GoopTabs, GoopTabsContent, GoopTabsList, GoopTabsTrigger } from '@goopui/vue'
</script>
<template>
<GoopTabs default-value="account">
<GoopTabsList>
<GoopTabsTrigger value="account">Account</GoopTabsTrigger>
<GoopTabsTrigger value="password">Password</GoopTabsTrigger>
</GoopTabsList>
<GoopTabsContent value="account">Make changes to your account here.</GoopTabsContent>
<GoopTabsContent value="password">Change your password here.</GoopTabsContent>
</GoopTabs>
</template>
All the goo lives on GoopTabsList. GoopTabs, GoopTabsTrigger, and GoopTabsContent are shadcn's.
Behavior
- The blob is the active tab's background. It slides to the new trigger with momentum, stretching with a neck of lumps while it travels, and lands with a jelly overshoot (
momentum,wobble). - It follows everything. Clicks, the arrow keys, and programmatic changes. It tracks whichever trigger has
data-state="active". - Labels stay sharp. The blob is
aria-hiddenand sits behind the real triggers. Text is never filtered. - Server render. SSR renders shadcn's markup, and the active trigger is styled until the blob attaches in the browser.
Vertical
orientation="vertical" is reka-ui's. The blob travels along the column:
<GoopTabs default-value="account" orientation="vertical">
<GoopTabsList>
<GoopTabsTrigger value="account">Account</GoopTabsTrigger>
<GoopTabsTrigger value="password">Password</GoopTabsTrigger>
</GoopTabsList>
</GoopTabs>
Line variant
variant="line" is shadcn's underline style. It keeps the underline and has no goo, since there's no filled surface to melt:
<GoopTabsList variant="line">...</GoopTabsList>
Tuning
Turn off the jelly or the stretch, change the feel, or go plain shadcn:
<GoopTabsList preset="wild">...</GoopTabsList>
<GoopTabsList :wobble="false" :momentum="false">...</GoopTabsList>
<GoopTabsList :goo="false">...</GoopTabsList>
:indicator="false" keeps the list's goo setting but gives the active tab shadcn's flat background. :goo="false" renders exactly shadcn's TabsList and TabsTrigger.
Driving it from code
GoopTabsList exposes handle, the live indicator handle (null before mount, when goo is off, or on the line variant).
Props
GoopTabs, GoopTabsTrigger, and GoopTabsContent take shadcn-vue's props. GoopTabsList takes reka-ui's TabsList props, class, and these:
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | 'default' | 'line' | 'default' | Same as shadcn. 'line' keeps shadcn's underline and has no goo. |
goo | boolean | true | Master switch. false renders a plain shadcn TabsList. |
indicator | boolean | true | The active tab's background is a goo blob that slides between triggers. false gives shadcn's flat background. |
wobble | boolean | true | Jelly overshoot when the blob lands. |
momentum | boolean | true | The blob stretches and necks to a trailing tail while it travels. |
speed | number | 1 | Animation speed multiplier. |
preset | 'subtle' | 'playful' | 'wild' | app config, then playful | The overall feel. |
renderer | 'svg' | 'canvas' | 'svg' | How the goo is drawn. |
Accessibility
It's reka-ui's Tabs: a tablist with tab and tabpanel roles, roving focus, and arrow, Home, and End keys. Under reduced motion the blob snaps to the new tab with no stretch or overshoot, and still marks the active tab. See Accessibility & reduced motion.