Mikkel Manniche
Dansk English Deutsch

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.json

Mit eingetragener Registry (components.json)

npx shadcn@latest add @manniche/floating-input

Abhängigkeiten

Dateien

  • floating-input.tsx

Manniche Lab · Open Source

Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.