CTextField
CTextField is the primary text input component. Built on top of CInput, it provides a fully styled, accessible, and validatable <input> with a floating label, icon slots, hint text, and theme support.
Usage
Show code
<template>
<c-text-field v-model="value" id="basic-email" label="Email" placeholder="Enter your email" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref('')
</script>States
CTextField supports all standard states: default, disabled, readonly, and clearable.
Show code
<template>
<c-text-field v-model="value" label="Default" />
<c-text-field v-model="value" label="Disabled" disabled />
<c-text-field v-model="readonly" label="Readonly" readonly />
<c-text-field v-model="value" label="Clearable" clearable />
</template>Validation
Pass an array of rule functions via the rules prop. Each rule receives the current value and returns { valid: boolean, message: string }. Use validate-on to control when validation fires: 'input' (default) or 'blur'.
If modelValue holds the displayed text and you need to validate a different value, pass it via the validation-value prop — rules receive it instead of modelValue. This is how CSelect validates the selected model rather than the string shown in the field.
Show code
<template>
<c-text-field
v-model="email"
label="Email"
:rules="emailRules"
validate-on="blur"
details="We'll never share your email"
/>
<c-text-field
v-model="password"
label="Password"
type="password"
:rules="passwordRules"
validate-on="blur"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const email = ref('')
const password = ref('')
const emailRules = [
(v: string) => ({ valid: !!v, message: 'Email is required' }),
(v: string) => ({ valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: 'Invalid email' }),
]
const passwordRules = [
(v: string) => ({ valid: !!v, message: 'Password is required' }),
(v: string) => ({ valid: v.length >= 8, message: 'Minimum 8 characters' }),
]
</script>Prepend, append and details slots
The prepend and append slots place content inside the field borders. The details slot fully replaces the hint/error area.
Show code
<template>
<!-- Prepend icon -->
<c-text-field v-model="search" label="Search">
<template #prepend>
<c-icon name="mdi-magnify" />
</template>
</c-text-field>
<!-- Append text -->
<c-text-field v-model="amount" label="Amount" type="number">
<template #append>
<span style="opacity: .6; font-size: 13px">USD</span>
</template>
</c-text-field>
<!-- Custom details slot -->
<c-text-field v-model="nickname" label="Nickname" :rules="nicknameRules" validate-on="input">
<template #details="{ errorMessage, hasError }">
<span :style="{ color: hasError ? 'var(--c-sys-color-error)' : 'inherit' }">
{{ errorMessage || `${nickname.length}/20 characters` }}
</span>
</template>
</c-text-field>
</template>Async validation
Rules may return a Promise. While validation is in progress, the details slot receives validating: true.
Try: admin, user, root (taken) or test@taken.com
Show code
<template>
<c-text-field v-model="username" label="Username" :rules="usernameRules" validate-on="blur">
<template #details="{ errorMessage, hasError, validating }">
<span v-if="validating" style="color: var(--c-sys-color-primary)">
Checking availability…
</span>
<span v-else-if="hasError" style="color: var(--c-sys-color-error)">
{{ errorMessage }}
</span>
<span v-else style="opacity: .6">Must be unique</span>
</template>
</c-text-field>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const username = ref('')
const taken = ['admin', 'user', 'root']
const usernameRules = [
(v: string) => ({ valid: v.length >= 3, message: 'Minimum 3 characters' }),
async (v: string) => {
await new Promise((resolve) => setTimeout(resolve, 800))
return { valid: !taken.includes(v.toLowerCase()), message: `"${v}" is already taken` }
},
]
</script>Presets
Presets let you define the field's appearance (label color, border) once during plugin initialization and reuse by name via the preset prop.
Show code
<template>
<c-text-field v-model="value" label="Email" preset="input.blue">
<template #prepend><c-icon name="fas:envelope" :size="16" source="fa" /></template>
</c-text-field>
</template>Register presets when initializing the plugin:
import { createVuelandUI } from '@vueland/ui'
import type { CInputPreset } from '@vueland/ui/types'
createVuelandUI({
presets: {
input: {
blue: {
base: {
field: {
base: { label: ['text-blue'] },
focused: { label: ['text-blue'], root: ['text-blue'] },
filled: { label: ['text-blue'] },
error: { label: ['text-red'] },
},
},
error: { details: ['text-red'] },
} satisfies CInputPreset,
},
},
})CInputPreset structure
A preset is a set of snapshots keyed by state — base plus optional per-state overrides. CInput owns the root and details zones, while the field preset (CFieldPreset) is composed in by value:
type CInputZone = 'root' | 'details'
type CInputState = 'focused' | 'filled' | 'error' | 'disabled' | 'readonly'
type CInputSnapshot = Partial<Record<CInputZone, string[]>> & {
field?: CFieldPreset
menu?: CMenuPreset
list?: CListPreset
}
type CInputPreset = Partial<Record<'base' | CInputState, CInputSnapshot>>The component is in a single current state, and that state's snapshot is applied — its zones replace base per-zone, no stacking and no priorities. See CInput → Preset system for the full model.
The preset is distributed automatically: CInput applies root and details and shares the set with the subtree via provide/inject; CField picks up the nested field preset from the base snapshot and resolves its own states (field root, input, label, prepend, append) itself.
API
Props
CTextField accepts CInput props, including label, details, clearable, disabled, readonly, focused, dirty, rules, validateOn, validationValue, and preset.
v-model works with string | number | null | undefined.
Native attributes
CTextField does not wrap <input> attributes in its own props. Thanks to inheritAttrs, any non-prop attribute falls through to the inner <input> as-is — so just use the standard HTML attributes directly:
<c-text-field
type="number"
placeholder="0"
:min="0"
:max="100"
:step="5"
inputmode="numeric"
maxlength="10"
autocomplete="off"
name="amount"
required
/>pattern, minlength, tabindex, enterkeyhint and any data-* / aria-* attributes fall through the same way. These are not documented as props — they are the standard native <input> contract.
Slots
prependappendmenu{ id: string }detailsCInputDetailsSlotPropsmenu slot props
idstringuid-menu)details slot props
errorMessagestring | undefinedhasErrorbooleanvalidatingbooleanuidstring<input> id)detailsstring | undefineddetails propEvents
update:modelValuestring | number | undefinedfocusblurExpose
Methods available via template ref:
validate() => Promise<boolean>reset() => voidfocus() => voiddisabled/readonlyblur() => voidisReadonly() => boolean | undefinedreadonly prop valueisDisabled() => boolean | undefineddisabled prop value<template>
<c-text-field ref="fieldRef" v-model="value" label="Name" :rules="rules" />
<c-btn @click="fieldRef?.validate()">Validate</c-btn>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const fieldRef = ref()
const value = ref('')
const rules = [(v: string) => ({ valid: !!v, message: 'Required' })]
</script>ValidateFn type
type ValidateResult = { valid: boolean; message: string }
type ValidateFn = (value: any) => ValidateResult | Promise<ValidateResult>CSS variables
CInput (root element)
--c-input-details-heightvar(--c-sys-control-height-sm)--c-input-transition-durationvar(--c-sys-motion-duration-medium)--c-input-primary-colorvar(--c-sys-color-primary)--c-input-error-colorvar(--c-sys-color-error)--c-input-disabled-colorvar(--c-sys-color-disabled)--c-input-readonly-colorvar(--c-sys-color-readonly)CField (border and label)
--c-field-min-heightvar(--c-sys-control-height-md)--c-field-prepend-min-widthvar(--c-sys-control-height-md)--c-field-append-min-widthvar(--c-sys-control-icon-size)--c-field-padding-inlinevar(--c-sys-control-padding-inline)--c-field-border-radiusvar(--c-sys-shape-md)--c-field-transition-durationvar(--c-sys-motion-duration-medium)--c-field-density-offsetvar(--c-sys-density-scale)--c-field-bg-colorvar(--c-sys-color-surface)--c-field-focused-bg-colorvar(--c-sys-color-surface-bright)--c-field-disabled-bg-colorvar(--c-sys-color-surface-dim)--c-field-border-colorvar(--c-sys-color-outline)--c-field-border-widthvar(--c-sys-border-width-thin)--c-field-input-text-colorvar(--c-sys-color-on-surface)--c-field-placeholder-colorvar(--c-sys-color-placeholder)--c-field-error-bg-colorvar(--c-sys-color-surface-bright)--c-field-error-border-colorvar(--c-sys-color-error)--c-field-readonly-bg-colorvar(--c-sys-color-readonly-container)--c-field-disabled-opacityvar(--c-sys-state-disabled-opacity)Override example
<c-text-field
v-model="value"
label="Custom styled"
style="
--c-input-primary-color: #7c3aed;
--c-field-border-color: #ddd6fe;
"
/>State CSS classes
c-input--focusedc-input--has-errordisabled/readonly)c-input--disableddisabled = truec-input--readonlyreadonly = truec-input--clearableclearable = truec-field--focusedc-field--filledc-field--errorc-field--disabledc-field--readonlyc-field--has-prepend