FloatingInput
FormA persistent floating label for text-like native inputs with linked guidance.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import { FloatingInput } from "@/components/ui/floating-input";
import { useState } from "react";
import { Button } from "@dashboardpack/core/components/ui/button";
export default function FloatingInputExample() {
const [email, setEmail] = useState("");
const [submitted, setSubmitted] = useState(false);
const [code, setCode] = useState("apex");
return (
<form
className="w-full max-w-sm space-y-5"
onSubmit={(event) => {
event.preventDefault();
setSubmitted(code.trim().length >= 6);
}}
>
<h3 className="font-semibold">A simpler workspace profile</h3>
<FloatingInput
label="Email address"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
hint="The label stays visible as you type."
/>
<FloatingInput label="Display name" defaultValue="Alex Morgan" />
<FloatingInput
label="Workspace code"
value={code}
onChange={(event) => setCode(event.target.value)}
minLength={6}
required
error={
code.trim().length < 6 ? "Use at least six characters." : undefined
}
/>
<FloatingInput
label="Managed organization"
defaultValue="Apex Studio"
disabled
/>
<Button type="submit">Save profile</Button>
<p role="status" className="text-xs text-muted-foreground">
{submitted
? "Preview profile saved locally."
: "No changes are sent to a server."}
</p>
</form>
);
}Usage
Use for compact text, email, password, search, telephone, or URL entry. The input reserves space above its value, and the label moves between the empty center and the top on focus/content. Native autofill and value/defaultValue use the ordinary input API. A blank-space placeholder is internal; never use it as an accessible label. Date and number controls are intentionally outside this component because their browser widgets need different layouts.
import { FloatingInput } from "@/components/ui/floating-input";Shipped as editable source in src/components/ui/floating-input.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.
Component source
The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.
src/components/ui/floating-input.tsx
"use client";
import { useId, type ComponentProps } from "react";
import { cn } from "@dashboardpack/core/lib/utils";
export interface FloatingInputProps
extends Omit<ComponentProps<"input">, "placeholder" | "type"> {
label: string;
type?: "text" | "email" | "password" | "search" | "tel" | "url";
hint?: string;
error?: string;
}
export function FloatingInput({
label,
id,
type = "text",
hint,
error,
className,
required,
...props
}: FloatingInputProps) {
const generated = useId();
const inputId = id ?? generated;
const described =
[
props["aria-describedby"],
hint ? `${inputId}-hint` : null,
error ? `${inputId}-error` : null,
]
.filter(Boolean)
.join(" ") || undefined;
return (
<div className={cn("min-w-0 space-y-2", className)}>
<div className="relative">
<input
{...props}
id={inputId}
type={type}
placeholder=" "
required={required}
aria-invalid={error ? true : props["aria-invalid"]}
aria-describedby={described}
className={cn(
"peer h-14 w-full rounded-xl border border-input bg-background px-3 pb-2 pt-6 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
error && "border-destructive",
)}
/>
<label
htmlFor={inputId}
className="pointer-events-none absolute start-3 top-2 text-xs text-muted-foreground transition-all peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:text-sm peer-focus:top-2 peer-focus:translate-y-0 peer-focus:text-xs motion-reduce:transition-none"
>
{label}
{required && <span aria-hidden> *</span>}
</label>
</div>
{hint && (
<p id={`${inputId}-hint`} className="text-xs text-muted-foreground">
{hint}
</p>
)}
{error && (
<p
id={`${inputId}-error`}
role="alert"
className="text-xs text-destructive"
>
{error}
</p>
)}
</div>
);
}API reference
Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.
| Prop | Type / Default | Description |
|---|---|---|
| label | stringDefault: required | Persistent visible input label. |
| type | text | email | password | search | tel | urlDefault: text | Supported text-like input type. |
| hint | stringDefault: — | Linked guidance below the control. |
| error | stringDefault: — | Linked announced validation error. |
| value / defaultValue / onChange | native input propsDefault: — | Standard controlled or uncontrolled input behavior. |
| required / disabled | booleanDefault: false | Native form and availability states. |
| className | stringDefault: — | Extend the container with semantic theme utilities. |