Skip to content

@carrot/design-vue-inputs

Vue 3 input components for the Carrot Design System. Text inputs, number inputs, textareas, checkboxes, and radio buttons with built-in validation, ARIA bindings, and @carrot/design-vue-forms integration.

Installation

ts
import {
  CarrotInput,
  CarrotInputNumber,
  CarrotInputNumberPin,
  CarrotInputGroup,
  CarrotTextArea,
  CarrotCheckbox,
  CarrotCheckboxGroup,
  CarrotRadio,
  CarrotRadioGroup,
} from "@carrot/design-vue-inputs";

Requires the CSS layer from @carrot/design-components-inputs.

Components

CarrotInput

Text input with validation, debounce, password reveal, search clear, and character count.

Usage

vue
<!-- Basic -->
<CarrotInput v-model="email" label="Email" type="email" placeholder="you@example.com" />

<!-- With form binding -->
<CarrotInput v-bind="form.field('email')" label="Email" type="email" />

<!-- Validation rules -->
<CarrotInput v-model="email" label="Email" :rules="[required(), email()]" validate-on="blur" />

<!-- Error/success from props -->
<CarrotInput v-model="name" label="Name" error="Name is required" />
<CarrotInput v-model="name" label="Name" success="Looks good!" />

<!-- Sizes -->
<CarrotInput v-model="val" size="sm" />
<CarrotInput v-model="val" size="lg" />

<!-- Password with reveal toggle -->
<CarrotInput v-model="password" label="Password" type="password" />

<!-- Search with clear button -->
<CarrotInput v-model="query" label="Search" type="search" />

<!-- Character count -->
<CarrotInput v-model="bio" label="Bio" :maxlength="200" show-count />

<!-- Debounced input -->
<CarrotInput v-model="search" label="Search" :debounce="300" />

<!-- Leading/trailing slots -->
<CarrotInput v-model="amount" label="Price">
  <template #leading>£</template>
  <template #trailing>.00</template>
</CarrotInput>

<!-- Required -->
<CarrotInput v-model="name" label="Full Name" required />

<!-- Disabled / readonly -->
<CarrotInput v-model="locked" label="Locked" disabled />
<CarrotInput 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

CarrotInputNumber

Numeric input with increment/decrement controls, precision, clamping, and hold-to-repeat.

Usage

vue
<!-- Basic -->
<CarrotInputNumber v-model="quantity" label="Quantity" :min="1" :max="99" />

<!-- With step and precision -->
<CarrotInputNumber v-model="price" label="Price" :step="0.01" :precision="2" />

<!-- Without controls -->
<CarrotInputNumber v-model="value" label="Value" :controls="false" />

<!-- Non-nullable -->
<CarrotInputNumber v-model="count" label="Count" :nullable="false" :min="0" />

<!-- Leading slot -->
<CarrotInputNumber v-model="price" label="Price">
  <template #leading>£</template>
</CarrotInputNumber>

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)

CarrotInputGroup

Groups related inputs in a fieldset with shared label, error, helper text, and layout options.

Usage

vue
<!-- Vertical group (default) -->
<CarrotInputGroup label="Address">
  <CarrotInput v-model="line1" placeholder="Line 1" />
  <CarrotInput v-model="line2" placeholder="Line 2" />
  <CarrotInput v-model="city" placeholder="City" />
</CarrotInputGroup>

<!-- Horizontal group -->
<CarrotInputGroup label="Date Range" direction="horizontal">
  <CarrotInput v-model="from" placeholder="From" />
  <CarrotInput v-model="to" placeholder="To" />
</CarrotInputGroup>

<!-- Seamless (collapsed borders) -->
<CarrotInputGroup label="Phone" direction="horizontal" seamless>
  <CarrotInput v-model="code" placeholder="+44" size="sm" />
  <CarrotInput v-model="number" placeholder="Number" />
</CarrotInputGroup>

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

CarrotTextArea

Multi-line textarea with auto-resize, character count, and validation.

Usage

vue
<!-- Basic -->
<CarrotTextArea v-model="description" label="Description" :rows="4" />

<!-- Auto-resize -->
<CarrotTextArea v-model="notes" label="Notes" auto-resize :max-rows="10" />

<!-- Character count -->
<CarrotTextArea v-model="bio" label="Bio" :maxlength="500" show-count />

<!-- Non-resizable -->
<CarrotTextArea 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

CarrotCheckbox

Single checkbox or group-compatible checkbox with indeterminate support.

Usage

vue
<!-- Boolean (single) -->
<CarrotCheckbox v-model="agreed" label="I agree to the terms" />

<!-- Indeterminate -->
<CarrotCheckbox v-model="selectAll" :indeterminate="isPartial" label="Select all" />

