Mikkel Manniche
Dansk English Deutsch

Controls

Floating input

A text field whose label floats up on focus, with a hint and an error state. CSS only.

Adapted from Watermelon UI (MIT) and rewritten with keyboard support, labels and reduced motion.

Preview

Usage

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>
  )
}

Source

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

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/floating-input.json

With the registry added (components.json)

npx shadcn@latest add @manniche/floating-input

Dependencies

Files

  • floating-input.tsx

Manniche Lab · open source

Free tools for AI agents and web development.