Components
Radio Group
shadcn's Radio Group where the selected dot is a blob of goo that stretches, flies, and splats onto the next item.
v-model: pro
Customize
GoopRadioGroup and GoopRadioGroupItem wrap reka-ui's RadioGroupRoot, RadioGroupItem, and RadioGroupIndicator, the same primitive as shadcn-vue's RadioGroup, with the same API and classes. v-model, orientation, loop, dir, disabled, name, required, asChild, roving focus, and arrow-key selection all come from reka-ui. The goo is one layer on the group root.
Installation
Install @goopui/nuxt or @goopui/vue as shown on the Button page. The Nuxt module auto-imports GoopRadioGroup and GoopRadioGroupItem. It uses the same shadcn tokens as the plain Radio Group (--primary, --primary-foreground, --input, --ring).
Usage
<script setup lang="ts">
// Nuxt auto-imports these. In plain Vue, import them:
import { GoopRadioGroup, GoopRadioGroupItem } from '@goopui/vue'
const plan = ref('pro')
</script>
<template>
<GoopRadioGroup v-model="plan">
<label v-for="p in ['free', 'pro', 'team']" :key="p" class="flex items-center gap-2 text-sm capitalize">
<GoopRadioGroupItem :id="`plan-${p}`" :value="p" />
{{ p }}
</label>
</GoopRadioGroup>
</template>
What it does
Selecting a different item moves one blob of goo from the old item to the new one:
- bridge. The blob stretches out of the old item on a fat goo bridge. The bridge snaps.
- Flight. It flies to the new item, stretched along its path with a teardrop tail. It makes a small hop on sideways jumps.
- splat. It lands flat along the flight, bulging across it, then rebounds and wobbles.
- droplets. A few drops splash off the landing.
The dot is the same unfiltered one shadcn renders. It is held back until the splat has mostly settled. The animation works in any direction, so keyboard selection animates too.
Horizontal
Set orientation="horizontal" and lay the group out yourself, as with shadcn:
<GoopRadioGroup v-model="size" orientation="horizontal" class="flex gap-6">
<label v-for="s in ['s', 'm', 'l', 'xl']" :key="s" class="flex items-center gap-2 text-sm uppercase">
<GoopRadioGroupItem :id="`size-${s}`" :value="s" />
{{ s }}
</label>
</GoopRadioGroup>
Effects, presets, and sizes
The goo props go on GoopRadioGroup. Each effect takes false (off), true (the preset's strength), or a number that scales it (0 is off). The item size is a class on GoopRadioGroupItem, as in shadcn, and the blob follows it.
<!-- No bridge -->
<GoopRadioGroup default-value="a" orientation="horizontal" class="flex gap-6" :bridge="false">
<GoopRadioGroupItem value="a" />
<GoopRadioGroupItem value="b" />
<GoopRadioGroupItem value="c" />
</GoopRadioGroup>
<!-- Wild -->
<GoopRadioGroup default-value="a" orientation="horizontal" class="flex gap-6" preset="wild">
...
</GoopRadioGroup>
<!-- Bigger items -->
<GoopRadioGroup default-value="a" orientation="horizontal" class="flex gap-10">
<GoopRadioGroupItem value="a" class="size-7" />
...
</GoopRadioGroup>
Disabled and goo off
A disabled item is shadcn's: dimmed, and it can't be selected. :goo="false" on the group renders a plain shadcn Radio Group.
<GoopRadioGroup default-value="one">
<GoopRadioGroupItem value="one" />
<GoopRadioGroupItem value="two" />
<GoopRadioGroupItem value="three" disabled />
</GoopRadioGroup>
<GoopRadioGroup default-value="a" :goo="false">
<GoopRadioGroupItem value="a" />
<GoopRadioGroupItem value="b" />
</GoopRadioGroup>
Colors
The blob uses the --goop-radio-fill variable on the group, which defaults to var(--primary), the color shadcn fills the checked item with. Override it on the group or an ancestor:
<GoopRadioGroup class="[--goop-radio-fill:var(--color-emerald-500)]">
<GoopRadioGroupItem value="a" class="data-checked:border-emerald-500" />
...
</GoopRadioGroup>
Props
GoopRadioGroup
Keeps the shadcn-vue RadioGroup API and adds the goo props.
| Prop | Type | Default | Notes |
|---|---|---|---|
modelValue | string | number | boolean | object | The controlled value (v-model). | |
defaultValue | same | The initial value when uncontrolled. | |
orientation | 'vertical' | 'horizontal' | Passed to reka-ui. It sets arrow-key direction. | |
dir | 'ltr' | 'rtl' | Reading direction. | |
loop | boolean | true | Arrow keys wrap around. |
disabled | boolean | false | Disables every item. |
name, required | Passed to reka-ui. | ||
asChild, as | The element to render, via reka-ui. | ||
class | string | array | object | Merged onto the root. The default is grid gap-2 w-full. | |
goo | boolean | true | Master switch. false renders a plain shadcn Radio Group. |
bridge | boolean | number | true | The blob stretches from the old item to the new one on a neck that snaps. |
splat | boolean | number | true | The blob lands in the new item with a splat and a wobble. |
droplets | boolean | number | true | A few droplets fly off on landing. |
speed | number | 1 | Speed multiplier. |
preset | 'subtle' | 'playful' | 'wild' | app config, then playful | The feel. |
renderer | 'svg' | 'canvas' | 'svg' | How the goo is drawn. |
It exposes handle (the live radio group handle, null before mount or with goo off) and el (the root).
GoopRadioGroupItem
Keeps the shadcn-vue RadioGroupItem API. It has no goo props of its own. Inside an active GoopRadioGroup, the checked item drops its own fill, because the group's blob is the fill.
| Prop | Type | Default | Notes |
|---|---|---|---|
value | string | number | boolean | object | This item's value. | |
disabled | boolean | false | |
id, name, required | Passed to reka-ui. | ||
asChild, as | The element to render, via reka-ui. | ||
class | string | array | object | Merged onto the item. Set the size here. |
The default slot replaces the dot.
Before it mounts, during SSR, and with :goo="false", it is a plain shadcn Radio Group, so first paint and hydration match.
Accessibility
It's a real reka-ui radio group: role="radiogroup" with role="radio" items, roving focus, and arrow keys that move and select. The goo layer is aria-hidden and can't be clicked.
Reduced motion keeps the goo shape and turns off the simulation: the blob sits on the selected item straight away, with no bridge, flight, splat, or droplets. See Accessibility & reduced motion.