Components

Inputs

Collect user input with text, select, combobox, multi-select, checkbox, radio, range, file, and color 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. optionGroup names the field that groups options under headings, in the order each group first appears, and optionDisabled the field that disables one. Without optionValue, the model holds the whole option and object options match by reference. The #option slot ({ option, selected, active }) renders each row, and #empty ({ query }) replaces emptyText ('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. freeText lets 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 without remote.

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. Set maxChips to keep the field to one row: past that many values, the chips give way to the maxChipsLabel summary ('{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. max caps the number of values; once it's reached, the other options are disabled until one is removed.
  • Events. Besides update:modelValue and update, it emits add and remove with the value. Screen readers hear each change, and the field's description lists what's selected.
  • Search on the server. remote, loading, searchDelay, minSearchLength and the search event work as on the combobox. Chips keep their labels across searches.
  • Free text. freeText ends 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/2026 in en-US, 05/10/2026 in es-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); pass placeholder to 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. presets lists shortcuts next to the calendar. Each is { label, value }, where value is a function returning a Date or { start, end }. datePresets builds the common ones: today(), yesterday(), tomorrow(), lastDays(n) and lastMonths(n) (ending today), thisMonth() and thisYear() (to today), and lastMonth() and lastYear() (the whole previous one), each with an optional label to translate it. The preset matching the picked dates is highlighted.
  • Times. time adds time fields: true (or 'minutes') for hours and minutes, 'seconds' to include seconds, and minuteStep sets 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 when time is 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 with time, with mode="multiple", or for a range with presets, so adjusting a time or trying presets doesn't refetch on every click.
  • Time slots. timeOptions replaces 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 as HH:mm) or a list such as ['09:00', '13:30']. disabledTimes gets the day with each slot's time and returns true for 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 turns time on; 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 (confirm is 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, so disabledDates can leave out weekends. time doesn'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. label becomes part of the button's accessible name.
  • Format. format takes Intl.DateTimeFormat options for the shown value, such as { dateStyle: 'medium' }. Typed dates are still read as numbers.
  • Calendar options. min, max, disabledDates, locale, weekStart and months (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 fluid one 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>
  • UiInputPassword shows or hides the password with an eye button inside the field. The button's accessible name stays revealLabel ('Show password') and its aria-pressed tells whether the password is visible; bind v-model:revealed to control it. autocomplete defaults to 'current-password' and maxlength to 128.
  • UiInputSearch puts a search icon inside the field, and a clear button once it holds text. Enter emits search with the value; the clear button or Esc empties the field and emits clear. Inside a dialog, the first Esc clears the field and the next one closes the dialog. autocomplete defaults to 'off' and maxlength to 50, as on UiInputText; clearLabel names the clear button. A disabled or read-only field ignores Esc.
  • UiInputTags keeps a string[]. Enter or the separator (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 unless allowDuplicates is set; max caps the count and maxlength (default 50) the length of each tag. A duplicate or a tag past max is left out, announced to screen readers and emitted as reject with 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 emits add and remove with 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="&#xe8b6;" />
    </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="&#xe894;" />
    </template>
  </UiInputText>
</template>

Shared base props

PropTypeDefaultDescription
idstring | null—Id of the field; the label and error messages link to it.
labelstring | null—Field label.
placeholderstring | null—Placeholder text.
errorMessagesstring[]—Validation messages shown below the field.
variant'filled' | 'outline' | 'transparent' | null—Visual style.
size'sm' | 'lg' | null—Field size.
disabled / required / readonly / roundedbooleanfalseField states.
hideLabelbooleanfalseVisually hide the label.
optionalLabelbooleanfalseShow "Optional" under the field (hidden while an error shows).
customClassstring | null—Extra classes on the field element.

Per-input highlights

ComponentNotable propsv-model type
UiInputTexttype, maxlength, min, max, autofocus, autocomplete, adornmentsstring | number | null
UiInputPasswordrevealLabel, v-model:revealed, autocompletestring | null
UiInputSearchclearLabel; emits search, clearstring | null
UiInputTagsmax, maxlength, separator, allowDuplicates, removeLabel; emits add, remove, rejectstring[]
UiInputTextarearows, cols, resizestring | null
UiInputSelectoptions, optionLabel, optionValuestring | number | object | null
UiInputComboboxoptions, optionLabel, optionValue, optionGroup, optionDisabled, filterable, clearable, remote, loading, searchDelay, minSearchLength, freeText, emptyText, clearLabel, toggleLabel; emits searchstring | number | object | null
UiInputMultiSelectThe combobox props, plus max, maxChips, maxChipsLabel, removeLabel; emits add, remove, search(string | number | object)[]
UiInputDatemode, trigger, presets, time, minuteStep, timeOptions, disabledTimes, confirm, months, min, max, disabledDates, locale, weekStart, format, clearable, labelsDate | IDateRange | Date[] | null
UiInputRadiooptions, optionLabel, optionValue, inlinestring | number | null
UiInputRangemin, max, stepstring | number | null
UiInputCheckvariant: 'switch', label (required), type (native input type, default 'checkbox')string | boolean | null
UiInputFileinputLabel, size: 'lg' (taller dropbox)File | null
UiInputColorPickermaxlengthstring | null
UiInputPhoneNumbermaxlength, autofocusstring | null
UiInputOtplength, integerOnly, autofocusstring
Every input emits both 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);
}
VariableDefaultControls
--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-4Field padding
--cffy-input-radius--cffy-shape-field (8px)Corners
--cffy-input-bg-color / --cffy-input-colortransparent / --cffy-on-backgroundBackground and text
--cffy-input-border-width / --cffy-input-border-color1px / --cffy-outline-text-fieldBorder
--cffy-input-font-size0.875remText size
--cffy-input-shadow--cffy-shadow-xsResting shadow
--cffy-input-placeholder-color--cffy-muted-a10 at 65%Placeholder text
--cffy-input-filled-bg-color--cffy-surface-container-highBackground of .form-filled fields
--cffy-input-hover-border-color--cffy-on-backgroundHovered border
--cffy-input-focus-bg-color / --cffy-input-focus-border-color--cffy-surface-container-high / --cffy-primary-a10Focused 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-300Background, border and ring transitions
--cffy-input-label-font-size / --cffy-input-label-font-weight--cffy-fs-3xs / 600Field label
--cffy-input-label-gap--cffy-space-8Space between the label and the field
--cffy-form-group-spacing--cffy-space-14Space below each field group

