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 (from @vibe-labs/design-vue-forms) -->
<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.
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 (defaults to min or 0 when cleared) -->
<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. Supports seamless mode for collapsed borders between adjacent inputs.
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>
<!-- With group-level error -->
<VibeInputGroup label="Credentials" error="Both fields are required">
<VibeInput v-model="user" placeholder="Username" />
<VibeInput v-model="pass" placeholder="Password" type="password" />
</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.
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.
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>
<!-- Custom icon slot -->
<VibeCheckbox v-model="checked" label="Custom">
<template #icon="{ checked }">
<MyCheckIcon v-if="checked" />
</template>
</VibeCheckbox>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 (auto-wired via provide/inject) -->
<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. Prop-level error/success take priority over rule-based validation.
ts
import { useInputField } from "@vibe-labs/design-vue-inputs";
const {
inputId,
helperId,
errorId,
validationState,
validationMessage,
helperOrMessage,
inputDataAttrs,
inputAttrs,
onBlur,
onChange,
runValidation,
clearValidation,
} = useInputField({ props, valueRef: internalValue, emit });useAutoResize(textareaEl, value, options)
Auto-resize textarea to fit content. Respects min rows, max rows, padding, and border widths.
ts
import { useAutoResize } from "@vibe-labs/design-vue-inputs";
useAutoResize(textareaEl, value, {
enabled: toRef(props, "autoResize"),
maxRows: toRef(props, "maxRows"),
rows: toRef(props, "rows"),
});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, input components can auto-register their validate/clearValidation with a parent VibeForm.
Validation
Validation can come from three sources (in priority order):
- Props —
error/successprops (highest priority) - Rules —
ValidationRule[]on the input itself - Form-level — from
VibeForm'svalidatefunction viaform.field()error binding
ts
interface ValidationRule {
validate: (value: string) => true | string;
trigger?: "change" | "blur" | "submit";
}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.