Floating Label Input
A TextInput whose label floats above the field on focus or fill, with an absorbed invalid/error state.
Install
One command copies the item's source into your project, where you own and edit it.
npx manteen add @house/floating-label-inputDependencies
@mantine/core@^9npm@tabler/icons-react@^3npm
Installing this item also installs @house/mantine-ui-license from the registry.
Preview
Usage
A copy-ready example published by the registry author. Imports use the aliases Manteen configures at init.
import { useState } from "react";import { FloatingLabelInput } from "@/components/ui/floating-label-input";
export function SignupEmailField() { const [email, setEmail] = useState(""); const isValid = email.trim().length === 0 || email.includes("@");
return ( <FloatingLabelInput label="Email" placeholder="you@example.com" autoComplete="email" value={email} onChange={(event) => setEmail(event.currentTarget.value)} error={isValid ? undefined : "Enter a valid email address"} /> );}Props
Author-documented props, carried verbatim from the registry item.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Floating label" | TextInput label text, rendered floating above the field on focus or fill. |
placeholder | string | "OMG, it also has a placeholder" | TextInput placeholder shown while the field is empty and unfocused. |
required | boolean | true | Renders the asterisk, which fades in only while the label is floating. |
autoComplete | string | "nope" | Browser-autofill-suppression default carried over from upstream. |
value | string | — | Controlled value. When provided, this component is fully controlled and onChange is the only way its displayed value changes. |
defaultValue | string | — | Initial value for uncontrolled usage. Ignored once value is provided. |
onChange | ChangeEventHandler<HTMLInputElement> | — | Fires on every keystroke regardless of controlled/uncontrolled mode; matches TextInput's own onChange signature. |
error | ReactNode | — | Inherited from TextInputProps. Renders the red invalid border/background and the error message, and composes with the floating label. |
errorIcon | ReactNode | null | <IconAlertTriangle /> | Shown in the right section only while error is set; pass null to keep the invalid styling without an icon. |
...others | Omit<TextInputProps, "value" | "defaultValue" | "onChange" | "classNames" | "styles" | "unstyled" | "vars" | "attributes" | "variant"> | — | All other TextInputProps are forwarded to the underlying TextInput (size, radius, disabled, leftSection, wrapperProps, description, id, name, etc). variant is intentionally not exposed — the CSS module assumes Mantine's default input chrome. Mantine Styles API props (classNames, styles, unstyled) are also accepted. |
* required
Styling
FloatingLabelInput exposes these parts through the public classNames/styles interface:
rootlabelrequiredinputerror
Author-declared; Manteen reports the declaration but does not verify each selector is wired.
Source
2 installable files ship with this item.
registry/mantine-ui/floating-label-input/floating-label-input.tsxcomponent · 6.1 kB · tsx
/** * Adapted from Mantine UI's FloatingLabelInput and InputValidation at * ffbf61c559f374a7ea28fcf00355e84dcbe9a908. MIT licensed; see * LICENSES/MANTINE-UI.txt after installation. * * Controlled/uncontrolled contract (settled across floating-label-input, * password-strength, autocomplete-loading — the tranche's three form * controls): whether the public `value`/`onChange` pair is event-based or * value-based is decided by the props surface, not by preference. * - Drop-in wrappers — `Props extends Omit<XProps, ...>` with X's own ref * type, rendering nothing but `<X {...} />` — keep X's exact contract, * because a consumer swapping `<X>` for the wrapper shouldn't have to * rewrite their handler. TextInput/PasswordInput/Textarea are * event-based (`ChangeEventHandler<HTMLInputElement>`); * Autocomplete/Select and the rest of the combobox family are * value-based (`(value: string) => void`). * - Composite components that own their own props surface (their own * BoxProps/ElementProps, their own ref) aren't a drop-in for any single * base, so they expose the simple value-based contract instead. * Both controlled and uncontrolled work everywhere. Event-based drop-ins * hand-roll `useState` + `isControlled = value !== undefined`, because * `useUncontrolled`'s onChange payload type is tied to the tracked value * type and can't emit a raw DOM event. Everything else uses * `@mantine/hooks`' `useUncontrolled`. * * This component is a drop-in for TextInput (its props extend * `Omit<TextInputProps, ...>` and its ref is `HTMLInputElement`): * event-based onChange, hand-rolled controlled/uncontrolled state. */
import { type Factory, factory, type StylesApiProps, TextInput, type TextInputProps, useProps, useStyles,} from "@mantine/core";import { IconAlertTriangle } from "@tabler/icons-react";import { type ChangeEventHandler, type ReactNode, useState } from "react";
import classes from "./floating-label-input.module.css";
export type FloatingLabelInputStylesNames = "root" | "label" | "required" | "input" | "error";
export interface FloatingLabelInputProps extends Omit< TextInputProps, | "classNames" | "styles" | "unstyled" | "vars" | "attributes" | "variant" | "value" | "defaultValue" | "onChange" >, StylesApiProps<FloatingLabelInputFactory> { /** * Controlled value. When provided, this component is fully controlled and * `onChange` is the only way its displayed value changes. When omitted, * the component tracks its own internal state (seeded from * `defaultValue`) so it also works as a drop-in uncontrolled input. */ value?: string; /** Initial value for uncontrolled usage. Ignored once `value` is provided. */ defaultValue?: string; onChange?: ChangeEventHandler<HTMLInputElement>; /** * Icon rendered in the right section while `error` is set. Pass `null` to * keep the invalid styling without an icon. * @default <IconAlertTriangle /> */ errorIcon?: ReactNode | null;}
export type FloatingLabelInputFactory = Factory<{ props: FloatingLabelInputProps; ref: HTMLInputElement; stylesNames: FloatingLabelInputStylesNames;}>;
const DEFAULT_ERROR_ICON = <IconAlertTriangle stroke={1.5} size={18} aria-hidden="true" />;
export const FloatingLabelInput = factory<FloatingLabelInputFactory>((_props) => { const props = useProps("FloatingLabelInput", null, _props); const { classNames, className, style, styles, unstyled, vars, attributes, ref, label = "Floating label", placeholder = "OMG, it also has a placeholder", required = true, autoComplete = "nope", value, defaultValue, onChange, onFocus, onBlur, error, errorIcon = DEFAULT_ERROR_ICON, rightSection, labelProps, ...others } = props;
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? ""); const [focused, setFocused] = useState(false); const isControlled = value !== undefined; const currentValue = isControlled ? value : uncontrolledValue; const floating = currentValue.trim().length !== 0 || focused || undefined;
const getStyles = useStyles<FloatingLabelInputFactory>({ name: "FloatingLabelInput", classes, props, className, style, classNames, styles, unstyled, attributes, vars, });
const errorIconSection = error && errorIcon !== null ? ( <span className={classes.errorIcon}>{errorIcon}</span> ) : undefined;
return ( <TextInput ref={ref} unstyled={unstyled} label={label} placeholder={placeholder} required={required} autoComplete={autoComplete} value={currentValue} error={error} rightSection={errorIconSection ?? rightSection} onChange={(event) => { if (!isControlled) setUncontrolledValue(event.currentTarget.value); onChange?.(event); }} onFocus={(event) => { setFocused(true); onFocus?.(event); }} onBlur={(event) => { setFocused(false); onBlur?.(event); }} data-floating={floating} labelProps={{ ...labelProps, "data-floating": floating }} classNames={{ root: getStyles("root").className, label: getStyles("label").className, required: getStyles("required").className, input: getStyles("input").className, error: getStyles("error").className, }} styles={{ root: getStyles("root").style, label: getStyles("label").style, required: getStyles("required").style, input: getStyles("input").style, error: getStyles("error").style, }} {...others} /> );});
FloatingLabelInput.classes = classes;FloatingLabelInput.displayName = "FloatingLabelInput";
export namespace FloatingLabelInput { export type Props = FloatingLabelInputProps; export type StylesNames = FloatingLabelInputStylesNames; export type Factory = FloatingLabelInputFactory;}registry/mantine-ui/floating-label-input/floating-label-input.module.cssstyle · 1.2 kB · css
/* * Adapted from Mantine UI's FloatingLabelInput and InputValidation at * ffbf61c559f374a7ea28fcf00355e84dcbe9a908. MIT licensed; see * LICENSES/MANTINE-UI.txt after installation. */.root { position: relative;}
.label { position: absolute; z-index: 2; top: 7px; left: var(--mantine-spacing-sm); pointer-events: none; color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); transition: transform 150ms ease, font-size 150ms ease, color 150ms ease;
&[data-floating] { transform: translate(calc(var(--mantine-spacing-sm) * -1), -28px); font-size: var(--mantine-font-size-xs); font-weight: 500; color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); }}
.required { transition: opacity 150ms ease; opacity: 0;
[data-floating] & { opacity: 1; }}
.input { &::placeholder { transition: color 150ms ease; color: transparent; }
&[data-floating] { &::placeholder { color: var(--mantine-color-placeholder); } }
&[data-error] { background-color: var(--mantine-color-red-light); }}
.errorIcon { color: light-dark(var(--mantine-color-red-6), var(--mantine-color-red-7));}Author notes
Adapted from Mantine UI FloatingLabelInput and InputValidation at ffbf61c559…; absorbed InputValidation's error state into one component, label/placeholder/required/autoComplete became props, and a missing aria-hidden on the decorative error icon was fixed.