Skip to content

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

Hint text below the field
Show code
vue
<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.

We'll never share your email
Show code
vue
<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.

@
USD
0/20 characters
Show code
vue
<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.

Min 3 characters, must be unique
We'll send a verification link

Try: admin, user, root (taken) or test@taken.com

Show code
vue
<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
vue
<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:

ts
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 statebase plus optional per-state overrides. CInput owns the root and details zones, while the field preset (CFieldPreset) is composed in by value:

ts
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:

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

Slot
Props
Description
prepend
Content placed on the left inside the field border
append
Content placed on the right inside the field border
menu
{ id: string }
Dropdown content associated with the field
details
CInputDetailsSlotProps
Replaces the entire hint/error area
Prop
Type
Description
id
string
Menu ID associated with the field (uid-menu)

details slot props

Prop
Type
Description
errorMessage
string | undefined
Current error message
hasError
boolean
Whether there is an active error
validating
boolean
Whether async validation is currently running
uid
string
Field ID (matches the native <input> id)
details
string | undefined
The value of the details prop

Events

Event
Arguments
Description
update:modelValue
string | number | undefined
Value changed (v-model)
focus
Field received focus
blur
Field lost focus

Expose

Methods available via template ref:

Method
Signature
Description
validate
() => Promise<boolean>
Trigger validation manually
reset
() => void
Clear the error state
focus
() => void
Programmatically focus the field; no-op when disabled/readonly
blur
() => void
Programmatically remove focus
isReadonly
() => boolean | undefined
Return the current readonly prop value
isDisabled
() => boolean | undefined
Return the current disabled prop value
vue
<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

ts
type ValidateResult = { valid: boolean; message: string }
type ValidateFn = (value: any) => ValidateResult | Promise<ValidateResult>

CSS variables

CInput (root element)

Variable
Default
Description
--c-input-details-height
var(--c-sys-control-height-sm)
Height of the details area
--c-input-transition-duration
var(--c-sys-motion-duration-medium)
Color transition duration
--c-input-primary-color
var(--c-sys-color-primary)
Base text color
--c-input-error-color
var(--c-sys-color-error)
Text color on error
--c-input-disabled-color
var(--c-sys-color-disabled)
Text color when disabled
--c-input-readonly-color
var(--c-sys-color-readonly)
Text color when readonly

CField (border and label)

Variable
Default
Description
--c-field-min-height
var(--c-sys-control-height-md)
Minimum field height
--c-field-prepend-min-width
var(--c-sys-control-height-md)
Min width of the prepend zone
--c-field-append-min-width
var(--c-sys-control-icon-size)
Min width of the append zone
--c-field-padding-inline
var(--c-sys-control-padding-inline)
Horizontal field padding
--c-field-border-radius
var(--c-sys-shape-md)
Field border radius
--c-field-transition-duration
var(--c-sys-motion-duration-medium)
Field transition duration
--c-field-density-offset
var(--c-sys-density-scale)
Added height offset
--c-field-bg-color
var(--c-sys-color-surface)
Field background
--c-field-focused-bg-color
var(--c-sys-color-surface-bright)
Field background on focus
--c-field-disabled-bg-color
var(--c-sys-color-surface-dim)
Field background when disabled
--c-field-border-color
var(--c-sys-color-outline)
Border color
--c-field-border-width
var(--c-sys-border-width-thin)
Border width
--c-field-input-text-color
var(--c-sys-color-on-surface)
Typed text color
--c-field-placeholder-color
var(--c-sys-color-placeholder)
Placeholder color
--c-field-error-bg-color
var(--c-sys-color-surface-bright)
Field background on error
--c-field-error-border-color
var(--c-sys-color-error)
Border color on error
--c-field-readonly-bg-color
var(--c-sys-color-readonly-container)
Field background when readonly
--c-field-disabled-opacity
var(--c-sys-state-disabled-opacity)
Opacity when disabled

Override example

vue
<c-text-field
  v-model="value"
  label="Custom styled"
  style="
    --c-input-primary-color: #7c3aed;
    --c-field-border-color: #ddd6fe;
  "
/>

State CSS classes

Class
Condition
c-input--focused
Field is focused
c-input--has-error
Validation error is active (and not disabled/readonly)
c-input--disabled
disabled = true
c-input--readonly
readonly = true
c-input--clearable
clearable = true
c-field--focused
Border is focused
c-field--filled
Field has a value (label is raised)
c-field--error
Validation error is active
c-field--disabled
Border disabled
c-field--readonly
Border readonly
c-field--has-prepend
prepend slot is present