Float Input
A field with one label doing two jobs. At rest the label sits inside the input and acts as the placeholder; on focus, or as soon as there is a value, it rises into the top border, shrinks and takes the accent colour. Nothing jumps: the label keeps its position in the DOM, so the input never reflows and the caret never moves.
The floating position is absolute, driven by a y transform instead of a class
swap. That keeps the transition interruptible, which matters when a user tabs
through a form quickly: a half-floated label reverses from wherever it is
rather than snapping back to the start.
Usage
The value is controlled with v-model. defaultValue covers the uncontrolled
case, and the floating state is derived from the value plus focus, so a field
prefilled from a server renders with the label already up.
<template>
<div class="flex w-full max-w-sm flex-col gap-6 py-6">
<MFloatInput
v-model="email"
icon="i-lucide-mail"
label="Email address"
placeholder="you@example.com"
/>
<MFloatInput
v-model="name"
label="Full name"
placeholder="Ada Lovelace"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const email = ref('')
const name = ref('Ada Lovelace')
</script>
icon accepts any Iconify name and is rendered inside the field; the slot of
the same name lets you pass an SVG or another component instead.
Customize
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | - | Controlled value. Leave it out to let the component keep its own. |
defaultValue | string | '' | Starting value in uncontrolled usage. |
label | string | - | Visible label, also used as the accessible name. Required. |
placeholder | string | '' | Placeholder shown only after the label has floated out of the field. |
icon | string | - | Iconify name rendered at the start of the field. |
disabled | boolean | false | Disable the input and fade it out. |
inputClass | string | - | Extra classes for the input element. |
labelClass | string | - | Extra classes for the floating label. |
class | string | - | Extra classes for the wrapper. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | (value: string) => void | Fired on every keystroke with the raw input value. Pair it with modelValue
for controlled usage. |
Slots
| Slot | Props | Description |
|---|---|---|
icon | - | Replaces the default icon render. The wrapper is aria-hidden, so the slot
should stay decorative; the label carries the meaning. |
Accessibility
- The label is a real
<label>with aforpointing at the input's generatedid, so clicking it focuses the field and screen readers announce it once. - The input also carries
aria-label, which keeps the accessible name intact if the label is restyled or moved outside the visual flow. - The placeholder is empty until the label floats, so the visible text is never duplicated by a placeholder at the same time.
- Focus is shown with a two-pixel ring offset from the border, not by the label colour alone.
- The floating transform is purely visual and does not affect layout, focus or hit testing.
- Under
prefers-reduced-motionthe label changes position instantly instead of animating.
Performance
- The field keeps no animation loop and no timers; motion only runs while the label transitions.
- Only
transform,colorandscaleare animated, all of which the compositor can handle without layout. - The wrapper is a single flex container with the label absolutely positioned, so showing or hiding the icon does not cause a reflow of the input.
inputClassandlabelClassare plain class merges; no styles are generated per instance.