Levitating Field Input
A real text input whose typed words hang above the field and cast a soft shadow on its lit shelf; focus lifts them higher and submitting lands them.
Props
"use client";
import { useId } from "react";
import type { ComponentPropsWithoutRef, CSSProperties } from "react";
/**
* LevitatingFieldInput — a real text input whose words hang above the field and cast a shadow on its shelf.
*
* The input is lifted a few pixels inside a pale field with a lit shelf
* along its bottom edge, and its text throws a soft shadow down onto the
* shelf (offset and blur grow with the height), so the typed words read as
* floating. The caret, selection, autofill and every input attribute stay
* native. Focus raises the words about 4px more and lights a faint wedge
* above the field; `dropped` (set it when the form submits) lands them on
* the shelf with one small settle. It is CSS only: one registered custom
* property drives the lift, the shadow and the light. Label, hint and error
* are wired with htmlFor, aria-describedby and aria-invalid. Reduced motion
* makes every change instant.
*
* Part of the Attention kit: a style of anomalies, familiar things made wrong
* (fish in dry air, a stone that breaks upward, a shadow that runs toward its
* lamp). One subject, one motivated light, one broken rule, and everything
* else calm. Cut byte for byte from the kit's one site (AttentionSite, a
* demo studio called Aerium). Paste it as its own file: it carries its own
* copy of every helper it uses and has no dependencies.
*/
/** The kit's type: Instrument Sans (through --font-instrument) over Geist. */
const A_SANS = 'var(--font-instrument, "Instrument Sans"), "Geist", system-ui, sans-serif';
/** The field's CSS: one registered number (--a-h, the text's height in px) drives the lift, the shadow and the light. */
const A_FIELD_CSS =
"@property --a-h{syntax:'<number>';inherits:true;initial-value:5}" +
".a-field{--a-h:var(--a-rest,5);transition:--a-h .24s cubic-bezier(.2,.7,.2,1)}" +
".a-field:focus-within{--a-h:var(--a-up,9)}" +
".a-field[data-dropped]{--a-h:0;animation:a-drop .3s cubic-bezier(.5,0,.75,0)}" +
"@keyframes a-drop{70%{--a-h:0}85%{--a-h:.7}100%{--a-h:0}}" +
".a-field input{transform:translate3d(0,calc(-1px * var(--a-h)),0);text-shadow:0 calc(1px + 1.5px * var(--a-h)) calc(0.6px + 0.9px * var(--a-h)) rgba(10,14,18,calc(0.36 - 0.018 * var(--a-h)))}" +
".a-field input::placeholder{color:rgba(10,14,18,.42);text-shadow:none}" +
".a-field .a-wedge{opacity:0;transition:opacity .3s}" +
".a-field:focus-within .a-wedge{opacity:1}" +
"@media (prefers-reduced-motion:reduce){.a-field,.a-field .a-wedge{transition:none}.a-field[data-dropped]{animation:none}}";
export interface LevitatingFieldInputProps extends Omit<ComponentPropsWithoutRef<"input">, "className" | "style" | "children" | "size"> {
/** The visible label. */
label: string;
/** A short hint under the field. */
hint?: string;
/** An error under the field (sets aria-invalid). */
error?: string;
/** How high the words hang at rest, 2-10px (focus lifts them about 4px more). */
lift?: number;
/** Settle the words onto the shelf (set it when the form submits). */
dropped?: boolean;
className?: string;
style?: CSSProperties;
}
/**
* LevitatingFieldInput — a real text input whose words hang above the field
* and cast a soft shadow on its lit shelf; focus lifts them, `dropped` lands them.
*/
export function LevitatingFieldInput({ label, hint, error, lift = 5, dropped = false, id, className = "", style, ...input }: LevitatingFieldInputProps) {
const auto = useId();
const fid = id ?? "a-field-" + auto;
const rest = Math.min(10, Math.max(2, Number.isFinite(lift) ? lift : 5));
const hintId = hint ? fid + "-hint" : undefined;
const errId = error ? fid + "-error" : undefined;
const described = [input["aria-describedby"], hintId, errId].filter(Boolean).join(" ") || undefined;
return (
<div
className={"a-field flex flex-col gap-2 " + className}
data-dropped={dropped ? "" : undefined}
style={{ "--a-rest": rest, "--a-up": rest + 4, fontFamily: A_SANS, ...style } as CSSProperties}
>
<style>{A_FIELD_CSS}</style>
<label htmlFor={fid} className="font-mono text-[10px] leading-[1.6] tracking-[0.14em] text-[#0a0e12]/72 uppercase">
{label}
</label>
<div className="relative rounded-[2px]" style={{ background: "rgba(250,252,253,0.72)", boxShadow: "inset 0 0 0 1px " + (error ? "rgba(150,40,30,0.55)" : "rgba(10,14,18,0.14)"), backdropFilter: "blur(10px)" }}>
<span
aria-hidden="true"
className="a-wedge pointer-events-none absolute inset-x-[8%] -top-[34px] h-[34px]"
style={{ background: "linear-gradient(180deg, rgba(255,255,255,0), rgba(255,255,255,0.5))", clipPath: "polygon(22% 0, 78% 0, 100% 100%, 0 100%)" }}
/>
<input
id={fid}
{...input}
aria-invalid={error ? true : input["aria-invalid"]}
aria-describedby={described}
className="relative z-[1] block w-full bg-transparent px-3.5 pt-3.5 pb-3 text-[16px] leading-[1.3] text-[#0a0e12] caret-[#0a0e12] outline-none"
/>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 bottom-0 h-[7px] rounded-b-[2px]"
style={{ background: "linear-gradient(180deg, rgba(10,14,18,0) 0, rgba(10,14,18,0.05) 5px, rgba(255,255,255,0.9) 6px, rgba(255,255,255,0.9) 7px)" }}
/>
<span aria-hidden="true" className="pointer-events-none absolute inset-0 rounded-[2px] [.a-field:focus-within_&]:shadow-[inset_0_0_0_2px_#0a0e12]" />
</div>
{hint && (
<p id={hintId} className="m-0 text-[12px] leading-[1.45] text-[#0a0e12]/70">
{hint}
</p>
)}
{error && (
<p id={errId} className="m-0 text-[12px] leading-[1.45] text-[#7a1f16]">
{error}
</p>
)}
</div>
);
}
Free, yours to ship. Pro adds the heroes, WebGL backgrounds and complete style kits of Attention, from $19/month (launch price until 31 Oct).
About this component
A text field where the words float. As you type, the letters hang a few pixels above the field and throw a soft shadow down onto a lit shelf along its bottom edge. Focus lifts them a little higher and lights a faint wedge above the field. When the form is sent, set dropped and the words land on the shelf with one small settle, their shadow turning into a contact line. It is a real input with a label, a hint and an error, so it drops into any form.
Curator’s note
CSS only: one registered custom property is the words' height, and the lift, the shadow's offset, blur and opacity, and the focus light all read it, so a single transition moves them together. The input stays a native input: caret, selection and autofill are untouched. Cut byte for byte from the Attention kit's one site (AttentionSite), so it reads as one system with the kit's rooms: one subject, one motivated light, one broken rule.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Levitating Link Nav
componentsA dark glass nav bar whose link labels hang in the air above a lit shelf, each over its own soft shadow; the link you point at, and the one for the section in view, lands.
Contrary Shadow Section
sectionsMounted letters on a lit plaster wall under a swinging lamp: every word's shadow runs from the light, except one, whose shadow runs toward it.
Levitating Plan Cards
sectionsThree plan slabs that lift off a lit floor as the section arrives, each over its own soft shadow; the featured one rises highest and the pointer raises any slab higher.