<!-- In a group (string array) -->
<CarrotCheckboxGroup label="Genres">
  <CarrotCheckbox v-model="selected" value="rock" label="Rock" />
  <CarrotCheckbox v-model="selected" value="pop" label="Pop" />
  <CarrotCheckbox v-model="selected" value="jazz" label="Jazz" />
</CarrotCheckboxGroup>

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

CarrotCheckboxGroup

Wraps multiple checkboxes in a fieldset with label and error support.

vue
<CarrotCheckboxGroup label="Interests" error="Select at least one" horizontal>
  <CarrotCheckbox v-model="interests" value="music" label="Music" />
  <CarrotCheckbox v-model="interests" value="art" label="Art" />
</CarrotCheckboxGroup>

Props

PropTypeDefaultDescription
labelstringGroup label
horizontalbooleanfalseHorizontal layout
errorstringGroup error message

CarrotRadio

Single radio button — works standalone or within a CarrotRadioGroup.

vue
<!-- Standalone -->
<CarrotRadio v-model="plan" value="free" label="Free" name="plan" />
<CarrotRadio v-model="plan" value="pro" label="Pro" name="plan" />

<!-- In a group -->
<CarrotRadioGroup v-model="plan" label="Plan" name="plan">
  <CarrotRadio value="free" label="Free" />
  <CarrotRadio value="pro" label="Pro" />
  <CarrotRadio value="enterprise" label="Enterprise" />
</CarrotRadioGroup>

Props

PropTypeDefaultDescription
modelValuestringCurrently selected value
valuestringrequiredThis radio's value
labelstringLabel text
sizeInputSize"md"sm · md · lg
disabledbooleanfalseDisabled
errorbooleanfalseError styling
namestringName (auto from group)

CarrotRadioGroup

Wraps radios with shared name/value context via provide/inject.

vue
<CarrotRadioGroup v-model="size" label="Size" name="size" horizontal>
  <CarrotRadio value="sm" label="Small" />
  <CarrotRadio value="md" label="Medium" />
  <CarrotRadio value="lg" label="Large" />
</CarrotRadioGroup>

Props

PropTypeDefaultDescription
modelValuestringSelected value
labelstringGroup label
namestringShared name for all radios
horizontalbooleanfalseHorizontal layout
errorstringGroup error message

CarrotInputNumberPin

Numeric PIN / OTP input with individual digit boxes. Supports auto-advance, smart backspace, arrow key navigation, paste support, and masked mode.

Usage

vue
<!-- Basic 6-digit PIN -->
<CarrotInputNumberPin v-model="pin" @complete="onVerify" />

<!-- 4-digit, masked -->
<CarrotInputNumberPin v-model="otp" :digits="4" masked autofocus />

<!-- With error message -->
<CarrotInputNumberPin v-model="pin" error="Invalid code" />

<!-- Small size -->
<CarrotInputNumberPin v-model="pin" size="sm" :digits="4" />

Props

PropTypeDefaultDescription
modelValuestringCombined digit string
digitsnumber6Number of digit boxes
sizeInputSize"md"sm · md · lg
maskedbooleanfalseHide digits (password dots)
disabledbooleanfalseDisabled state
errorboolean | stringfalseError state or message
autofocusbooleanfalseFocus first box on mount
ariaLabelstring"PIN code"Accessible label for the group

Events

EventPayloadDescription
update:modelValuestringCombined value changed
completestringAll digits filled

Exposed Methods

MethodDescription
focus()Focus the first empty box (or the first box)
clear()Clear all digits and focus the first box
valueComputedRef<string> — the combined digit value

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 @carrot/design-vue-forms via two paths:

1. form.field() binding — the easy way:

vue
<CarrotInput 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 CarrotForm's validate function via form.field() error binding

Dependencies

PackagePurpose
@carrot/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 {
  CarrotInput,
  CarrotInputNumber,
  CarrotInputNumberPin,
  CarrotInputGroup,
  CarrotTextArea,
  CarrotCheckbox,
  CarrotCheckboxGroup,
  CarrotRadio,
  CarrotRadioGroup,
} from "@carrot/design-vue-inputs";
import "@carrot/design-components-inputs";

CarrotInput

Standard text input with validation

vue
<script setup lang="ts">
import { CarrotInput } from "@carrot/design-vue-inputs";
import { required, email } from "@carrot/design-vue-forms";
import { ref } from "vue";

const emailValue = ref("");
</script>

<template>
  <CarrotInput
    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 { CarrotInput } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const query = ref("");

watch(query, (val) => {
  performSearch(val);
});
</script>

<template>
  <!-- Emits model update after 300ms of inactivity -->
  <CarrotInput
    v-model="query"
    type="search"
    placeholder="Search tracks..."
    :debounce="300"
    label="Search"
  />
</template>

Input with leading/trailing slots

