primitives
Floating Label Input
A single-line text input whose label begins at the field baseline — acting as a visible placeholder — then animates upward and shrinks when the field receives focus or contains a value. The bottom-border transitions from the muted --border token to a two-stop linear-gradient built from --primary on focus, giving a clean gradient underline accent without a surrounding box. Technique uses the CSS :placeholder-shown selector (with a single-space placeholder) and an adjacent sibling combinator to drive the label's position and size purely in CSS — no JS required, making this a server component. Fully controlled (value + onChange), forwardRef, htmlFor/id wired, disabled-safe.
Ported from mrhyddenn (MIT)
Install
1. Register the namespace (once per project):
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
npx shadcn add @bottega/floating-label-inputUsage
<FloatingLabelInput />Props
Standard props — see source.
Source
import * as React from "react";
import styles from "./floating-label-input.module.css";
export type FloatingLabelInputProps = {
/** Unique id wiring the <label> to the <input>. Required for a11y. */
id: string;
/** Text displayed as the floating label. */
label: string;
/** Controlled value. */
value: string;
/** Change handler — receives the raw string value. */
onChange: (value: string) => void;
/** Placeholder text (visually hidden; used for the CSS :placeholder-shown trigger). Defaults to " " (a single space) so the label floats correctly even when value is empty. */
placeholder?: string;
/** Makes the field non-interactive. */
disabled?: boolean;
/** Additional class applied to the root wrapper. */
className?: string;
/** Forwarded to the native <input>. */
name?: string;
/** Forwarded to the native <input>. */
autoComplete?: string;
/** Forwarded to the native <input>. */
required?: boolean;
/** Forwarded to the native <input>. */
type?: string;
};
/** A text input with a floating label that animates up on focus or when a
* value is present. The bottom-border transitions from a muted token color
* to a primary-tinted gradient on focus. Fully controlled; passes `id` through
* to the native element so the `<label>` `htmlFor` is always wired. */
export const FloatingLabelInput = React.forwardRef<
HTMLInputElement,
FloatingLabelInputProps
>(function FloatingLabelInput(
{
id,
label,
value,
onChange,
placeholder = " ",
disabled = false,
className = "",
name,
autoComplete,
required,
type = "text",
},
ref,
) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return (
<div className={[styles.group, className].filter(Boolean).join(" ")}>
<input
ref={ref}
id={id}
name={name}
type={type}
className={styles.field}
placeholder={placeholder}
value={value}
onChange={handleChange}
disabled={disabled}
required={required}
autoComplete={autoComplete}
aria-label={label}
/>
<label htmlFor={id} className={styles.label}>
{label}
</label>
</div>
);
});
FloatingLabelInput.displayName = "FloatingLabelInput";
Dependencies
- @bottega/tokens