Inputs
All Colorffy form inputs share a common base — id, label, placeholder, disabled, required, readonly, errorMessages, optionalLabel, variant, size, rounded, hideLabel, customClass — and bind with v-model.
Text and textarea
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('')
const bio = ref('')
</script>
<template>
<UiInputText v-model="name" label="Name" placeholder="Jane Doe" required />
<UiInputTextarea v-model="bio" label="Bio" :rows="4" />
</template>
Select
options accepts an array; map labels / values with optionLabel / optionValue:
<script setup lang="ts">
import { ref } from 'vue'
const role = ref(null)
const options = [
{ name: 'Admin', value: 'admin' },
{ name: 'User', value: 'user' },
]
</script>
<template>
<UiInputSelect
v-model="role"
:options="options"
option-label="name"
option-value="value"
label="Role"
/>
</template>
UiInputSelect is the native <select>, the better choice on phones for a short list. For a long list, a list to search, or options that need more than a line of text, use UiInputCombobox.
Combobox
UiInputCombobox picks one value from a list you can search. It takes the same options, optionLabel and optionValue as UiInputSelect, plus the shared base props.
<script setup lang="ts">
import { ref } from 'vue'
const leadId = ref<string | null>(null)
</script>
<template>
<UiInputCombobox
id="lead"
v-model="leadId"
label="Project lead"
placeholder="Search the team"
:options="members"
option-label="name"
option-value="id"
option-group="team"
option-disabled="away"
clearable
>
<template #option="{ option }">
<UiAvatar :src="option.avatar" size="sm" />
{{ option.name }}
</template>
</UiInputCombobox>
</template>
- Searching. Typing filters the options by label, ignoring case and accents, and highlights the first match. Leaving the field with text that matches an option exactly selects it; leaving it empty clears the value; anything else puts the selected label back.
- Select-only. Set
:filterable="false"for a field that works like a select: typing jumps to the first option that starts with the typed letters. - Options.
optionGroupnames the field that groups options under headings, in the order each group first appears, andoptionDisabledthe field that disables one. WithoutoptionValue, the model holds the whole option and object options match by reference. The#optionslot ({ option, selected, active }) renders each row, and#empty({ query }) replacesemptyText('No results'). - Keyboard. ↓ and ↑ open the list and move through it, Page Down and Page Up jump ten options, Enter picks, and Esc closes the list. With
clearable, a second Esc clears the value; inside a dialog, the dialog closes only on the Esc after that. Focus stays in the field the whole time. - The list is a popover anchored to the field. It opens below and flips above when there isn't room, and it sits on top of dialogs and outside scrolling or clipping containers. Browsers without CSS anchor positioning get the same placement from a small script.
Search on the server
Set remote when the options come from a server. Typing emits search with the trimmed text once typing pauses (searchDelay, 300 ms) and at least minSearchLength characters (1) are typed; a shorter text shows "Type to search". Fetch the matches, pass them in options and set loading while you wait: the list shows "Searching…", then screen readers hear how many results arrived. An emptied field, or closing the list after a search, emits search(''), so you can go back to the default options or clear them.
<script setup lang="ts">
import { ref } from 'vue'
const client = ref<string | null>(null)
const results = ref<string[]>([])
const loading = ref(false)
let latest = 0
async function search(query: string) {
const request = ++latest
if (!query) {
results.value = []
return
}
loading.value = true
const found = await $fetch<string[]>('/api/clients', { query: { q: query } })
// A slower, older request must not overwrite newer results
if (request === latest) {
results.value = found
loading.value = false
}
}
</script>
<template>
<UiInputCombobox
id="client"
v-model="client"
label="Client"
placeholder="Search the CRM or type a new client"
:options="results"
remote
:loading="loading"
free-text
@search="search"
/>
</template>
A picked option keeps its label when the next search replaces options. For a value set before any search, such as an edit form, include its option in the first options so the field can show its label.
- Free text.
freeTextlets the field hold text that isn't an option: Enter, or leaving the field, stores what's typed. Typing doesn't highlight a suggestion, so ↓ picks one; text that matches an option exactly still selects it. It works with or withoutremote.
Multi-select
UiInputMultiSelect picks several values from the same kind of list. The model is an array, in the order the values were picked; everything else works as on UiInputCombobox: options, optionLabel, optionValue, optionGroup, optionDisabled, filterable, clearable and the #option / #empty slots.
<script setup lang="ts">
import { ref } from 'vue'
const projectIds = ref<string[]>([])
</script>
<template>
<UiInputMultiSelect
id="projects"
v-model="projectIds"
label="Add to projects"
placeholder="Search projects"
:options="projects"
option-label="name"
option-value="id"
option-group="status"
:max="5"
clearable
/>
</template>
- Picking. Enter or a click toggles the highlighted option, and the list stays open for the next one. A search clears after each pick.
- The field shows a chip per value, like
UiInputTags: each chip has a remove button, and Backspace in an empty field removes the last one. SetmaxChipsto keep the field to one row: past that many values, the chips give way to themaxChipsLabelsummary ('{count} selected', or a function such as({ count }) => count === 1 ? '1 skill' : `${count} skills`), long chip labels are cut with an ellipsis, and the summary steps aside while you search.:max-chips="0"always shows the summary, for tight spaces such as a toolbar filter. - Limits.
maxcaps the number of values; once it's reached, the other options are disabled until one is removed. - Events. Besides
update:modelValueandupdate, it emitsaddandremovewith the value. Screen readers hear each change, and the field's description lists what's selected. - Search on the server.
remote,loading,searchDelay,minSearchLengthand thesearchevent work as on the combobox. Chips keep their labels across searches. - Free text.
freeTextends the list with an "Add “…”" row for typed text that isn't an option or already picked; Enter on it, or a click, adds the text as a value. Matches come first, so Enter still picks the first match and ↓ reaches the row. Use it for labels or tags with suggestions.
Date
UiInputDate is a date field with a calendar popup. The model is a Date at midnight local time, { start, end } with mode="range", or a Date[] with mode="multiple", as on UiCalendar.
<script setup lang="ts">
import { ref } from 'vue'
const dueDate = ref<Date | null>(null)
</script>
<template>
<UiInputDate id="due" v-model="dueDate" label="Due date" :min="new Date()" clearable />
</template>
A range with presets, the setup a report filter usually needs:
<script setup lang="ts">
import type { IDateRange } from '@colorffy/ui'
import { datePresets } from '@colorffy/ui'
const period = ref<IDateRange | null>(datePresets.lastDays(7).value() as IDateRange)
const presets = [
datePresets.today(),
datePresets.lastDays(7),
datePresets.lastDays(30),
datePresets.thisMonth(),
datePresets.lastMonth(),
]
</script>
<template>
<UiInputDate id="period" v-model="period" mode="range" label="Report period" :presets="presets" />
</template>
- Typing. The field shows the locale's numeric date (
10/05/2026inen-US,05/10/2026ines-SV) and accepts dates typed in the same order, or year first (2026-10-05). A range is two dates with a dash between them. The value updates on Enter or when you leave the field; text that isn't an allowed date puts the current value back. The placeholder shows the order (mm/dd/yyyy); passplaceholderto translate it. - Picking. Clicking the field opens the calendar and leaves focus in the field; ↓ or the calendar button moves focus into the calendar. A date, or a range's end, closes the popup and updates the value. A range is written only once both ends are picked.
- Presets.
presetslists shortcuts next to the calendar. Each is{ label, value }, wherevalueis a function returning aDateor{ start, end }.datePresetsbuilds the common ones:today(),yesterday(),tomorrow(),lastDays(n)andlastMonths(n)(ending today),thisMonth()andthisYear()(to today), andlastMonth()andlastYear()(the whole previous one), each with an optional label to translate it. The preset matching the picked dates is highlighted. - Times.
timeadds time fields:true(or'minutes') for hours and minutes,'seconds'to include seconds, andminuteStepsets how far the arrows move. A single date gets a Time field under the calendar, and each end of a range gets one in the From / To row; a Now button next to each sets the current date and time. The field then shows and accepts a time after the date (10/05/2026 2:30 PM), and a typed date without one keeps the current time. Picked days keep their time too, and a range without a time on its end runs to the end of that day (11:59 PM), so presets like "Last 7 days" cover the whole of today. The time fields are native<input type="time">elements, so they use the browser's 12- or 24-hour clock. - From / To. In range mode, From and To fields sit side by side above the calendar when there's no field to type in (
trigger="button") or whentimeis on, each with its time field and Now button. Typing a date there moves the calendar to it. - Confirm. With
confirm, picks wait for Apply; Cancel, Esc or a click outside discards them. It's on by default withtime, withmode="multiple", or for a range with presets, so adjusting a time or trying presets doesn't refetch on every click. - Time slots.
timeOptionsreplaces the time field of a single date with a list of times beside the calendar, for bookings and meetings:{ step, start, end }(minutes between slots, first and latest time asHH:mm) or a list such as['09:00', '13:30'].disabledTimesgets the day with each slot's time and returnstruefor one that's taken. The list appears once a day is picked, shows the times in the locale's clock, and keeps the picked slot when you change days; ↑, ↓, Home and End move through the open slots and Enter picks one. Apply waits for an open slot, and a typed time has to be one. It turnstimeon; a range keeps its time fields. On phones the slots become a row of chips under the calendar.<UiInputDate id="review" v-model="reviewAt" trigger="button" label="Schedule a review" :time-options="{ step: 30, start: '09:00', end: '17:00' }" :disabled-dates="isWeekend" :disabled-times="date => booked.some(slot => slot.getTime() === date.getTime())" /> - Several dates.
mode="multiple"picks any number of days: each click on the calendar adds or removes one, and Apply writes them in order (confirmis on by default; without it, each click updates the value and the popup stays open). The field lists the dates separated by commas and accepts them typed the same way; the button and the footer show one date, or "3 dates". A range preset adds every allowed day in it, sodisabledDatescan leave out weekends.timedoesn't apply.<UiInputDate id="time-off" v-model="daysOff" mode="multiple" label="Out of office" :min="today" :disabled-dates="isWeekend" clearable /> - Button trigger.
trigger="button"replaces the field with an outline button that shows the matching preset ("Last 7 days") or the dates, for toolbars and report headers.labelbecomes part of the button's accessible name. - Format.
formattakesIntl.DateTimeFormatoptions for the shown value, such as{ dateStyle: 'medium' }. Typed dates are still read as numbers. - Calendar options.
min,max,disabledDates,locale,weekStartandmonths(1 for a date, 2 for a range by default) go to the calendar; typed dates outside the limits are rejected. - The popup is a non-modal dialog anchored to the field, like the combobox list: it flips above when there isn't room and sits on top of dialogs. Esc closes it without closing a dialog around it, and focus returns to the field or button.
- On phones (below 600px), the popup becomes a bottom sheet over a dimmed backdrop: the presets turn into a scrolling row of chips, From and To stack, the calendar fills the width like a
fluidone with its months one after another, and the Apply footer stays pinned at the bottom. A tap on the backdrop discards the picks, like a click outside.
labels takes toggle ('Choose date', the calendar button and the popup's name when there's no label), clear ('Clear date'), apply, cancel, presets, now, time, from, to, startDate, endDate, startTime, endTime, the typing hint's dayLetters, monthLetters and yearLetters, dates ("3 dates", a function for the plural), times ('Available times') and pickDay ('Pick a day to see the times') for the time slots, and the calendar's previousMonth, nextMonth and rangeStart. To translate every date field at once, set them app-wide; see Localization.
Checkbox, switch, and radio
<template>
<UiInputCheck v-model="agree" label="I agree to the terms" required />
<UiInputCheck v-model="dark" label="Dark mode" variant="switch" />
<UiInputRadio v-model="plan" :options="plans" option-label="name" option-value="id" inline />
</template>
Range, file, color, and phone
UiInputFile, UiInputColorPicker, and UiInputCheck share the same base props as the other inputs (errorMessages, disabled, required, optionalLabel, hideLabel) and bind with v-model like the rest:
<template>
<UiInputRange v-model="volume" :min="0" :max="100" :step="5" label="Volume" />
<UiInputFile v-model="file" label="Upload" input-label="Choose a file" :error-messages="fileErrors" />
<UiInputPhoneNumber v-model="phone" label="Phone" />
<UiInputColorPicker v-model="color" label="Brand color" :maxlength="7" />
</template>
size="sm" on UiInputRange shrinks the track and the thumb along with the field, for dense forms and toolbars:
<template>
<UiInputRange v-model="zoom" size="sm" label="Zoom" />
</template>
OTP Input
UiInputOtp renders one box per character of a segmented PIN or verification code. It auto-advances focus as boxes fill, supports Backspace/Arrow key navigation, and distributes pasted (or autofilled) codes across boxes. modelValue is always a plain string; length (default 6) controls the number of boxes.
<script setup lang="ts">
import { ref } from 'vue'
const code = ref('')
function handleComplete(value: string) {
console.log('Code complete:', value)
}
</script>
<template>
<UiInputOtp v-model="code" label="Verification code" @complete="handleComplete" />
<UiInputOtp v-model="pin" label="4-digit PIN" :length="4" />
</template>
Props: length (default 6), integerOnly (default true — numeric inputmode, non-digit characters discarded on input/paste; set to false for alphanumeric codes), autofocus, plus the shared base props (errorMessages, disabled, required, readonly, variant, size, rounded, hideLabel).
Events: update:modelValue, update, and complete — fired once with the full value when every box is filled.
Password, search and tags
UiInputPassword, UiInputSearch and UiInputTags take the shared base props, so id, label, errorMessages, variant, size and rounded behave as on the other inputs.
<script setup lang="ts">
import { ref } from 'vue'
const password = ref('')
const query = ref('')
const emails = ref<string[]>([])
</script>
<template>
<UiInputPassword id="password" v-model="password" label="Password" autocomplete="new-password" />
<UiInputSearch id="search" v-model="query" label="Search projects" hide-label placeholder="Search" @search="runSearch" />
<UiInputTags id="emails" v-model="emails" label="Invite by email" placeholder="name@company.com" :max="10" />
</template>
UiInputPasswordshows or hides the password with an eye button inside the field. The button's accessible name staysrevealLabel('Show password') and itsaria-pressedtells whether the password is visible; bindv-model:revealedto control it.autocompletedefaults to'current-password'andmaxlengthto128.UiInputSearchputs a search icon inside the field, and a clear button once it holds text. Enter emitssearchwith the value; the clear button or Esc empties the field and emitsclear. Inside a dialog, the first Esc clears the field and the next one closes the dialog.autocompletedefaults to'off'andmaxlengthto50, as onUiInputText;clearLabelnames the clear button. A disabled or read-only field ignores Esc.UiInputTagskeeps astring[]. Enter or theseparator(default,) adds the typed tag, Backspace on an empty field removes the last one, and pasted lines become one tag each. Duplicates are compared without case unlessallowDuplicatesis set;maxcaps the count andmaxlength(default50) the length of each tag. A duplicate or a tag pastmaxis left out, announced to screen readers and emitted asrejectwith the tag and'duplicate'or'max', so you can show a message. Once the field is full, the text cursor steps aside so no empty row is left, and Backspace still removes the last tag. It emitsaddandremovewith the tag, and announces both.
Prefix and suffix
UiInputText exposes #prefix and #suffix named slots. By default, content in either slot gets a bordered box attached to the input — useful for units. Set adornments="inline" to place it inside the field instead, for icons and icon buttons:
<template>
<UiInputText v-model="search" label="Search">
<template #prefix>
<UiIconMaterial icon-code="" />
</template>
</UiInputText>
<UiInputText v-model="price" label="Price" type="number">
<template #prefix>
$
</template>
<template #suffix>
USD
</template>
</UiInputText>
<UiInputText v-model="site" label="Website" adornments="inline">
<template #prefix>
<UiIconMaterial icon-code="" />
</template>
</UiInputText>
</template>
Shared base props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | null | — | Id of the field; the label and error messages link to it. |
label | string | null | — | Field label. |
placeholder | string | null | — | Placeholder text. |
errorMessages | string[] | — | Validation messages shown below the field. |
variant | 'filled' | 'outline' | 'transparent' | null | — | Visual style. |
size | 'sm' | 'lg' | null | — | Field size. |
disabled / required / readonly / rounded | boolean | false | Field states. |
hideLabel | boolean | false | Visually hide the label. |
optionalLabel | boolean | false | Show "Optional" under the field (hidden while an error shows). |
customClass | string | null | — | Extra classes on the field element. |
Per-input highlights
| Component | Notable props | v-model type |
|---|---|---|
UiInputText | type, maxlength, min, max, autofocus, autocomplete, adornments | string | number | null |
UiInputPassword | revealLabel, v-model:revealed, autocomplete | string | null |
UiInputSearch | clearLabel; emits search, clear | string | null |
UiInputTags | max, maxlength, separator, allowDuplicates, removeLabel; emits add, remove, reject | string[] |
UiInputTextarea | rows, cols, resize | string | null |
UiInputSelect | options, optionLabel, optionValue | string | number | object | null |
UiInputCombobox | options, optionLabel, optionValue, optionGroup, optionDisabled, filterable, clearable, remote, loading, searchDelay, minSearchLength, freeText, emptyText, clearLabel, toggleLabel; emits search | string | number | object | null |
UiInputMultiSelect | The combobox props, plus max, maxChips, maxChipsLabel, removeLabel; emits add, remove, search | (string | number | object)[] |
UiInputDate | mode, trigger, presets, time, minuteStep, timeOptions, disabledTimes, confirm, months, min, max, disabledDates, locale, weekStart, format, clearable, labels | Date | IDateRange | Date[] | null |
UiInputRadio | options, optionLabel, optionValue, inline | string | number | null |
UiInputRange | min, max, step | string | number | null |
UiInputCheck | variant: 'switch', label (required), type (native input type, default 'checkbox') | string | boolean | null |
UiInputFile | inputLabel, size: 'lg' (taller dropbox) | File | null |
UiInputColorPicker | maxlength | string | null |
UiInputPhoneNumber | maxlength, autofocus | string | null |
UiInputOtp | length, integerOnly, autofocus | string |
update:modelValue (for v-model) and an update convenience event with the same payload.CSS variables
Set these on :root, or on any wrapper to restyle only the fields inside it. They style text inputs, textareas and selects, and the color picker's field reads the same surface, border, radius, shadow, focus and invalid variables; its swatch rounds to half the field radius. The outline, rounded, transparent, sm and lg styles keep their own values, and disabled fields keep their muted look.
:root {
--cffy-input-radius: var(--cffy-radius-full);
--cffy-input-padding-inline: var(--cffy-space-14);
--cffy-input-focus-border-color: var(--cffy-accent-a10);
}
| Variable | Default | Controls |
|---|---|---|
--cffy-input-height | --cffy-field-height-md (2.5rem) | Field height; sm and lg use --cffy-field-height-sm / -lg, which follow the density |
--cffy-input-padding-inline / --cffy-input-padding-block | --cffy-space-8 / --cffy-space-4 | Field padding |
--cffy-input-radius | --cffy-shape-field (8px) | Corners |
--cffy-input-bg-color / --cffy-input-color | transparent / --cffy-on-background | Background and text |
--cffy-input-border-width / --cffy-input-border-color | 1px / --cffy-outline-text-field | Border |
--cffy-input-font-size | 0.875rem | Text size |
--cffy-input-shadow | --cffy-shadow-xs | Resting shadow |
--cffy-input-placeholder-color | --cffy-muted-a10 at 65% | Placeholder text |
--cffy-input-filled-bg-color | --cffy-surface-container-high | Background of .form-filled fields |
--cffy-input-hover-border-color | --cffy-on-background | Hovered border |
--cffy-input-focus-bg-color / --cffy-input-focus-border-color | --cffy-surface-container-high / --cffy-primary-a10 | Focused field |
--cffy-input-focus-ring-color | --cffy-primary-base-fixed at 25% | Focus ring |
--cffy-input-invalid-color / --cffy-input-invalid-ring-color | --cffy-danger-a10 / --cffy-danger-base-fixed at 25% | Invalid border and message, and its focus ring |
--cffy-input-easing / --cffy-input-duration | --cffy-ease-emphasized / --cffy-duration-300 | Background, border and ring transitions |
--cffy-input-label-font-size / --cffy-input-label-font-weight | --cffy-fs-3xs / 600 | Field label |
--cffy-input-label-gap | --cffy-space-8 | Space between the label and the field |
--cffy-form-group-spacing | --cffy-space-14 | Space below each field group |
Range CSS variables
UiInputRange (.form-range) reads these. The sm size keeps its own track and thumb size.
| Variable | Default | Controls |
|---|---|---|
--cffy-input-range-track-color / --cffy-input-range-fill-color | --cffy-surface-container-high / primary tint | Empty track, and the filled track and thumb |
--cffy-input-range-track-size / --cffy-input-range-radius | 0.75rem / --cffy-radius-full | Track thickness and corners (the thumb shares the radius) |
--cffy-input-range-thumb-height / --cffy-input-range-thumb-width | 1.95rem / 0.3rem | Thumb size (it narrows while focused) |
--cffy-input-range-thumb-ring-color | --cffy-surface-body-pane | Ring that separates the thumb from the track |
--cffy-input-range-easing / --cffy-input-range-duration | --cffy-ease-decelerate / --cffy-duration-100 | Thumb width transition |
File dropbox CSS variables
UiInputFile (.input-file-dropbox) reads these. The large dropbox keeps its own height, and a selected file its success colors. Hover and pressed backgrounds come from the shared state layer.
| Variable | Default | Controls |
|---|---|---|
--cffy-input-file-bg-color / --cffy-input-file-color | --cffy-surface-body-pane / --cffy-on-background | Background, and the icon and text |
--cffy-input-file-border-width / --cffy-input-file-border-color | --cffy-border-width-md / --cffy-outline-text-field | Dashed border |
--cffy-input-file-radius / --cffy-input-file-height | --cffy-shape-field, up to --cffy-shape-container / 5rem | Corners and height |
--cffy-input-file-font-size | --cffy-fs-3xs | Text size |
--cffy-input-file-easing / --cffy-input-file-duration | --cffy-ease-decelerate / --cffy-duration-100 | State transitions |
Checkbox, switch and radio CSS variables
UiInputCheck and UiInputRadio (.form-check) read these. Switches follow --cffy-input-check-checked-color unless --cffy-input-switch-checked-color is set. The sm and lg sizes keep their own box size and gap, and the danger checkbox its color.
:root {
--cffy-input-check-checked-color: var(--cffy-accent-a10);
--cffy-input-check-radius: var(--cffy-radius-sm);
}
| Variable | Default | Controls |
|---|---|---|
--cffy-input-check-size | 1.25rem | Box and radio size (switches are twice as wide) |
--cffy-input-check-gap / --cffy-input-check-spacing | --cffy-space-12 / --cffy-space-14 | Space between the box and its label, and below each option |
--cffy-input-check-bg-color / --cffy-input-check-border-color | transparent / --cffy-outline-text-field | Unchecked box |
--cffy-input-check-border-width | --cffy-border-width-md | Box, radio and switch border |
--cffy-input-check-checked-color | --cffy-primary-a10 | Checked box, radio and switch |
--cffy-input-check-radius | Half of --cffy-shape-field (4px by default), up to --cffy-radius-sm (6px) | Checkbox corners |
--cffy-input-check-easing / --cffy-input-check-duration | --cffy-ease-emphasized / --cffy-duration-300 | State transitions |
--cffy-input-switch-bg-color / --cffy-input-switch-border-color | --cffy-surface-container / --cffy-outline-surface | Switch track when off |
--cffy-input-switch-thumb-color / --cffy-input-switch-checked-thumb-color | --cffy-muted-base / --cffy-surface-container-highest | Switch thumb, off and on |
--cffy-input-switch-checked-color | --cffy-input-check-checked-color | Switch track when on |
Prefix and suffix CSS variables
The prefix and suffix adornments (.input-group-prefix, .input-group-suffix) follow the field's --cffy-input-radius, --cffy-input-border-width, --cffy-input-border-color and --cffy-input-bg-color, so they stay joined to it. These override the rest:
| Variable | Default | Controls |
|---|---|---|
--cffy-input-group-bg-color | --cffy-input-bg-color | Adornment background |
--cffy-input-group-color | --cffy-muted-a10 | Adornment text and icon |
--cffy-input-group-padding-inline | 0.5rem | Adornment padding |
--cffy-input-group-font-size / --cffy-input-group-icon-size | --cffy-fs-2xs / --cffy-fs-base | Adornment text and icon size |
With .input-group-inline (adornments="inline"), the adornments sit inside the field as squares the height of the input, and the input pads itself to clear them. A second .input-group-suffix sits next to the first, as the combobox's clear and toggle buttons do. The input stays the bordered element, so focus, invalid, variants and sizes are unchanged.
Tags CSS variables
UiInputTags (.form-tags) is a field-styled wrapper around the tags and a borderless .form-tags-input: it reads the same --cffy-input-* variables as a text field, grows in height as tags wrap, and shows focus with :focus-within. The tags are compact .btn-chip.chip-closable chips sized to the field, with their corners nested inside the field's (--cffy-input-radius minus the border and padding), so a rounded field gets pill-shaped tags. Once it holds a tag, the field uses its block padding on the start edge too, so tags sit the same distance from every edge; an empty field keeps the text padding.
| Variable | Default | Controls |
|---|---|---|
--cffy-input-tags-gap | --cffy-space-4 | Space between tags and the text cursor |
Combobox CSS variables
The combobox field is a text input and the multi-select field a tags field (.form-tags.multiselect), so both read the --cffy-input-* variables. The list (.listbox-popup, with .listbox-option rows) reads these; the highlighted row comes from the shared state layer.
| Variable | Default | Controls |
|---|---|---|
--cffy-listbox-bg-color / --cffy-listbox-color | --cffy-surface-overlay / --cffy-on-background | List surface and text |
--cffy-listbox-border-color / --cffy-listbox-radius / --cffy-listbox-shadow | --cffy-outline-overlay / --cffy-shape-container / --cffy-shadow-lg | List frame |
--cffy-listbox-padding | --cffy-space-4 | Space around the options |
--cffy-listbox-max-height | 18rem | Height before the list scrolls |
--cffy-listbox-offset | --cffy-space-4 | Gap between the field and the list |
--cffy-listbox-option-padding-block / --cffy-listbox-option-padding-inline | --cffy-space-8 / --cffy-space-8 | Option padding |
--cffy-listbox-option-radius / --cffy-listbox-option-font-size | half the list radius / --cffy-fs-2xs | Option corners and text |
--cffy-listbox-selected-bg-color / --cffy-listbox-selected-color | --cffy-surface-container-highest / --cffy-on-background | The selected option |
--cffy-listbox-group-color | --cffy-muted-a10 | Group headings |
--cffy-listbox-create-color | --cffy-primary-a10 | The multi-select's "Add “…”" row |
--cffy-listbox-easing / --cffy-listbox-duration | --cffy-ease-decelerate / --cffy-duration-150 | Opening fade |
Date CSS variables
The date field reads the --cffy-input-* variables and the calendar its --cffy-calendar-* ones; the picked time slot (.calendar-time) takes the calendar's selected colors. The popup (.calendar-popup, with .calendar-preset buttons) reads these:
| Variable | Default | Controls |
|---|---|---|
--cffy-calendar-popup-bg-color / --cffy-calendar-popup-color | --cffy-surface-overlay / --cffy-on-background | Popup surface and text |
--cffy-calendar-popup-border-color / --cffy-calendar-popup-radius / --cffy-calendar-popup-shadow | --cffy-outline-overlay / --cffy-shape-container / --cffy-shadow-lg | Popup frame; the border color also divides the presets and the footer |
--cffy-calendar-popup-padding | --cffy-space-12 | Space inside the popup |
--cffy-calendar-popup-offset | --cffy-space-4 | Gap between the field and the popup |
--cffy-calendar-popup-easing / --cffy-calendar-popup-duration | --cffy-ease-decelerate / --cffy-duration-150 | Opening fade |
--cffy-calendar-preset-selected-bg-color | --cffy-surface-container-highest | The preset matching the picked dates |
OTP CSS variables
UiInputOtp (.form-otp) boxes are square text fields: they take their size from --cffy-input-height and their surface, border and focus from the other --cffy-input-* variables. These set the rest:
| Variable | Default | Controls |
|---|---|---|
--cffy-input-otp-gap | 0.5rem | Space between boxes |
--cffy-input-otp-font-size / --cffy-input-otp-font-weight | --cffy-fs-base / --cffy-fw-500 | Digit text |