Range CSS variables

UiInputRange (.form-range) reads these. The sm size keeps its own track and thumb size.

VariableDefaultControls
--cffy-input-range-track-color / --cffy-input-range-fill-color--cffy-surface-container-high / primary tintEmpty track, and the filled track and thumb
--cffy-input-range-track-size / --cffy-input-range-radius0.75rem / --cffy-radius-fullTrack thickness and corners (the thumb shares the radius)
--cffy-input-range-thumb-height / --cffy-input-range-thumb-width1.95rem / 0.3remThumb size (it narrows while focused)
--cffy-input-range-thumb-ring-color--cffy-surface-body-paneRing that separates the thumb from the track
--cffy-input-range-easing / --cffy-input-range-duration--cffy-ease-decelerate / --cffy-duration-100Thumb 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.

VariableDefaultControls
--cffy-input-file-bg-color / --cffy-input-file-color--cffy-surface-body-pane / --cffy-on-backgroundBackground, and the icon and text
--cffy-input-file-border-width / --cffy-input-file-border-color--cffy-border-width-md / --cffy-outline-text-fieldDashed border
--cffy-input-file-radius / --cffy-input-file-height--cffy-shape-field, up to --cffy-shape-container / 5remCorners and height
--cffy-input-file-font-size--cffy-fs-3xsText size
--cffy-input-file-easing / --cffy-input-file-duration--cffy-ease-decelerate / --cffy-duration-100State 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);
}
VariableDefaultControls
--cffy-input-check-size1.25remBox and radio size (switches are twice as wide)
--cffy-input-check-gap / --cffy-input-check-spacing--cffy-space-12 / --cffy-space-14Space between the box and its label, and below each option
--cffy-input-check-bg-color / --cffy-input-check-border-colortransparent / --cffy-outline-text-fieldUnchecked box
--cffy-input-check-border-width--cffy-border-width-mdBox, radio and switch border
--cffy-input-check-checked-color--cffy-primary-a10Checked box, radio and switch
--cffy-input-check-radiusHalf 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-300State transitions
--cffy-input-switch-bg-color / --cffy-input-switch-border-color--cffy-surface-container / --cffy-outline-surfaceSwitch track when off
--cffy-input-switch-thumb-color / --cffy-input-switch-checked-thumb-color--cffy-muted-base / --cffy-surface-container-highestSwitch thumb, off and on
--cffy-input-switch-checked-color--cffy-input-check-checked-colorSwitch 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:

