Keyboard shortcut: Ctrl + K
Get started

Input

A complete text field: label above, hint below, icons inside, a character counter, a clear button and a loading state, all around a real <input>.

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VInput } from 'vectis-ui'

const email = ref('')
</script>

<template>
  <VInput v-model="email" label="Email" placeholder="[email protected]" />
</template>

Examples

Label and hint

label renders above the field and focuses it when clicked. hint renders under the field and is tied to it through aria-describedby.

vue

Shown next to everything you publish.

<script setup lang="ts">
import { ref } from 'vue'
import { VInput } from 'vectis-ui'

const name = ref('')
</script>

<template>
  <VInput
    v-model="name"
    label="Display name"
    hint="Shown next to everything you publish."
    placeholder="Ada Lovelace"
  />
</template>

Sizes

size sets the height to 32, 40 or 48 pixels, and compact takes 4px off any of them.

vue
<script setup lang="ts">
import { VInput } from 'vectis-ui'
</script>

<template>
  <div class="grid">
    <VInput size="sm" label="Small" placeholder="32px" />
    <VInput size="sm" compact label="Small compact" placeholder="28px" />
    <VInput size="md" label="Medium" placeholder="40px" />
    <VInput size="md" compact label="Medium compact" placeholder="36px" />
    <VInput size="lg" label="Large" placeholder="48px" />
    <VInput size="lg" compact label="Large compact" placeholder="44px" />
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--vectis-space-4);
}
</style>

Icons

iconStart and iconEnd place a decorative icon at either end of the field. The #end slot replaces the end icon, where #start is rendered after the start icon rather than in its place.

vue

This handle is free.

<script setup lang="ts">
import { ref } from 'vue'
import { VInput } from 'vectis-ui'
import { check_circle as checkCircle, search } from 'vectis-ui/icons'

const query = ref('')
const handle = ref('ada')
</script>

<template>
  <div class="fields">
    <VInput
      v-model="query"
      label="Search"
      :icon-start="search"
      placeholder="Orders, people, files"
    />
    <VInput v-model="handle" label="Handle" :icon-end="checkCircle" hint="This handle is free." />
  </div>
</template>

<style scoped>
.fields {
  display: grid;
  gap: var(--vectis-space-4);
}
</style>

Clearable

clearable adds a cross that empties the field, shown while there is something to clear and the field can be edited. clearVisible answers that question yourself, for a field whose value is not its text.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VInput } from 'vectis-ui'
import { search } from 'vectis-ui/icons'

const query = ref('wool socks')
</script>

<template>
  <VInput v-model="query" label="Search" :icon-start="search" clearable />
</template>

States

disabled greys the field out and takes it out of the tab order. readonly keeps it focusable and copyable, and hides the clear cross unless told otherwise. invalid is for a rule the browser cannot check by itself, and loading puts a spinner where the end icon goes.

vue

That handle is already taken.

Loading…
<script setup lang="ts">
import { ref } from 'vue'
import { VInput } from 'vectis-ui'

const handle = ref('ada')
const query = ref('metro')
</script>

<template>
  <div class="fields">
    <VInput label="Disabled" model-value="Not editable" disabled />
    <VInput label="Read-only" model-value="[email protected]" readonly />
    <VInput v-model="handle" label="Invalid" invalid hint="That handle is already taken." />
    <VInput v-model="query" label="Loading" loading />
  </div>
</template>

<style scoped>
.fields {
  display: grid;
  gap: var(--vectis-space-4);
}
</style>

Clickable icons

A @click:icon-start or @click:icon-end listener turns that icon into a real button, which then needs iconStartLabel or iconEndLabel.

vue

Press the magnifier to search.

<script setup lang="ts">
import { ref } from 'vue'
import { VInput } from 'vectis-ui'
import { search, swap_vert as swapVert } from 'vectis-ui/icons'

const query = ref('wool socks')
const searched = ref('')

const amount = ref('120.00')
const currency = ref<'EUR' | 'USD'>('EUR')

function switchCurrency() {
  currency.value = currency.value === 'EUR' ? 'USD' : 'EUR'
}
</script>

<template>
  <div class="fields">
    <VInput
      v-model="query"
      label="Search"
      :icon-end="search"
      icon-end-label="Run the search"
      :hint="searched ? `Searched for ${searched}.` : 'Press the magnifier to search.'"
      @click:icon-end="searched = query"
    />

    <VInput
      v-model="amount"
      :label="`Amount in ${currency}`"
      :icon-start="swapVert"
      icon-start-label="Switch currency"
      @click:icon-start="switchCurrency"
    />
  </div>
</template>

<style scoped>
.fields {
  display: grid;
  gap: var(--vectis-space-4);
}
</style>

Counters

counter shows the length at the end of the field. maxlength is the browser's hard limit, where softLimit is a line the reader may cross: the text is never cut, the counter turns red and the field goes into error through the native validity. The prop counts characters, as on VTextarea; on VFileInput the same name counts files and their size.

