Skip to content

Floating Label Input

ComponentMantine ≥ 9MantineProvider

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.

Terminal window
npx manteen add @house/floating-label-input

Dependencies

  • @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.

floating-label-input.usage.tsx
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.

PropTypeDefaultDescription
labelstring"Floating label"TextInput label text, rendered floating above the field on focus or fill.
placeholderstring"OMG, it also has a placeholder"TextInput placeholder shown while the field is empty and unfocused.
requiredbooleantrueRenders the asterisk, which fades in only while the label is floating.
autoCompletestring"nope"Browser-autofill-suppression default carried over from upstream.
valuestringControlled value. When provided, this component is fully controlled and onChange is the only way its displayed value changes.
defaultValuestringInitial value for uncontrolled usage. Ignored once value is provided.
onChangeChangeEventHandler<HTMLInputElement>Fires on every keystroke regardless of controlled/uncontrolled mode; matches TextInput's own onChange signature.
errorReactNodeInherited from TextInputProps. Renders the red invalid border/background and the error message, and composes with the floating label.
errorIconReactNode | null<IconAlertTriangle />Shown in the right section only while error is set; pass null to keep the invalid styling without an icon.
...othersOmit<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:

  • root
  • label
  • required
  • input
  • error

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.