Forms
FloatInput
A text field whose label sits inside like a placeholder, then floats up and shrinks when you focus or type. A line grows from the middle on focus.
Download
- React
- Tailwind CSS v4
components/ui/float-input.tsx
"use client";
import { useId, type CSSProperties, type InputHTMLAttributes } from "react";
type FloatInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "placeholder" | "className"> & {
/** Field name shown inside the box. It floats up once you type or focus. */
label: string;
/** Small help text under the field. */
hint?: string;
/** Error text under the field. Turns the field red and sets aria-invalid. */
error?: string;
/** Colour of the floated label and focus line. */
accent?: string;
/** Wrapper look (width etc.). */
className?: string;
};
/* FloatInput: the label sits inside the box like a placeholder, then floats up and shrinks when you focus or type. A line grows from the middle on focus. */
export function FloatInput({
label,
hint,
error,
accent = "#a78bfa",
className = "w-full max-w-sm",
id,
type = "text",
...rest
}: FloatInputProps) {
const autoId = useId();
const inputId = id ?? autoId;
const noteId = `${inputId}-note`;
const note = error ?? hint;
return (
<div className={className} style={{ "--fi-accent": error ? "#f87171" : accent } as CSSProperties}>
<div className="relative">
<input
id={inputId}
type={type}
placeholder=" "
aria-invalid={error ? true : undefined}
aria-describedby={note ? noteId : undefined}
className="peer block h-14 w-full rounded-xl border border-white/12 bg-white/[0.04] px-4 pt-5 pb-1.5 text-base text-white outline-none transition-colors duration-200 hover:border-white/25 focus:border-[var(--fi-accent)] aria-invalid:border-red-400/70 motion-reduce:transition-none"
{...rest}
/>
<label
htmlFor={inputId}
className="pointer-events-none absolute top-1/2 left-4 origin-left -translate-y-1/2 text-base text-white/50 transition-[translate,scale,color] duration-200 ease-[cubic-bezier(0.2,0.8,0.2,1)] peer-focus:-translate-y-[115%] peer-focus:scale-[0.78] peer-focus:text-[var(--fi-accent)] peer-[:not(:placeholder-shown)]:-translate-y-[115%] peer-[:not(:placeholder-shown)]:scale-[0.78] motion-reduce:transition-none"
>
{label}
</label>
<span
aria-hidden
className="pointer-events-none absolute inset-x-3 bottom-0 h-0.5 scale-x-0 rounded-full bg-[var(--fi-accent)] transition-transform duration-300 ease-out peer-focus:scale-x-100 motion-reduce:transition-none"
/>
</div>
{note && (
<p id={noteId} className={`mt-1.5 px-1 text-xs ${error ? "text-red-300" : "text-white/45"}`}>
{note}
</p>
)}
</div>
);
}
1. Paste the Code tab into
components/ui/float-input.tsx. The styles are Tailwind CSS v4 classes.2. Use it. This is the preview above:
app/float-input-demo.tsx
"use client"; import { useState } from "react"; import { FloatInput } from "@/components/ui/float-input"; export default function FloatInputExample() { const [email, setEmail] = useState(""); const bad = email.length > 0 && !/^\S+@\S+\.\S+$/.test(email); return ( <div className="grid h-full place-items-center bg-[#0b0a14] p-5"> <form className="flex w-full max-w-sm flex-col gap-4" onSubmit={(e) => e.preventDefault()}> <FloatInput label="Full name" name="name" autoComplete="name" className="w-full" /> <FloatInput label="Email" name="email" type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} hint="We never share it." error={bad ? "That email looks incomplete." : undefined} className="w-full" /> </form> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| label | string | — | Field name inside the box. |
| hint | string | — | Help text under the field. |
| error | string | — | Error text. Turns the field red and sets aria-invalid. |
| accent | string | "#a78bfa" | Floated label and focus line colour. |
| ...input props | InputHTMLAttributes | — | type, name, value, onChange, autoComplete… all pass to the input. |
| className | string | "w-full max-w-sm" | Wrapper look. |