Text Field
Text fields allow users to enter text into a UI. They typically appear in forms and dialogs.
Simple Text Field
The default text field will look similar to a native <input type="text">
with a border surrounding the text content that will change color and thickness
when hovered or focused. Every text field should have an accessible label by
providing one of the following props: label
, aria-label
, aria-labelledby
.
It is generally recommended to place a label with the text field, but placeholder only text fields are also supported.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { type ReactElement } from "react";
export default function OutlinedTextField(): ReactElement {
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextField aria-label="Label" placeholder="Placeholder-only" />
<TextField label="Label" placeholder="Placeholder" />
</Form>
);
}
Filled Text Field
Filled text fields have more visual emphasis than outlined text fields, making them stand out when surrounded by other content and components.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { type ReactElement } from "react";
export default function FilledTextField(): ReactElement {
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextField
aria-label="Label"
placeholder="Placeholder-only"
theme="filled"
/>
<TextField label="Label" placeholder="Placeholder" theme="filled" />
<TextField
label="Label"
placeholder="Placeholder"
theme="filled"
underlineDirection="center"
/>
<TextField
label="Label"
placeholder="Placeholder"
theme="filled"
underlineDirection="right"
/>
</Form>
);
}
Underlined Text Field
Underlined text fields have the least emphasis, making them better for less prominent editing.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { type ReactElement } from "react";
export default function UnderlinedTextField(): ReactElement {
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextField
aria-label="Label"
placeholder="Placeholder-only"
theme="underline"
/>
<TextField label="Label" placeholder="Placeholder" theme="underline" />
<TextField
label="Label"
placeholder="Placeholder"
theme="underline"
underlineDirection="center"
/>
<TextField
label="Label"
placeholder="Placeholder"
theme="underline"
underlineDirection="right"
/>
</Form>
);
}
No Theme Text Field
The text field can also be set with theme="none"
which removes most of the
styling behavior. There isn't much use for this though since the floating label
will not display correctly when this is set and can only be used for placeholder
only text fields.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { type ReactElement } from "react";
export default function NoThemeTextField(): ReactElement {
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextField
aria-label="Label"
placeholder="Placeholder-only"
theme="none"
/>
</Form>
);
}
Text Field State
The text field supports the following states:
disabled
- Greys out the input and prevents modificationsreadOnly
- Normal styles but prevents modificationserror
- Applies the theme error color (normally red)active
- Applies the focus state if it is required to be manually be triggered
"use client";
import { Box } from "@react-md/core/box/Box";
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { Radio } from "@react-md/core/form/Radio";
import { TextField } from "@react-md/core/form/TextField";
import { type FormTheme } from "@react-md/core/form/types";
import { useRadioGroup } from "@react-md/core/form/useRadioGroup";
import { Typography } from "@react-md/core/typography/Typography";
import { typography } from "@react-md/core/typography/typographyStyles";
import { type ReactElement } from "react";
export default function TextFieldState(): ReactElement {
const { value: theme, getRadioProps } = useRadioGroup<FormTheme>({
name: "theme",
defaultValue: "outline",
});
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextField label="Disabled" theme={theme} disabled />
<TextField
label="Read Only"
theme={theme}
readOnly
defaultValue="Some text to display"
/>
<TextField label="Active" theme={theme} active />
<TextField label="Error" theme={theme} error />
<TextField label="Normal" theme={theme} />
<Box stacked disablePadding align="start" fullWidth>
<Typography>Form Theme</Typography>
{themes.map((theme) => (
<Radio
key={theme}
{...getRadioProps(theme)}
label={theme}
className={typography({ type: null, textTransform: "capitalize" })}
/>
))}
</Box>
</Form>
);
}
const themes: readonly FormTheme[] = ["underline", "filled", "outline"];
Text Field Type
The text field supports rendering as most of the input types that are displayed as a textbox. There is no additional functionality built-in to support these types other than attempting to display them correctly.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { type ReactElement } from "react";
export default function TextFieldType(): ReactElement {
return (
<Form className={box({ stacked: true, fullWidth: true, align: "start" })}>
<TextField label="Text" type="text" />
<TextField label="Password" type="password" />
<TextField label="Number" type="number" />
<TextField label="Tel" type="tel" />
<TextField label="Email" type="email" />
<TextField label="Date" type="date" />
<TextField label="Time" type="time" />
<TextField label="Datetime-local" type="datetime-local" />
<TextField label="Url" type="url" />
<TextField label="Color" type="color" />
<TextField label="Search" type="search" />
</Form>
);
}
Password
When rendering password fields, it is recommended to use the Password
component instead which will allow the user to toggle the visibility of the
password.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { Password } from "@react-md/core/form/Password";
import { type ReactElement } from "react";
export default function PasswordExample(): ReactElement {
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<Password label="Password" theme="outline" />
<Password label="Password" theme="filled" />
<Password label="Password" theme="underline" />
</Form>
);
}
Number
The default browser implementation <input type="number">
leaves much to be
desired so a useNumberField
hook is provided to fix the following issues and
additional type safety.
- the value will only be
undefined
if thedefaultValue
provided wasundefined
- displays an error message when the user types an invalid number. These are
technically valid numbers in the browser implementation:
--0
,0-0
,0-0-
,++0
, etc - attempts to fix the number on blur:
00000
->0
001
->1
1e+3
->1000
- enforce the value between the
min
andmax
values (if provided)
See more info at the useNumberField documentation.
"use client";
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button } from "@react-md/core/button/Button";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { useNumberField } from "@react-md/core/form/useNumberField";
import { Typography } from "@react-md/core/typography/Typography";
import { type ReactElement } from "react";
export default function NumberExample(): ReactElement {
const { value, error, errorMessage, fieldProps, fieldRef, reset, setState } =
useNumberField({
name: "numberExample",
});
return (
<Form onReset={reset}>
<Typography margin="none">The current value is:</Typography>
<Typography margin="bottom">{`${value}`}</Typography>
<TextField label="Number" {...fieldProps} />
<Button type="reset" theme="warning" themeType="outline">
Reset
</Button>
</Form>
);
}
Simple TextArea
Use the TextArea
component when multiple lines of text should be supported. It
supports all the same theming, addons, help text, and error text as the
TextField
but renders in a <textarea>
instead.
The default behavior of the TextArea
is to resize it's height based on the
current amount of text.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextArea } from "@react-md/core/form/TextArea";
import { type ReactElement } from "react";
export default function SimpleTextArea(): ReactElement {
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextArea label="Label" placeholder="Placeholder..." />
<TextArea label="Label" placeholder="Placeholder..." theme="filled" />
<TextArea label="Label" placeholder="Placeholder..." theme="underline" />
</Form>
);
}
Setting Max Rows
Since it might not be ideal to support an infinite editing height, the max
number of rows to grow to can be specified by the maxRows
prop.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextArea } from "@react-md/core/form/TextArea";
import { type ReactElement } from "react";
export default function ResizingTextAreaMaxRows(): ReactElement {
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextArea
label="Label"
placeholder="Placeholder..."
maxRows={8}
defaultValue={LOREM_IPSUM}
/>
<TextArea
label="Label"
placeholder="Placeholder..."
maxRows={8}
theme="filled"
defaultValue={LOREM_IPSUM}
/>
<TextArea
label="Label"
placeholder="Placeholder..."
maxRows={8}
theme="underline"
defaultValue={LOREM_IPSUM}
/>
</Form>
);
}
const LOREM_IPSUM = `Duis vehicula risus quis urna varius ultrices. Cras id ipsum sed mauris sollicitudin feugiat at eget erat. Sed sed magna sed risus ornare ullamcorper. Curabitur vehicula lorem ante, vel facilisis nunc pulvinar quis. Duis gravida, purus at consequat scelerisque, libero est eleifend sem, ac aliquet dolor ex vitae arcu. Nam at dignissim orci. Nulla posuere sollicitudin malesuada. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nullam at libero mi. Ut a lorem at leo euismod ultricies.
Etiam nisi tellus, accumsan ut leo vel, iaculis feugiat ligula. Nullam congue lorem non lorem maximus porta. Pellentesque nunc magna, tincidunt consectetur maximus vel, euismod in ligula. Duis quis metus ligula. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer dapibus laoreet tincidunt. Vestibulum at erat eu dui convallis cursus. Sed sagittis ut nibh at feugiat. Duis vitae arcu eget risus mattis placerat. Donec eu metus a lorem sollicitudin sollicitudin id nec odio. Curabitur purus urna, vulputate at bibendum id, blandit ut arcu. Vestibulum enim ante, porta et aliquam id, pellentesque et augue.
`;
Disabling Resize Transition
The textarea's height transition can be disabled by setting disableTransition
.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextArea } from "@react-md/core/form/TextArea";
import { type ReactElement } from "react";
export default function DisablingResizeTransition(): ReactElement {
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextArea
label="Label"
placeholder="Placeholder..."
disableTransition
maxRows={8}
/>
</Form>
);
}
Other Resize Behavior
The native resize behavior
is also supported by setting the resize
prop to one of: horizontal
,
vertical
, both
, or none
.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextArea } from "@react-md/core/form/TextArea";
import { type ReactElement } from "react";
export default function OtherResizeBehavior(): ReactElement {
return (
<Form
className={box({ stacked: true, align: "start", fullWidth: true })}
style={{ minWidth: 0 }}
>
<TextArea
label="Resize both"
placeholder="Placeholder..."
resize="both"
/>
<TextArea
label="Resize horizontal"
placeholder="Placeholder..."
resize="horizontal"
/>
<TextArea
label="Resize vertical"
placeholder="Placeholder..."
resize="vertical"
/>
<TextArea label="No resize" placeholder="Placeholder..." resize="none" />
</Form>
);
}
Text Field Addon
The TextField
can render addons (normally icons) before and after the text of
by using the leftAddon
/rightAddon
props. The addon will be placed above the
<input>
using absolute positioning and the <input>
will gain additional
padding-left
/padding-right
so the addons will not overlap with the text
content.
"use client";
import { Avatar } from "@react-md/core/avatar/Avatar";
import { Box } from "@react-md/core/box/Box";
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { Radio } from "@react-md/core/form/Radio";
import { TextField } from "@react-md/core/form/TextField";
import { type FormTheme } from "@react-md/core/form/types";
import { useRadioGroup } from "@react-md/core/form/useRadioGroup";
import { Typography } from "@react-md/core/typography/Typography";
import { typography } from "@react-md/core/typography/typographyStyles";
import FavoriteIcon from "@react-md/material-icons/FavoriteIcon";
import { type ReactElement } from "react";
export default function TextFieldAddon(): ReactElement {
const { value: theme, getRadioProps } = useRadioGroup<FormTheme>({
name: "theme",
defaultValue: "outline",
});
return (
<Form className={box({ stacked: true, align: "stretch" })}>
<TextField
label="Label"
theme={theme}
placeholder="Placeholder"
leftAddon={<FavoriteIcon />}
/>
<TextField
label="Label"
theme={theme}
placeholder="Placeholder"
rightAddon={<FavoriteIcon />}
/>
<TextField
label="Label"
theme={theme}
placeholder="Placeholder"
leftAddon={<FavoriteIcon />}
rightAddon={<Avatar size="icon">A</Avatar>}
/>
<Box stacked disablePadding align="start" fullWidth>
<Typography>Form Theme</Typography>
{themes.map((theme) => (
<Radio
key={theme}
{...getRadioProps(theme)}
label={theme}
className={typography({ type: null, textTransform: "capitalize" })}
/>
))}
</Box>
</Form>
);
}
const themes: readonly FormTheme[] = ["underline", "filled", "outline"];
Non-icon Addons
If an addon is not icon sized, additional styling will be required. Here are a few available options:
- Modify the
--rmd-text-field-padding-left
/--rmd-text-field-padding-right
to be the width of the addon plus some additional padding. This is the "best" option if the addons should still be rendered above the<input>
element since it will also update the floating label position automatically- See the Automatic Addon Padding example to see how this can be handled automatically.
- Set
disableLeftAddonStyles
/disableRightAddonStyles
totrue
to no longer use absolute positioning and render the addon inline - Add custom styles with
leftAddonProps
/rightAddonProps
as needed
Automatic Addon Padding
If the addons are dynamic or custom styles are not desired, the
useTextFieldContainerAddons
hook can be used to automatically update the
--rmd-text-field-padding-left
/--rmd-text-field-padding-right
variables using
the useResizeObserver behind the scenes.
The useTextFieldContainerAddons
hook requires a leftAddon
and rightAddon
flag to be provided and will return a style
object, leftAddonRef
, and
rightAddonRef
which can be passed to the TextField
component.
"use client";
import { Avatar } from "@react-md/core/avatar/Avatar";
import { box } from "@react-md/core/box/styles";
import { Button } from "@react-md/core/button/Button";
import { Chip } from "@react-md/core/chip/Chip";
import { cssUtils } from "@react-md/core/cssUtils";
import { Form } from "@react-md/core/form/Form";
import { TextField, type TextFieldProps } from "@react-md/core/form/TextField";
import { useTextFieldContainerAddons } from "@react-md/core/form/useTextFieldContainerAddons";
import { CircularProgress } from "@react-md/core/progress/CircularProgress";
import { useToggle } from "@react-md/core/useToggle";
import { loop } from "@react-md/core/utils/loop";
import { randomInt } from "@react-md/core/utils/randomInt";
import CancelIcon from "@react-md/material-icons/CancelIcon";
import FavoriteIcon from "@react-md/material-icons/FavoriteIcon";
import { type ReactElement, useEffect, useState } from "react";
export default function AutomaticAddonPaddingExample(): ReactElement {
const { leftAddon, rightAddon, toggle, running } = useRotatingAddons();
const { style, leftAddonRef, rightAddonRef } = useTextFieldContainerAddons({
leftAddon: !!leftAddon,
rightAddon: !!rightAddon,
// this will be merged with the returned `style`
// style: {
// color: "red",
// },
// if there should be more of a gap between the addon and the input, use
// these two options to add values to the `calc()` expression. This **must**
// start with a `+ ` or `- `
//
// leftAddonExtraCalc: "+ var(--rmd-icon-spacing) - 0.125rem",
// rightAddonExtraCalc: "+ calc(var(--rmd-icon-spacing) * 2)",
// if refs are required for the addon container, they will be merged with
// the returned leftAddonRef/rightAddonRef
// leftAddonRef: customLeftAddonRef,
// rightAddonRef: customRightAddonRef,
});
return (
<Form
style={{ maxWidth: "30rem" }}
className={box({ align: "start", stacked: true, fullWidth: true })}
>
<TextField
label="Label"
placeholder="Placeholder"
style={style}
leftAddon={leftAddon}
leftAddonProps={{ ref: leftAddonRef, pointerEvents: true }}
rightAddon={rightAddon}
rightAddonProps={{ ref: rightAddonRef, pointerEvents: true }}
defaultValue="Here's some default content to show padding and overflow"
inputClassName={cssUtils({ textOverflow: "ellipsis" })}
/>
<Button onClick={toggle} themeType="outline" theme="primary">
{running && <CircularProgress aria-label="Running" />}
<span>{running ? "Stop" : "Start"}</span>
</Button>
</Form>
);
}
const ADDONS = [
<CircularProgress
aria-label="Loading"
dense
disableCentered
key="progress"
/>,
<Chip key="chip">Chip</Chip>,
<Button
aria-label="Favorite"
buttonType="icon"
iconSize="small"
key="favorite-button"
>
<FavoriteIcon />
</Button>,
<Avatar color="orange" key="orange-avatar">
O
</Avatar>,
<Avatar color="red" key="red-avatar">
R
</Avatar>,
<Chip key="cancelable-chip" rightAddon={<CancelIcon />} selected>
Chip
</Chip>,
];
const max = ADDONS.length - 1;
interface RotatingAddonsProps
extends Pick<TextFieldProps, "leftAddon" | "rightAddon"> {
toggle: () => void;
running: boolean;
}
function useRotatingAddons(): RotatingAddonsProps {
const [leftAddonIndex, setLeftAddonIndex] = useState<number | undefined>();
const [rightAddonIndex, setRightAddonIndex] = useState<number | undefined>();
const { toggle, toggled: running } = useToggle();
useEffect(() => {
if (!running) {
return;
}
const interval = window.setInterval(() => {
setLeftAddonIndex((prev) => {
const next = randomInt({ min: 0, max });
if (next === prev) {
return loop({ value: prev, increment: true, max });
}
return next;
});
setRightAddonIndex((prev) => {
const next = randomInt({ min: 0, max });
if (next === prev) {
return loop({ value: prev, increment: true, max });
}
return next;
});
}, 1500);
return () => {
window.clearInterval(interval);
};
}, [leftAddonIndex, rightAddonIndex, running]);
return {
toggle() {
if (
typeof leftAddonIndex === "undefined" &&
typeof rightAddonIndex === "undefined"
) {
setLeftAddonIndex(randomInt({ min: 0, max }));
setRightAddonIndex(randomInt({ min: 0, max }));
}
toggle();
},
running,
leftAddon: typeof leftAddonIndex === "number" && ADDONS[leftAddonIndex],
rightAddon: typeof rightAddonIndex === "number" && ADDONS[rightAddonIndex],
};
}
Help Text and Error Text
The TextField
component is wrapped in the
FormMessageContainer so additional
hint or error messages can be displayed.
import { box } from "@react-md/core/box/styles";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { type ReactElement } from "react";
export default function HelpTextAndErrorText(): ReactElement {
return (
<Form className={box({ stacked: true, align: "start" })}>
<TextField
label="Label"
placeholder="Placeholder"
messageProps={{
children: "This is some help text",
}}
/>
<TextField
label="Label"
error
placeholder="Placeholder"
messageProps={{
error: true,
children: "This is some error text",
}}
/>
</Form>
);
}
Text Field With Counter
A text field can also render an inline counter using the messageProps
by
providing the current length of the value and a maxLength
.
"use client";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { type ReactElement, useState } from "react";
const maxLength = 30;
export default function TextFieldWithCounter(): ReactElement {
const [value, setValue] = useState("");
const error = value.length > maxLength;
return (
<Form>
<TextField
label="Label"
placeholder="Placeholder"
value={value}
onChange={(event) => {
setValue(event.currentTarget.value);
}}
// Uncomment this line to allow the browser to prevent adding more
// characters as well
// maxLength={maxLength}
messageProps={{
error,
length: value.length,
maxLength,
children: error ? "Value too long" : "Optional help text",
}}
/>
</Form>
);
}
Text Field Hook
The useTextField
hook can be used to control the value of a single text field
to conditionally display help text, error text, error icons, and inline
counters. The hook also supports simple validation using the
Constraint Validation API.
Check out the useTextField documentation for more information.
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Box } from "@react-md/core/box/Box";
import { box } from "@react-md/core/box/styles";
import { Button } from "@react-md/core/button/Button";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { useTextField } from "@react-md/core/form/useTextField";
import {
defaultGetErrorIcon,
defaultGetErrorMessage,
} from "@react-md/core/form/validation";
import FavoriteIcon from "@react-md/material-icons/FavoriteIcon";
import { type ReactElement } from "react";
export default function TextFieldHookExample(): ReactElement {
const { value, error, errorMessage, fieldRef, reset, setState, fieldProps } =
useTextField({
name: "example",
counter: true,
required: true,
helpText: "Alpha-numeric characters only",
pattern: "^[0-9A-Za-z]+$",
minLength: 10,
maxLength: 60,
// This can be used to configure the error icon that gets placed as a
// rightAddon
getErrorIcon(options) {
const { error, errorMessage, errorIcon } = options;
if (!error) {
return <FavoriteIcon />;
}
return defaultGetErrorIcon(options);
},
// This can be used to set a custom validation message when there
// is an error in the input
getErrorMessage(options) {
const {
isBlurEvent,
isNumber,
validationMessage,
validationType,
validity,
value,
maxLength,
minLength,
pattern,
required,
} = options;
return defaultGetErrorMessage(options);
},
});
return (
<Form
onReset={reset}
className={box({
stacked: true,
align: "stretch",
disablePadding: true,
fullWidth: true,
})}
>
<TextField label="Label" placeholder="Placeholder" {...fieldProps} />
<Box disablePadding disableWrap justify="space-between">
<Button type="reset" theme="warning" themeType="outline">
Reset
</Button>
<Button type="submit" theme="primary" themeType="contained">
Submit
</Button>
</Box>
</Form>
);
}