Text area
A multi-line text field whose height the crew drag a whole line at a time, between a fewest and a most lines.
On this page
Code
import { FormField } from "@bosunkit/components/form-field";
import { TextArea } from "@bosunkit/components/text-area";
import {
useId,
useState,
type ChangeEvent,
type CSSProperties,
type Dispatch,
type ReactElement,
type SetStateAction,
} from "react";
export const description: string =
"A watch log remark and the announcement an anchor alarm speaks, with a character counter. Drag the handle under " +
"either field, or tab to it and press the arrow keys: each grows a line at a time from three lines to five.";
const ANNOUNCEMENT_LIMIT: number = 160;
const HINT: CSSProperties = {
display: "flex",
justifyContent: "space-between",
gap: "var(--space-sm)",
fontFamily: "var(--font-mono)",
fontSize: "0.75em",
color: "var(--text-dim)",
};
/** The count keeps its figures and its unit on one line; the description beside it wraps instead. */
const COUNT: CSSProperties = { flex: "none", whiteSpace: "nowrap" };
export default function Showcase(): ReactElement {
const announcementHintId: string = useId();
const [announcement, setAnnouncement]: [string, Dispatch<SetStateAction<string>>] = useState<string>(
"Anchor alarm. The vessel is 38 meters outside the swing circle. Check the position and start the engine.",
);
return (
<div style={{ width: "100%", maxWidth: 560, display: "grid", gap: "var(--space-lg)" }}>
<FormField label="Watch log remark">
<TextArea
aria-label="Watch log remark"
placeholder="What happened on watch: sail changes, traffic, weather, anything the next watch should know."
defaultValue={
"02:10 Wind backed SW 18 kn, gusts 24. Took the second reef.\n" +
"03:05 Ferry Pont-Aven passed 1.2 NM to port, CPA steady.\n" +
"03:40 Barometer 1006 hPa, down 3 hPa in three hours."
}
/>
</FormField>
<FormField label="Anchor alarm announcement">
<TextArea
aria-label="Anchor alarm announcement"
aria-describedby={announcementHintId}
maxLength={ANNOUNCEMENT_LIMIT}
value={announcement}
onChange={(event: ChangeEvent<HTMLTextAreaElement>): void => {
setAnnouncement(event.target.value);
}}
/>
<div id={announcementHintId} style={HINT}>
<span>Spoken on every speaker when the alarm sounds.</span>
<span style={COUNT}>
{announcement.length} of {ANNOUNCEMENT_LIMIT} characters
</span>
</div>
</FormField>
</div>
);
}
A text area takes text that runs to several lines: a log remark, a handover note, the words an alarm announces. It starts three lines tall. The handle on its bottom edge makes it taller or shorter, one whole line at a time, from three lines to five; past the fifth line the text scrolls inside the field. The field always fills the width of its container and never resizes sideways, so a form keeps its columns however the crew size a field.
How it resizes
Dragging. Pressing the handle and moving down adds a line as the pointer passes each half-line, and moving up
takes one away, between minRows and maxRows. The drag follows the pointer that started it, whether a mouse, a
finger or a pen, until it is lifted. The height changes at once, with no animation, so there is nothing to slow
down under reduced motion.
Range. By default the field runs from 3 to 5 lines. minRows and maxRows set another range: whole numbers,
at least one line, and a most no smaller than the fewest. A range outside those rules throws when the field renders.
rows with onRowsChange lets the caller hold the line count, and defaultRows starts the field's own count.
Keyboard and screen readers. The handle is a focusable separator with aria-orientation="horizontal". It
reports its lines as aria-valuenow between aria-valuemin and aria-valuemax, is named "Field height" and reads
"3 of 5 lines" in the crew's language. ArrowUp and ArrowDown step a line; Home and End go to the fewest and the most.
Pass resizeLabel when a page holds several text areas, so each handle names its own field.
Text. The textarea takes its own props: value or defaultValue with onChange, placeholder, maxLength,
name, readOnly, disabled and every aria-* attribute. A read-only field keeps its handle, so a long entry can
be opened up to read. A disabled field disables its handle too. aria-invalid draws the border in the alarm color;
put the message in a form field's error.
Examples
From three lines to five
Code
import { TextArea } from "@bosunkit/components/text-area";
import type { ReactElement } from "react";
export const description: string =
"Three lines to start, five at most: drag the handle down a line at a time. Text past the fifth line scrolls.";
export default function Default(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 420 }}>
<TextArea aria-label="Remarks" placeholder="Remarks for the next watch" />
</div>
);
}
From two lines to eight
Code
import { TextArea } from "@bosunkit/components/text-area";
import type { ReactElement } from "react";
export const description: string =
"minRows and maxRows set the range: passage notes start at two lines and open to eight.";
export default function Range(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 420 }}>
<TextArea
aria-label="Passage notes"
minRows={2}
maxRows={8}
defaultValue={
"Leave Camaret at 06:30 on the last of the ebb.\n" +
"Raz de Sein at slack, 08:10. Keep 0.5 NM off La Vieille.\n" +
"Audierne by noon; anchor off Sainte-Évette in 4 m, sand."
}
/>
</div>
);
}
Lines held by the caller
Code
import { Button } from "@bosunkit/components/button";
import { TextArea } from "@bosunkit/components/text-area";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"The caller holds rows: the buttons set the height, and a drag or the arrow keys report the new line count.";
export default function Controlled(): ReactElement {
const [rows, setRows]: [number, Dispatch<SetStateAction<number>>] = useState<number>(4);
return (
<div style={{ width: "100%", maxWidth: 420, display: "grid", gap: "var(--space-sm)" }}>
<TextArea
aria-label="Handover notes"
rows={rows}
onRowsChange={setRows}
defaultValue={"Watermaker running since 05:00.\nPort genoa sheet chafing at the lead block."}
/>
<div style={{ display: "flex", alignItems: "center", gap: "var(--space-sm)" }}>
<Button
variant="ghost"
size="sm"
type="button"
onClick={(): void => {
setRows(3);
}}
>
Fewest lines
</Button>
<Button
variant="ghost"
size="sm"
type="button"
onClick={(): void => {
setRows(5);
}}
>
Most lines
</Button>
<span style={{ fontFamily: "var(--font-mono)", fontSize: "0.75em", color: "var(--text-dim)" }}>
{rows} lines
</span>
</div>
</div>
);
}
Read-only
Code
import { TextArea } from "@bosunkit/components/text-area";
import type { ReactElement } from "react";
export const description: string =
"A read-only field stays focusable and selectable, and its handle opens it up to read a long entry.";
export default function ReadOnly(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 420 }}>
<TextArea
aria-label="Signed log entry"
readOnly
defaultValue={
"04:00 Handed over to Marta Lindqvist.\n" +
"Position 48°13.4′N 004°29.9′W, course 215°, 6.2 kn.\n" +
"Wind SW 16 kn, sea moderate, visibility good.\n" +
"Second reef in. Engine off since 01:15.\n" +
"Signed: Anneke de Vries, skipper."
}
/>
</div>
);
}
Disabled
Code
import { FormField } from "@bosunkit/components/form-field";
import { TextArea } from "@bosunkit/components/text-area";
import type { ReactElement } from "react";
export const description: string =
"A disabled field and its handle are dimmed and skipped by the keyboard; the hint says why.";
export default function Disabled(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 420 }}>
<FormField label="Departure remarks">
<TextArea
aria-label="Departure remarks"
aria-describedby="departure-remarks-hint"
disabled
defaultValue="Fuel full, water 340 l, crew of four."
/>
<div
id="departure-remarks-hint"
style={{ fontFamily: "var(--font-mono)", fontSize: "0.75em", color: "var(--text-dim)" }}
>
The passage has started, so its departure remarks are closed.
</div>
</FormField>
</div>
);
}
Invalid, with its error
Code
import { FormField } from "@bosunkit/components/form-field";
import { TextArea } from "@bosunkit/components/text-area";
import type { ReactElement } from "react";
export const description: string =
"aria-invalid draws the border in the alarm color, and the form field's error says what to fix.";
export default function Invalid(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 420 }}>
<FormField label="Reason for the course change" error="Say why the course changed, such as traffic or a squall.">
<TextArea aria-label="Reason for the course change" aria-invalid="true" required defaultValue="" />
</FormField>
</div>
);
}
In a form field
Code
import { FormField } from "@bosunkit/components/form-field";
import { TextArea } from "@bosunkit/components/text-area";
import type { ReactElement } from "react";
export const description: string =
"Inside a form field the label sits above, the hint below, and the field takes the label as its name.";
export default function InFormField(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 420 }}>
<FormField label="Engine room check">
<TextArea
aria-label="Engine room check"
aria-describedby="engine-room-check-hint"
placeholder="Oil, coolant, belts, bilge, stern gland"
/>
<div
id="engine-room-check-hint"
style={{ fontFamily: "var(--font-mono)", fontSize: "0.75em", color: "var(--text-dim)" }}
>
Anything that differs from the last check.
</div>
</FormField>
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| ref | Ref<HTMLTextAreaElement> | undefined | – | The textarea element. |
| rows | number | undefined | – | How many lines tall the field is, when the caller holds it; kept between minRows and maxRows. |
| defaultRows | number | undefined | – | How many lines tall the field starts when the caller does not hold rows; minRows by default. |
| onRowsChange | ((rows: number) => void) | undefined | – | Called with the new line count when the crew drag the handle or step it from the keyboard. |
| minRows | number | undefined | DEFAULT_MIN_ROWS | The fewest lines the field shows: a whole number, at least 1; 3 by default. |
| maxRows | number | undefined | DEFAULT_MAX_ROWS | The most lines the field grows to before its text scrolls: a whole number, at least minRows; 5 by default. |
| resizeLabel | string | undefined | – | The drag handle's accessible name, translated by the caller; "Field height" in the crew's language by default. |
| className | string | undefined | – | Classes added to the root, which holds the field and its handle. |
| testId | string | undefined | – | Becomes data-testid on the root; the handle is ${testId}--handle. |
Also takes every attribute of Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "rows" | "cols">.
Related
- Form field: the label above a text area and the error under it.
- Input: the single-line field for a name, a number or a short value.
- Schema form: the form that draws every long-text field of an entity as a text area.