Bedienelemente
Floating input
Ein Textfeld, dessen Label beim Fokus nach oben schwebt, mit Hinweis und Fehlerzustand. Nur CSS.
Angepasst von Watermelon UI (MIT) und neu geschrieben mit Tastatur, Beschriftungen und reduzierter Bewegung.
Vorschau
Verwendung
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>
)
}Quellcode
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>
)
}Installieren
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/floating-input.jsonMit eingetragener Registry (components.json)
npx shadcn@latest add @manniche/floating-input