vue
0/20

The browser refuses the twenty-first character.

42/40

Type past forty and the field goes into error instead.

<script setup lang="ts">
import { ref } from 'vue'
import { VInput } from 'vectis-ui'

const nickname = ref('')
const headline = ref('A design system that leans on the platform')
</script>

<template>
  <div class="fields">
    <VInput
      v-model="nickname"
      label="Nickname"
      hint="The browser refuses the twenty-first character."
      counter
      :maxlength="20"
    />

    <VInput
      v-model="headline"
      label="Headline"
      hint="Type past forty and the field goes into error instead."
      counter
      :maxlength="40"
      soft-limit
    />
  </div>
</template>

<style scoped>
.fields {
  display: grid;
  gap: var(--vectis-space-4);
}
</style>

Pattern

There is no pattern prop: the native attribute reaches the input through fallthrough, along with inputmode, name and everything else a form needs. The field turns red through :user-invalid, once the reader has left it.

vue

Five digits.

<script setup lang="ts">
import { ref } from 'vue'
import { VInput } from 'vectis-ui'

const postcode = ref('')
</script>

<template>
  <VInput
    v-model="postcode"
    label="Postcode"
    hint="Five digits."
    pattern="[0-9]{5}"
    inputmode="numeric"
    title="Five digits, 75001 for instance."
  />
</template>

API

Props

PropTypeDefault
sizeInputSize'sm' | 'md' | 'lg''md'
The height of the field: 32, 40 or 48 pixels.
compactbooleanfalse
Takes 4px off the height, leaving the padding, the text and the icons as they are.
typeInputType'text' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'url''text'
The native type of the input, which is also what tells a phone which keyboard to offer: a numeric pad for number, an @ key for email.
invalidbooleanfalse
Marks the field as invalid whatever the browser thinks. This is the route for a rule only the server can check; anything the browser can validate on its own already colours the field without it.
disabledbooleanfalse
Makes the field unusable, greyed out through the colour tokens.
readonlybooleanfalse
Shows the value without allowing it to be changed. The field can still be focused and copied from, and it hides the clear button unless clearVisible answers that question explicitly.
noTypingbooleanfalse
Refuses the keyboard without drawing the field as read-only: the native attribute is set, but the field keeps its ordinary look and its clear cross. For a field whose value comes from somewhere else, a picker or a file dialog, and which is no less editable for it.
labelstringnone
The label above the field, tied to it so that clicking it focuses the field.
hintstringnone
A line of help under the field. It is tied to the input for assistive technology, so it is read out along with the label.
iconStartIconSourcenone
An icon inside the field, at the start. It is decorative until a @click:icon-start listener is attached, at which point it becomes a real button and needs iconStartLabel.
iconEndIconSourcenone
The same at the end of the field. The #end slot replaces it, and the loading spinner takes its place while it turns.
iconStartLabelstringnone
What the start icon does, in words, once it is clickable.
iconEndLabelstringnone
What the end icon does, in words, once it is clickable.
loadingbooleanfalse
Shows a spinner at the end of the field, in place of the end icon or slot.
loadingTextstringnone
What screen readers announce while the spinner turns. It falls back to the design system dictionary.
clearablebooleanfalse
Offers a cross that empties the field. It appears when there is something to clear and the field can be edited.
clearVisiblebooleannone
Decides whether the cross is shown, instead of letting the field work it out from its own content. It exists for the components built on this one, where what there is to clear is not the text: VCombobox holds its selection as chips beside the field, and a read-only date or time picker changes its value through a panel rather than by typing.
clearLabelstringnone
What the clear button does, in words. It falls back to the design system dictionary.
maxlengthnumbernone
The maximum number of characters. By default this is the browser's own limit, which simply refuses anything beyond it.
softLimitbooleanfalse
Turns that limit into a soft one: the reader may type past it, and the field goes into error instead of silently refusing the keystrokes. It is reported through the native validity, so a form cannot be submitted over the limit.
counterbooleanfalse
Shows how much has been typed, at the end of the field: 12/80 against a limit, or just 12 without one.
v-modelstring | number''
The value, typed as text or a number rather than text alone. On an <input type="number"> Vue converts the value to a number by itself, so a string-only model would hand a number back to a consumer who passed a string in.

Events

EventType
click:icon-start[event: MouseEvent]
The start icon was pressed. Attaching this listener is what turns it into a button.
click:icon-end[event: MouseEvent]
The end icon was pressed. Attaching this listener is what turns it into a button.
clear[]
The clear button was pressed. The value has already been emptied.

Slots

SlotType
start{}
Content at the start of the field, rendered after iconStart rather than in its place.
value-end{}
Controls of your own inside the field, placed before the field's own: the clear cross and the end icon. It is where something that acts on the value belongs, so that the reading order and the tab order agree.
end{}
Content at the end of the field, which replaces iconEnd. It is hidden while the field is loading, the spinner taking that place.

Types

The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender