Skip to content

@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

PropTypeDefaultDescription
modelValuestringInput value
typeInputType"text"text · email · password · url · tel · search
sizeInputSize"md"sm · md · lg
labelstringField label
placeholderstringPlaceholder text
helperTextstringHelper text below input
errorstringError message (sets error state)
successstringSuccess message (sets success state)
disabledbooleanfalseDisabled state
readonlybooleanfalseRead-only state
requiredbooleanfalseRequired indicator on label
maxlengthnumberMax characters
showCountbooleanfalseShow character count (requires maxlength)
debouncenumber0Debounce delay in ms
rulesValidationRule[]Validation rules
validateOn"change" | "blur" | "submit""blur"When to validate
patternstringHTML pattern attribute
autocompletestringAutocomplete hint
idstringautoHTML id
namestringForm name

Events

EventPayloadDescription
update:modelValuestringValue changed
validateInputValidationEventValidation ran
focusFocusEventInput focused
blurFocusEventInput blurred
clearSearch cleared

Slots

SlotDescription
leadingLeading content (icons, prefix text)
trailingTrailing content (suffix, icons)
clearIconCustom clear button icon
revealIconCustom password reveal icon (receives visible prop)

Exposed Methods

MethodDescription
focus()Focus the input
blur()Blur the input
select()Select all text
validate()Run validation programmatically
clearValidation()Clear validation state
elDirect 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

PropTypeDefaultDescription
modelValuenumber | nullNumeric value
minnumberMinimum value
maxnumberMaximum value
stepnumber1Increment step
precisionnumberDecimal precision
controlsbooleantrueShow +/- buttons
nullablebooleantrueAllow 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

PropTypeDefaultDescription
labelstringGroup label
errorstringGroup-level error message
helperTextstringHelper text below the group
direction"vertical" | "horizontal""vertical"Layout direction
sizeInputSize"md"Size applied to children via CSS scope
seamlessbooleanfalseCollapse 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

PropTypeDefaultDescription
modelValuestringTextarea value
rowsnumber3Visible rows
autoResizebooleanfalseAuto-grow to content
maxRowsnumberMax rows when auto-resizing
maxlengthnumberMax characters
showCountbooleanfalseShow character count
resizablebooleantrueManual 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

PropTypeDefaultDescription
modelValueboolean | string[]Checked state or group array
valuestringValue when used in a group
labelstringLabel text
sizeInputSize"md"sm · md · lg
indeterminatebooleanfalseIndeterminate state
disabledbooleanfalseDisabled
errorbooleanfalseError styling
namestringForm 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

PropTypeDefaultDescription
labelstringGroup label
horizontalbooleanfalseHorizontal layout
errorstringGroup 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

PropTypeDefaultDescription
modelValuestringCurrently selected value
valuestringrequiredThis radio's value
labelstringLabel text
sizeInputSize"md"sm · md · lg
disabledbooleanfalseDisabled
errorbooleanfalseError styling
namestringName (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

PropTypeDefaultDescription
modelValuestringSelected value
labelstringGroup label
namestringShared name for all radios
horizontalbooleanfalseHorizontal layout
errorstringGroup 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):

  1. Propserror / success props (highest priority)
  2. RulesValidationRule[] on the input itself
  3. Form-level — from VibeForm's validate function via form.field() error binding
ts
interface ValidationRule {
  validate: (value: string) => true | string;
  trigger?: "change" | "blur" | "submit";
}

Dependencies

PackagePurpose
@vibe-labs/design-components-inputsCSS tokens + generated styles

Build

bash
npm run build

Built with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.