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 -->
<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.

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

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.

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

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.

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

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.

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

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 -->
<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.

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:

  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

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.


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>

Vibe