Kontroller
Floating input
Et tekstfelt, hvor etiketten svæver op ved fokus, med hjælpetekst og fejltilstand. Kun CSS.
Tilpasset fra Watermelon UI (MIT) og skrevet om med tastatur, etiketter og reduceret bevægelse.
Forhåndsvisning
Brug
floating-input-demo.tsx
import { useState } from 'react'
import { FloatingInput } from '@/components/floating-input'
export default function FloatingInputDemo() {
const [zip, setZip] = useState('')
const bad = zip !== '' && !/^\d{4,5}$/.test(zip)
return (
<form className="mx-auto flex w-full max-w-sm flex-col gap-4" onSubmit={(e) => e.preventDefault()}>
<FloatingInput label="Full name" autoComplete="name" />
<FloatingInput label="Email" type="email" autoComplete="email" hint="We send the receipt here." />
<FloatingInput
label="Postcode"
inputMode="numeric"
autoComplete="postal-code"
value={zip}
onChange={(e) => setZip(e.target.value)}
error={bad ? 'Use 4 or 5 digits.' : undefined}
/>
</form>
)
}Kilde
floating-input.tsx
// Based on Watermelon UI's “Floating input” (MIT, © 2026 Watermelon Platform Contributors,
// github.com/WatermelonCorp/watermelon-platform). Rewritten in CSS only, with a linked label, hint and error.
import { useId, type InputHTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
export type FloatingInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'placeholder'> & {
label: string
hint?: string
/** Shown in red under the field, and marks the field invalid. */
error?: string
}
/** A text field whose label sits inside it and floats up into the border when you type or focus. */
export function FloatingInput({ label, hint, error, className, id, ...props }: FloatingInputProps) {
const own = useId()
const fieldId = id ?? own
const note = error ?? hint
return (
<div className={cn('w-full', className)}>
<div className="relative">
<input
id={fieldId}
placeholder=" "
aria-invalid={error ? true : undefined}
aria-describedby={note ? `${fieldId}-note` : undefined}
className={cn(
'peer h-12 w-full rounded-xl border bg-background px-4 pt-1 outline-none transition-[border-color,box-shadow] duration-200',
'focus:border-ring focus:ring-3 focus:ring-ring/20',
error && 'border-destructive focus:border-destructive focus:ring-destructive/20',
)}
{...props}
/>
<label
htmlFor={fieldId}
className={cn(
'pointer-events-none absolute top-0 left-3 origin-left -translate-y-1/2 bg-background px-1 text-xs text-muted-foreground transition-all duration-200 ease-out',
'peer-placeholder-shown:top-1/2 peer-placeholder-shown:text-base',
'peer-focus:top-0 peer-focus:text-xs peer-focus:text-ring',
error && 'text-destructive peer-focus:text-destructive',
'motion-reduce:transition-none',
)}
>
{label}
</label>
</div>
{note && (
<p id={`${fieldId}-note`} className={cn('mt-1.5 px-1 text-xs', error ? 'text-destructive' : 'text-muted-foreground')}>
{note}
</p>
)}
</div>
)
}Installér
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/floating-input.jsonMed registret tilføjet (components.json)
npx shadcn@latest add @manniche/floating-input