Appearance
@vibe-labs/design-vue-inputs
Vue 3 input components for the Vibe Design System. Text inputs, number inputs, textareas, checkboxes, and radio buttons with built-in validation, ARIA bindings, and @vibe-labs/design-vue-forms integration.
Installation
ts
import {
VibeInput,
VibeInputNumber,
VibeInputGroup,
VibeTextArea,
VibeCheckbox,
VibeCheckboxGroup,
VibeRadio,
VibeRadioGroup,
} from "@vibe-labs/design-vue-inputs";Requires the CSS layer from @vibe-labs/design-components-inputs.
Components
VibeInput
Text input with validation, debounce, password reveal, search clear, and character count.
Usage
vue
<!-- Basic -->
<VibeInput v-model="email" label="Email" type="email" placeholder="you@example.com" />
<!-- With form binding -->
<VibeInput v-bind="form.field('email')" label="Email" type="email" />
<!-- Validation rules -->
<VibeInput v-model="email" label="Email" :rules="[required(), email()]" validate-on="blur" />
<!-- Error/success from props -->
<VibeInput v-model="name" label="Name" error="Name is required" />
<VibeInput v-model="name" label="Name" success="Looks good!" />
<!-- Sizes -->
<VibeInput v-model="val" size="sm" />
<VibeInput v-model="val" size="lg" />
<!-- Password with reveal toggle -->
<VibeInput v-model="password" label="Password" type="password" />
<!-- Search with clear button -->
<VibeInput v-model="query" label="Search" type="search" />
<!-- Character count -->
<VibeInput v-model="bio" label="Bio" :maxlength="200" show-count />
<!-- Debounced input -->
<VibeInput v-model="search" label="Search" :debounce="300" />
<!-- Leading/trailing slots -->
<VibeInput v-model="amount" label="Price">
<template #leading>£</template>
<template #trailing>.00</template>
</VibeInput>
<!-- Required -->
<VibeInput v-model="name" label="Full Name" required />
<!-- Disabled / readonly -->
<VibeInput v-model="locked" label="Locked" disabled />
<VibeInput v-model="locked" label="Read Only" readonly />Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | Input value |
type | InputType | "text" | text · email · password · url · tel · search |
size | InputSize | "md" | sm · md · lg |
label | string | — | Field label |
placeholder | string | — | Placeholder text |
helperText | string | — | Helper text below input |
error | string | — | Error message (sets error state) |
success | string | — | Success message (sets success state) |
disabled | boolean | false | Disabled state |
readonly | boolean | false | Read-only state |
required | boolean | false | Required indicator on label |
maxlength | number | — | Max characters |
showCount | boolean | false | Show character count (requires maxlength) |
debounce | number | 0 | Debounce delay in ms |
rules | ValidationRule[] | — | Validation rules |
validateOn | "change" | "blur" | "submit" | "blur" | When to validate |
pattern | string | — | HTML pattern attribute |
autocomplete | string | — | Autocomplete hint |
id | string | auto | HTML id |
name | string | — | Form name |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | Value changed |
validate | InputValidationEvent | Validation ran |
focus | FocusEvent | Input focused |
blur | FocusEvent | Input blurred |
clear | — | Search cleared |
Slots
| Slot | Description |
|---|---|
leading | Leading content (icons, prefix text) |
trailing | Trailing content (suffix, icons) |
clearIcon | Custom clear button icon |
revealIcon | Custom password reveal icon (receives visible prop) |
Exposed Methods
| Method | Description |
|---|---|
focus() | Focus the input |
blur() | Blur the input |
select() | Select all text |
validate() | Run validation programmatically |
clearValidation() | Clear validation state |
el | Direct ref to the input element |
VibeInputNumber
Numeric input with increment/decrement controls, precision, clamping, and hold-to-repeat.
Usage
vue
<!-- Basic -->
<VibeInputNumber v-model="quantity" label="Quantity" :min="1" :max="99" />
<!-- With step and precision -->
<VibeInputNumber v-model="price" label="Price" :step="0.01" :precision="2" />
<!-- Without controls -->
<VibeInputNumber v-model="value" label="Value" :controls="false" />
<!-- Non-nullable -->
<VibeInputNumber v-model="count" label="Count" :nullable="false" :min="0" />
<!-- Leading slot -->
<VibeInputNumber v-model="price" label="Price">
<template #leading>£</template>
</VibeInputNumber>Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | number | null | — | Numeric value |
min | number | — | Minimum value |
max | number | — | Maximum value |
step | number | 1 | Increment step |
precision | number | — | Decimal precision |
controls | boolean | true | Show +/- buttons |
nullable | boolean | true | Allow empty/null value |
+ all InputBaseProps |
Keyboard
- ArrowUp / ArrowDown: Increment / decrement by step
- Hold-to-repeat on +/- buttons (120ms interval)
VibeInputGroup
Groups related inputs in a fieldset with shared label, error, helper text, and layout options.
Usage
vue
<!-- Vertical group (default) -->
<VibeInputGroup label="Address">
<VibeInput v-model="line1" placeholder="Line 1" />
<VibeInput v-model="line2" placeholder="Line 2" />
<VibeInput v-model="city" placeholder="City" />
</VibeInputGroup>
<!-- Horizontal group -->
<VibeInputGroup label="Date Range" direction="horizontal">
<VibeInput v-model="from" placeholder="From" />
<VibeInput v-model="to" placeholder="To" />
</VibeInputGroup>
<!-- Seamless (collapsed borders) -->
<VibeInputGroup label="Phone" direction="horizontal" seamless>
<VibeInput v-model="code" placeholder="+44" size="sm" />
<VibeInput v-model="number" placeholder="Number" />
</VibeInputGroup>Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Group label |
error | string | — | Group-level error message |
helperText | string | — | Helper text below the group |
direction | "vertical" | "horizontal" | "vertical" | Layout direction |
size | InputSize | "md" | Size applied to children via CSS scope |
seamless | boolean | false | Collapse borders between adjacent inputs |
VibeTextArea
Multi-line textarea with auto-resize, character count, and validation.
Usage
vue
<!-- Basic -->
<VibeTextArea v-model="description" label="Description" :rows="4" />
<!-- Auto-resize -->
<VibeTextArea v-model="notes" label="Notes" auto-resize :max-rows="10" />
<!-- Character count -->
<VibeTextArea v-model="bio" label="Bio" :maxlength="500" show-count />
<!-- Non-resizable -->
<VibeTextArea v-model="text" label="Fixed" :resizable="false" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | Textarea value |
rows | number | 3 | Visible rows |
autoResize | boolean | false | Auto-grow to content |
maxRows | number | — | Max rows when auto-resizing |
maxlength | number | — | Max characters |
showCount | boolean | false | Show character count |
resizable | boolean | true | Manual resize handle |
+ all InputBaseProps |
VibeCheckbox
Single checkbox or group-compatible checkbox with indeterminate support.
Usage
vue
<!-- Boolean (single) -->
<VibeCheckbox v-model="agreed" label="I agree to the terms" />
<!-- Indeterminate -->
<VibeCheckbox v-model="selectAll" :indeterminate="isPartial" label="Select all" />
<!-- In a group (string array) -->
<VibeCheckboxGroup label="Genres">
<VibeCheckbox v-model="selected" value="rock" label="Rock" />
<VibeCheckbox v-model="selected" value="pop" label="Pop" />
<VibeCheckbox v-model="selected" value="jazz" label="Jazz" />
</VibeCheckboxGroup>Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | string[] | — | Checked state or group array |
value | string | — | Value when used in a group |
label | string | — | Label text |
size | InputSize | "md" | sm · md · lg |
indeterminate | boolean | false | Indeterminate state |
disabled | boolean | false | Disabled |
error | boolean | false | Error styling |
name | string | — | Form name |
VibeCheckboxGroup
Wraps multiple checkboxes in a fieldset with label and error support.
vue
<VibeCheckboxGroup label="Interests" error="Select at least one" horizontal>
<VibeCheckbox v-model="interests" value="music" label="Music" />
<VibeCheckbox v-model="interests" value="art" label="Art" />
</VibeCheckboxGroup>Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Group label |
horizontal | boolean | false | Horizontal layout |
error | string | — | Group error message |
VibeRadio
Single radio button — works standalone or within a VibeRadioGroup.
vue
<!-- Standalone -->
<VibeRadio v-model="plan" value="free" label="Free" name="plan" />
<VibeRadio v-model="plan" value="pro" label="Pro" name="plan" />
<!-- In a group -->
<VibeRadioGroup v-model="plan" label="Plan" name="plan">
<VibeRadio value="free" label="Free" />
<VibeRadio value="pro" label="Pro" />
<VibeRadio value="enterprise" label="Enterprise" />
</VibeRadioGroup>Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | Currently selected value |
value | string | required | This radio's value |
label | string | — | Label text |
size | InputSize | "md" | sm · md · lg |
disabled | boolean | false | Disabled |
error | boolean | false | Error styling |
name | string | — | Name (auto from group) |
VibeRadioGroup
Wraps radios with shared name/value context via provide/inject.
vue
<VibeRadioGroup v-model="size" label="Size" name="size" horizontal>
<VibeRadio value="sm" label="Small" />
<VibeRadio value="md" label="Medium" />
<VibeRadio value="lg" label="Large" />
</VibeRadioGroup>Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | Selected value |
label | string | — | Group label |
name | string | — | Shared name for all radios |
horizontal | boolean | false | Horizontal layout |
error | string | — | Group error message |
Composables
useInputField(options)
Core composable powering all text-like inputs. Handles ID generation, validation state, ARIA bindings, and CSS data attributes.
useAutoResize(textareaEl, value, options)
Auto-resize textarea to fit content. Respects min rows, max rows, padding, and border widths.
Forms Integration
All text-like inputs work with @vibe-labs/design-vue-forms via two paths:
1. form.field() binding — the easy way:
vue
<VibeInput v-bind="form.field('email')" label="Email" />2. useFormField registration — for submit-time orchestration.
Validation
Validation priority order:
- Props —
error/successprops (highest priority) - Rules —
ValidationRule[]on the input itself - Form-level — from
VibeForm'svalidatefunction viaform.field()error binding
Dependencies
| Package | Purpose |
|---|---|
@vibe-labs/design-components-inputs | CSS tokens + generated styles |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.
Usage Guide
Setup
ts
import {
VibeInput,
VibeInputNumber,
VibeInputGroup,
VibeTextArea,
VibeCheckbox,
VibeCheckboxGroup,
VibeRadio,
VibeRadioGroup,
} from "@vibe-labs/design-vue-inputs";
import "@vibe-labs/design-components-inputs";VibeInput
Standard text input with validation
vue
<script setup lang="ts">
import { VibeInput } from "@vibe-labs/design-vue-inputs";
import { required, email } from "@vibe-labs/design-vue-forms";
import { ref } from "vue";
const emailValue = ref("");
</script>
<template>
<VibeInput
v-model="emailValue"
label="Email"
type="email"
placeholder="you@example.com"
:rules="[required(), email()]"
validate-on="blur"
required
/>
</template>Search input with debounce
vue
<script setup lang="ts">
import { VibeInput } from "@vibe-labs/design-vue-inputs";
import { ref } from "vue";
const query = ref("");
watch(query, (val) => {
performSearch(val);
});
</script>
<template>
<!-- Emits model update after 300ms of inactivity -->
<VibeInput
v-model="query"
type="search"
placeholder="Search tracks..."
:debounce="300"
label="Search"
/>
</template>Input with leading/trailing slots
vue
<script setup lang="ts">
import { VibeInput } from "@vibe-labs/design-vue-inputs";
import { ref } from "vue";
const price = ref("");
</script>
<template>
<VibeInput v-model="price" label="Price" type="text">
<template #leading>£</template>
<template #trailing>GBP</template>
</VibeInput>
</template>VibeInputNumber
Quantity selector
vue
<script setup lang="ts">
import { VibeInputNumber } from "@vibe-labs/design-vue-inputs";
import { ref } from "vue";
const quantity = ref(1);
</script>
<template>
<VibeInputNumber
v-model="quantity"
label="Quantity"
:min="1"
:max="99"
:nullable="false"
/>
</template>VibeTextArea
Bio field with character count
vue
<script setup lang="ts">
import { VibeTextArea } from "@vibe-labs/design-vue-inputs";
import { ref } from "vue";
const bio = ref("");
</script>
<template>
<VibeTextArea
v-model="bio"
label="Artist Bio"
auto-resize
:max-rows="8"
:maxlength="500"
show-count
helper-text="Describe yourself in a few sentences"
/>
</template>VibeCheckbox / VibeCheckboxGroup
Multi-select genres
vue
<script setup lang="ts">
import { VibeCheckbox, VibeCheckboxGroup } from "@vibe-labs/design-vue-inputs";
import { ref } from "vue";
const selectedGenres = ref<string[]>([]);
</script>
<template>
<VibeCheckboxGroup label="Preferred Genres" horizontal>
<VibeCheckbox v-model="selectedGenres" value="electronic" label="Electronic" />
<VibeCheckbox v-model="selectedGenres" value="jazz" label="Jazz" />
<VibeCheckbox v-model="selectedGenres" value="hip-hop" label="Hip Hop" />
<VibeCheckbox v-model="selectedGenres" value="classical" label="Classical" />
</VibeCheckboxGroup>
</template>Select-all with indeterminate state
vue
<script setup lang="ts">
import { VibeCheckbox, VibeCheckboxGroup } from "@vibe-labs/design-vue-inputs";
import { ref, computed } from "vue";
const items = ["Track A", "Track B", "Track C"];
const selected = ref<string[]>([]);
const allSelected = computed(() => selected.value.length === items.length);
const isPartial = computed(() => selected.value.length > 0 && !allSelected.value);
function toggleAll() {
selected.value = allSelected.value ? [] : [...items];
}
</script>
<template>
<VibeCheckbox
:model-value="allSelected"
:indeterminate="isPartial"
label="Select all"
@update:model-value="toggleAll"
/>
<VibeCheckboxGroup label="Tracks">
<VibeCheckbox v-for="item in items" :key="item" v-model="selected" :value="item" :label="item" />
</VibeCheckboxGroup>
</template>VibeRadioGroup
Plan selector
vue
<script setup lang="ts">
import { VibeRadio, VibeRadioGroup } from "@vibe-labs/design-vue-inputs";
import { ref } from "vue";
const plan = ref("free");
</script>
<template>
<VibeRadioGroup v-model="plan" label="Choose a plan" name="plan">
<VibeRadio value="free" label="Free" />
<VibeRadio value="pro" label="Pro — £9/mo" />
<VibeRadio value="enterprise" label="Enterprise" />
</VibeRadioGroup>
</template>VibeInputGroup
Phone number with country code
vue
<script setup lang="ts">
import { VibeInput, VibeInputGroup } from "@vibe-labs/design-vue-inputs";
import { ref } from "vue";
const countryCode = ref("+44");
const phone = ref("");
</script>
<template>
<VibeInputGroup label="Phone number" direction="horizontal" seamless>
<VibeInput v-model="countryCode" size="sm" style="max-width: 80px" />
<VibeInput v-model="phone" placeholder="7700 900123" type="tel" />
</VibeInputGroup>
</template>Common Patterns
Wire to @vibe-labs/design-vue-forms
vue
<script setup lang="ts">
import { VibeForm, required } from "@vibe-labs/design-vue-forms";
import { VibeInput, VibeTextArea } from "@vibe-labs/design-vue-inputs";
const initialValues = { title: "", notes: "" };
</script>
<template>
<VibeForm v-slot="form" :initial-values="initialValues" @submit="onSubmit">
<!-- form.field() spreads modelValue, error, name, and onBlur -->
<VibeInput v-bind="form.field('title')" label="Title" required />
<VibeTextArea v-bind="form.field('notes')" label="Notes" auto-resize />
<button type="submit">Save</button>
</VibeForm>
</template>