Appearance
@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
| 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 |
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
| 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)
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
| 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 |
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
| 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 |
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
| 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 |
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
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Group label |
horizontal | boolean | false | Horizontal layout |
error | string | — | Group 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
| 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) |
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
| 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 |
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
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | Combined digit string |
digits | number | 6 | Number of digit boxes |
size | InputSize | "md" | sm · md · lg |
masked | boolean | false | Hide digits (password dots) |
disabled | boolean | false | Disabled state |
error | boolean | string | false | Error state or message |
autofocus | boolean | false | Focus first box on mount |
ariaLabel | string | "PIN code" | Accessible label for the group |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | Combined value changed |
complete | string | All digits filled |
Exposed Methods
| Method | Description |
|---|---|
focus() | Focus the first empty box (or the first box) |
clear() | Clear all digits and focus the first box |
value | ComputedRef<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:
- Props —
error/successprops (highest priority) - Rules —
ValidationRule[]on the input itself - Form-level — from
CarrotForm'svalidatefunction viaform.field()error binding
Dependencies
| Package | Purpose |
|---|---|
@carrot/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 {
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>