Components
Input New · experimental
shadcn's Input and Textarea with a goo focus ring. It flows around the border from where you clicked, ripples as you type, and drains away on blur.
Customize
GoopInput is shadcn-vue's Input: the same classes, v-model, class merge, and aria-invalid styling. Every attribute you set (type, placeholder, id, name, disabled, ...) goes to the real <input>. The goo is a ring drawn on a layer in a wrapper around it, so the field itself is never filtered. GoopTextarea is the same for shadcn's Textarea.
Installation
Install @goopui/nuxt or @goopui/vue as shown on the Button page. The Nuxt module auto-imports GoopInput and GoopTextarea. They use the same shadcn tokens as the plain Input (--input, --ring, --destructive).
Usage
<script setup lang="ts">
// Nuxt auto-imports this. In plain Vue, import it:
import { GoopInput } from '@goopui/vue'
const email = ref('')
</script>
<template>
<label for="email" class="text-sm font-medium">Email</label>
<GoopInput id="email" v-model="email" type="email" placeholder="[email protected]" />
</template>
What it does
The ring replaces shadcn's flat focus ring. Each part is an effect you can turn off:
- flow. On focus, a dollop of goo lands where you clicked (the left edge for keyboard focus) and two bulbous fronts race around the border both ways.
- close. Where the fronts meet, the ring closes with a wobble.
- wobble. While the field is focused, the closed ring wobbles gently.
- ripple. Each edit sends a small ripple along the ring from the point nearest the caret.
- drain. On blur, the ring drains away, top first.
- drip. On an invalid field, a small drip hangs off the bottom of the ring.
- drips. Off by default. Small drips hang under the ring while it shows.
Invalid
Set aria-invalid and the ring shows even when the field isn't focused, in the destructive color, with a small drip hanging off the bottom edge (drip). The ring follows the attribute, so it turns on and off with your validation.
Enter a valid email address.
<GoopInput v-model="email" type="email" aria-invalid="true" aria-describedby="email-error" />
<p id="email-error" class="text-sm text-destructive">Enter a valid email address.</p>
Effects and presets
Every effect is a prop. false turns it off, true uses the preset's strength, and a number scales it. preset, speed, and renderer set the base.
<GoopInput drips />
<GoopInput preset="wild" />
<GoopInput preset="subtle" />
<GoopInput :flow="false" />
Disabled and goo off
A disabled field is shadcn's: dimmed, and it can't be focused, so the ring never shows. :goo="false" renders a plain shadcn Input with no wrapper.
<GoopInput disabled />
<GoopInput :goo="false" />
Textarea
GoopTextarea has the same ring and the same props. The ring follows the textarea as it grows (shadcn's field-sizing-content), and typing ripples the edge nearest the caret's line.
<script setup lang="ts">
import { GoopTextarea } from '@goopui/vue'
const bio = ref('')
</script>
<template>
<GoopTextarea v-model="bio" placeholder="Tell us a little about yourself." />
</template>
Colors
The ring reads these CSS variables. Override them on the field's wrapper or an ancestor:
| Variable | Default | What it colors |
|---|---|---|
--goop-input-color | var(--ring) | The focus ring. |
--goop-input-opacity | 0.5 | The focus ring's opacity. |
--goop-input-invalid-color | var(--destructive) | The ring and drip of an invalid field. |
--goop-input-invalid-opacity | 0.5 | Their opacity. |
<GoopInput container-class="[--goop-input-color:var(--color-emerald-500)]" />
Props
GoopInput and GoopTextarea keep the shadcn-vue API and take the same props. Every other attribute goes to the <input> or <textarea>.
| Prop | Type | Default | Notes |
|---|---|---|---|
modelValue | string | number | The value (v-model). | |
defaultValue | string | number | The initial value when uncontrolled. | |
class | string | array | object | Merged onto the field, like shadcn. | |
containerClass | string | array | object | 'relative w-full min-w-0' | Classes for the wrapper that holds the goo layer. Set width or margins here. |
goo | boolean | true | Master switch. false renders a plain shadcn Input or Textarea, with no wrapper. |
flow | boolean | number | true | On focus, goo flows around the border from the click point (the left edge on keyboard focus). Off: the ring appears at once. |
close | boolean | number | true | The wobble where the two flow fronts meet and close the ring. |
ripple | boolean | number | true | Each edit sends a small ripple along the ring from the point nearest the caret. |
wobble | boolean | number | true | The closed ring wobbles gently while the field is focused. |
drain | boolean | number | true | On blur the ring drains away, top first. Off: it disappears at once. |
drip | boolean | number | true | With aria-invalid, a small drip hangs off the bottom of the destructive ring. |
drips | boolean | number | false | Resting drips: small drips hang under the ring while it shows. Off by default. |
speed | number | 1 | Animation speed multiplier. |
preset | 'subtle' | 'playful' | 'wild' | app config, then playful | The feel: ring thickness, flow and drain timing, wobble and ripple strength. |
renderer | 'svg' | 'canvas' | 'svg' | How the goo is drawn. |
Both expose handle (the live goo handle, null before mount or with goo off) and el (the <input> or <textarea>).
Before it mounts, during SSR, and with :goo="false", the field looks exactly like shadcn's, so first paint and hydration match.
Accessibility
It's a real <input> or <textarea>: labels, aria-*, autofill, and form submission work as usual. The goo layer is aria-hidden and can't be clicked, and the field is never filtered. With the goo attached, shadcn's focus ring steps aside for the goo ring, which shows on every focus, keyboard included.
Under reduced motion the ring is a static closed band: it appears on focus and goes away on blur, with no flow, ripple, wobble, or drain. Invalid fields keep their ring and drip. See Accessibility & reduced motion.