Input
A text input in the kit's style, with an optional icon and unit inside its edges.
On this page
In UTC, as the log is kept.
93 of 120 characters
The engine is off; the hours fill in when it runs.
Code
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { FormField } from "@bosunkit/components/form-field";
import { Input } from "@bosunkit/components/input";
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import { PositionInput } from "@bosunkit/components/position-input";
import { Clock, NotebookPen, Search } from "lucide-react";
import {
useId,
useRef,
useState,
type ChangeEvent,
type CSSProperties,
type Dispatch,
type ReactElement,
type RefObject,
type SetStateAction,
} from "react";
export const description: string =
"A logbook entry: text, a call sign checked as it is typed, MMSI, time, a number with its unit inside the field, a " +
"search with an icon and harbor suggestions, a long remark with a count, read-only and disabled fields, and a ref " +
"that moves the cursor to the remark when it is empty.";
const REMARK_LIMIT: number = 120;
const CALL_SIGN: RegExp = /^[A-Z0-9]*$/;
const ROW: CSSProperties = { display: "flex", alignItems: "center", gap: "var(--space-sm)" };
const HARBORS: readonly string[] = ["Brest", "Camaret-sur-Mer", "Douarnenez", "Morgat", "Audierne", "Concarneau"];
export default function Showcase(): ReactElement {
const id: string = useId();
const remarkInput: RefObject<HTMLInputElement | null> = useRef<HTMLInputElement | null>(null);
const [callSign, setCallSign]: [string, Dispatch<SetStateAction<string>>] = useState<string>("PD7AL-");
const [mmsi, setMmsi]: [string, Dispatch<SetStateAction<string>>] = useState<string>("24412345");
const [remark, setRemark]: [string, Dispatch<SetStateAction<string>>] = useState<string>(
"Second reef in the main off Pointe Saint-Mathieu as the wind rose to 24 kn from the southwest",
);
const [logged, setLogged]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
const callSignError: string | undefined = CALL_SIGN.test(callSign)
? undefined
: "A call sign has only letters and digits, such as PD7AL.";
return (
<div
style={{
width: "100%",
maxWidth: 600,
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(170px, 1fr))",
gap: "var(--space-md)",
}}
>
<FormField label="Vessel">
<Input defaultValue="SV Althea" autoComplete="off" />
</FormField>
<FormField label="Call sign" error={callSignError}>
<Input
value={callSign}
autoCapitalize="characters"
spellCheck={false}
maxLength={7}
onChange={(event: ChangeEvent<HTMLInputElement>): void => {
setCallSign(event.target.value.toUpperCase());
}}
/>
</FormField>
<FormField label="MMSI" htmlFor={`${id}-mmsi`}>
<MmsiInput id={`${id}-mmsi`} value={mmsi} onValueChange={setMmsi} hint="Nine digits, as the VHF shows it." />
</FormField>
<FormField label="Time" hint="In UTC, as the log is kept.">
<Input type="time" defaultValue="02:20" icon={<Clock />} />
</FormField>
<FormField label="Barometer">
<Input type="number" inputMode="decimal" step={0.1} min={900} max={1100} defaultValue="1004.2" unit="hPa" />
</FormField>
<FormField label="Next harbor">
<Input
type="search"
list={`${id}-harbors`}
placeholder="Search harbors"
defaultValue="Camaret-sur-Mer"
icon={<Search />}
/>
<datalist id={`${id}-harbors`}>
{HARBORS.map((harbor: string): ReactElement => (
<option key={harbor} value={harbor} />
))}
</datalist>
</FormField>
<div style={{ gridColumn: "1 / -1" }}>
<FormField label="Remark" required hint={`${String(remark.length)} of ${String(REMARK_LIMIT)} characters`}>
<Input
ref={remarkInput}
placeholder="What happened on watch, in one sentence"
maxLength={REMARK_LIMIT}
value={remark}
onChange={(event: ChangeEvent<HTMLInputElement>): void => {
setRemark(event.target.value);
}}
/>
</FormField>
</div>
<FormField label="Position" htmlFor={`${id}-position`}>
<PositionInput id={`${id}-position`} readOnly value={{ latitude: 48.22367, longitude: -4.49783 }} />
</FormField>
<FormField label="Engine hours" hint="The engine is off; the hours fill in when it runs.">
<Input disabled value="1,284.6" unit="hr" />
</FormField>
<div style={{ ...ROW, alignSelf: "end", flexWrap: "wrap" }}>
<Button
variant="primary"
onClick={(): void => {
if (remark.trim() === "") {
remarkInput.current?.focus();
return;
}
setLogged((count: number): number => count + 1);
setRemark("");
}}
>
<NotebookPen size={14} aria-hidden="true" />
Log entry
</Button>
{logged > 0 && <Badge variant="green">{String(logged)} logged</Badge>}
</div>
</div>
);
}
An input is a native input element in the kit's style. It takes every attribute of one, so type, value,
placeholder and onChange work as they do on the element. An input has no visible label of its own: put it in a
form field, whose label names it and whose hint and error it reads with its value, or
name it with aria-label. It is for a single field outside a form, such as a search, a filter or a setting saved on its
own; a field of a form is an outlined field. unit sits inside its right edge and icon inside its left edge; a press on either lands
on the input.
An input is 48 px tall, the touch target, on a phone or a tablet (a screen up to the 68.75em breakpoint) or under a coarse pointer, and 44 px on a desktop with a fine pointer, as a button is. The combo box, the schema form's fields and a setting's field are inputs and follow it; a textarea in the input's style keeps the height of its rows.
Examples
Text
Code
import { Input } from "@bosunkit/components/input";
import type { ReactElement } from "react";
export const description: string = "A text input takes the attributes of a native input, here a URL type and a placeholder.";
export default function Text(): ReactElement {
return (
<Input
aria-label="Podcast feed URL"
type="url"
defaultValue="https://example.org/feed.xml"
placeholder="https://…/feed.xml"
/>
);
}
With a unit
unit takes the unit as SN.1/Circ.243 writes it (m, kn, °T, hPa). It sits inside the right edge, the text
stops before it, and it is read after the value through aria-describedby, so the field's name need not repeat it.
Code
import { FormField } from "@bosunkit/components/form-field";
import { Input } from "@bosunkit/components/input";
import type { ReactElement } from "react";
export const description: string =
"Units inside the right edge, as SN.1/Circ.243 writes them, read after the value so the name need not repeat them.";
export default function WithUnit(): ReactElement {
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))",
gap: "var(--space-md)",
width: "100%",
}}
>
<FormField label="Depth alarm">
<Input type="number" inputMode="decimal" step={0.1} defaultValue="3.0" unit="m" />
</FormField>
<FormField label="Speed limit">
<Input type="number" inputMode="decimal" step={0.1} defaultValue="6.0" unit="kn" />
</FormField>
<FormField label="Course to steer">
<Input type="number" inputMode="numeric" min={0} max={359} defaultValue="218" unit="°T" />
</FormField>
</div>
);
}
With an icon
icon sits inside the left edge, before the text. It is decorative and hidden from assistive technology, so the
input's name says what the field is for.
Code
import { Input } from "@bosunkit/components/input";
import { Search } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"A search field with a magnifier inside its left edge, hidden from assistive technology.";
export default function WithIcon(): ReactElement {
return <Input type="search" aria-label="Search the logbook" placeholder="Search the logbook" icon={<Search />} />;
}
Invalid
An input marked aria-invalid draws a dashed neutral border. Keyboard focus adds a separate outer ring, so the
invalid indication stays visible. A form field marks its input and associates the correction message with it.
Code
import { FormField } from "@bosunkit/components/form-field";
import { Input } from "@bosunkit/components/input";
import type { ReactElement } from "react";
export const description: string =
"An input whose field shows an error is marked aria-invalid and draws a dashed neutral border, with a separate focus ring.";
export default function Invalid(): ReactElement {
return (
<FormField label="Swing radius" error="The anchor watch takes a swing radius from 20 to 200 m; 350 m was entered.">
<Input type="number" inputMode="decimal" defaultValue="350" unit="m" />
</FormField>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | undefined | – | Additional input classes. |
| ref | Ref<HTMLInputElement> | undefined | – | The input element. |
| icon | ReactNode | undefined | – | An icon inside the field's left edge, before the text, such as a magnifier on a search field. It is decorative: the field's label says what it is for. |
| unit | string | undefined | – | The unit of the value, inside the field's right edge (m, kn, °T), as SN.1/Circ.243 writes it. It is read
after the value, through aria-describedby, so the field's name need not repeat it. |
| onValueChange | ((value: string, eventDetails: Input.ChangeEventDetails) => void) | undefined | – | Callback fired when the value changes. Use when controlled. |
| defaultValue | Field.Control.Props['defaultValue'] | undefined | – | The default value of the input. Use when uncontrolled. |
| value | React.ComponentProps<'input'>['value'] | undefined | – | The value of the input. Use when controlled. |
| render | React.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined | – | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. |
| style | React.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined | – | Style applied to the element, or a function that returns a style object based on the component's state. |
Also takes every attribute of Omit<Primitive.Props, "className">.
Related
- Form field: the label and error around an input.
- Accessibility: target sizes and accessible names.