VariableDefaultControls
--cffy-input-group-bg-color--cffy-input-bg-colorAdornment background
--cffy-input-group-color--cffy-muted-a10Adornment text and icon
--cffy-input-group-padding-inline0.5remAdornment padding
--cffy-input-group-font-size / --cffy-input-group-icon-size--cffy-fs-2xs / --cffy-fs-baseAdornment 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.

VariableDefaultControls
--cffy-input-tags-gap--cffy-space-4Space 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.

VariableDefaultControls
--cffy-listbox-bg-color / --cffy-listbox-color--cffy-surface-overlay / --cffy-on-backgroundList surface and text
--cffy-listbox-border-color / --cffy-listbox-radius / --cffy-listbox-shadow--cffy-outline-overlay / --cffy-shape-container / --cffy-shadow-lgList frame
--cffy-listbox-padding--cffy-space-4Space around the options
--cffy-listbox-max-height18remHeight before the list scrolls
--cffy-listbox-offset--cffy-space-4Gap between the field and the list
--cffy-listbox-option-padding-block / --cffy-listbox-option-padding-inline--cffy-space-8 / --cffy-space-8Option padding
--cffy-listbox-option-radius / --cffy-listbox-option-font-sizehalf the list radius / --cffy-fs-2xsOption corners and text
--cffy-listbox-selected-bg-color / --cffy-listbox-selected-color--cffy-surface-container-highest / --cffy-on-backgroundThe selected option
--cffy-listbox-group-color--cffy-muted-a10Group headings
--cffy-listbox-create-color--cffy-primary-a10The multi-select's "Add “…”" row
--cffy-listbox-easing / --cffy-listbox-duration--cffy-ease-decelerate / --cffy-duration-150Opening 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:

VariableDefaultControls
--cffy-calendar-popup-bg-color / --cffy-calendar-popup-color--cffy-surface-overlay / --cffy-on-backgroundPopup surface and text
--cffy-calendar-popup-border-color / --cffy-calendar-popup-radius / --cffy-calendar-popup-shadow--cffy-outline-overlay / --cffy-shape-container / --cffy-shadow-lgPopup frame; the border color also divides the presets and the footer
--cffy-calendar-popup-padding--cffy-space-12Space inside the popup
--cffy-calendar-popup-offset--cffy-space-4Gap between the field and the popup
--cffy-calendar-popup-easing / --cffy-calendar-popup-duration--cffy-ease-decelerate / --cffy-duration-150Opening fade
--cffy-calendar-preset-selected-bg-color--cffy-surface-container-highestThe 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:

VariableDefaultControls
--cffy-input-otp-gap0.5remSpace between boxes
--cffy-input-otp-font-size / --cffy-input-otp-font-weight--cffy-fs-base / --cffy-fw-500Digit text
Copyright © 2026