vue
<script setup lang="ts">
import { CarrotInput } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const price = ref("");
</script>

<template>
  <CarrotInput v-model="price" label="Price" type="text">
    <template #leading>£</template>
    <template #trailing>GBP</template>
  </CarrotInput>
</template>

CarrotInputNumber

Quantity selector

vue
<script setup lang="ts">
import { CarrotInputNumber } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const quantity = ref(1);
</script>

<template>
  <CarrotInputNumber
    v-model="quantity"
    label="Quantity"
    :min="1"
    :max="99"
    :nullable="false"
  />
</template>

CarrotTextArea

Bio field with character count

vue
<script setup lang="ts">
import { CarrotTextArea } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const bio = ref("");
</script>

<template>
  <CarrotTextArea
    v-model="bio"
    label="Artist Bio"
    auto-resize
    :max-rows="8"
    :maxlength="500"
    show-count
    helper-text="Describe yourself in a few sentences"
  />
</template>

CarrotCheckbox / CarrotCheckboxGroup

Multi-select genres

vue
<script setup lang="ts">
import { CarrotCheckbox, CarrotCheckboxGroup } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const selectedGenres = ref<string[]>([]);
</script>

<template>
  <CarrotCheckboxGroup label="Preferred Genres" horizontal>
    <CarrotCheckbox v-model="selectedGenres" value="electronic" label="Electronic" />
    <CarrotCheckbox v-model="selectedGenres" value="jazz" label="Jazz" />
    <CarrotCheckbox v-model="selectedGenres" value="hip-hop" label="Hip Hop" />
    <CarrotCheckbox v-model="selectedGenres" value="classical" label="Classical" />
  </CarrotCheckboxGroup>
</template>

Select-all with indeterminate state

vue
<script setup lang="ts">
import { CarrotCheckbox, CarrotCheckboxGroup } from "@carrot/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>
  <CarrotCheckbox
    :model-value="allSelected"
    :indeterminate="isPartial"
    label="Select all"
    @update:model-value="toggleAll"
  />
  <CarrotCheckboxGroup label="Tracks">
    <CarrotCheckbox v-for="item in items" :key="item" v-model="selected" :value="item" :label="item" />
  </CarrotCheckboxGroup>
</template>

CarrotRadioGroup

Plan selector

vue
<script setup lang="ts">
import { CarrotRadio, CarrotRadioGroup } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const plan = ref("free");
</script>

<template>
  <CarrotRadioGroup v-model="plan" label="Choose a plan" name="plan">
    <CarrotRadio value="free" label="Free" />
    <CarrotRadio value="pro" label="Pro — £9/mo" />
    <CarrotRadio value="enterprise" label="Enterprise" />
  </CarrotRadioGroup>
</template>

CarrotInputNumberPin

Verification code input

vue
<script setup lang="ts">
import { CarrotInputNumberPin } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const code = ref("");
const error = ref<string | false>(false);

async function onComplete(value: string) {
  try {
    await verifyCode(value);
  } catch {
    error.value = "Invalid code. Please try again.";
  }
}
</script>

<template>
  <CarrotInputNumberPin
    v-model="code"
    :digits="6"
    :error="error"
    autofocus
    @complete="onComplete"
  />
</template>

Masked 4-digit PIN

vue
<script setup lang="ts">
import { CarrotInputNumberPin } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const pin = ref("");
</script>

<template>
  <CarrotInputNumberPin v-model="pin" :digits="4" masked size="lg" />
</template>

CarrotInputGroup

Phone number with country code

vue
<script setup lang="ts">
import { CarrotInput, CarrotInputGroup } from "@carrot/design-vue-inputs";
import { ref } from "vue";

const countryCode = ref("+44");
const phone = ref("");
</script>

<template>
  <CarrotInputGroup label="Phone number" direction="horizontal" seamless>
    <CarrotInput v-model="countryCode" size="sm" style="max-width: 80px" />
    <CarrotInput v-model="phone" placeholder="7700 900123" type="tel" />
  </CarrotInputGroup>
</template>

Common Patterns

Wire to @carrot/design-vue-forms

vue
<script setup lang="ts">
import { CarrotForm, required } from "@carrot/design-vue-forms";
import { CarrotInput, CarrotTextArea } from "@carrot/design-vue-inputs";

const initialValues = { title: "", notes: "" };
</script>

<template>
  <CarrotForm v-slot="form" :initial-values="initialValues" @submit="onSubmit">
    <!-- form.field() spreads modelValue, error, name, and onBlur -->
    <CarrotInput v-bind="form.field('title')" label="Title" required />
    <CarrotTextArea v-bind="form.field('notes')" label="Notes" auto-resize />
    <button type="submit">Save</button>
  </CarrotForm>
</template>

Carrot