Segmented radio group
Two to five options in one row, of which exactly one is chosen, such as the conning screen's rose or chart.
On this page
The compass rose with heading, course over ground and the wind fills the center.
Code
import { SegmentedRadioGroup, type SegmentedRadioOption } from "@bosunkit/components/segmented-radio-group";
import { Compass, Map as MapIcon, Navigation, Ship, SquareSplitHorizontal } from "lucide-react";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"The conning screen's controls: the center view as rose, chart or both, with icons and labels; the chart's " +
"orientation as icons alone, each named in a tooltip; and the radar range in five steps, held by the control.";
const VIEWS: readonly SegmentedRadioOption[] = [
{ value: "rose", label: "Rose", icon: <Compass size="1.25em" /> },
{ value: "chart", label: "Chart", icon: <MapIcon size="1.25em" /> },
{ value: "both", label: "Both", icon: <SquareSplitHorizontal size="1.25em" /> },
];
const ORIENTATIONS: readonly SegmentedRadioOption[] = [
{ value: "north", label: "North up", icon: <Compass size="1.25em" /> },
{ value: "head", label: "Head up", icon: <Ship size="1.25em" /> },
{ value: "course", label: "Course up", icon: <Navigation size="1.25em" /> },
];
const RANGES: readonly SegmentedRadioOption[] = ["0.5", "1.5", "3", "6", "12"].map((range: string): SegmentedRadioOption => ({
value: range,
label: `${range} NM`,
}));
const SHOWN: Readonly<Record<string, string>> = {
rose: "The compass rose with heading, course over ground and the wind fills the center.",
chart: "The chart with own vessel, her track and the AIS targets fills the center.",
both: "The rose and the chart share the center, side by side.",
};
export default function Showcase(): ReactElement {
const [view, setView]: [string, Dispatch<SetStateAction<string>>] = useState<string>("rose");
const [orientation, setOrientation]: [string, Dispatch<SetStateAction<string>>] = useState<string>("head");
return (
<div style={{ display: "grid", gap: "var(--space-lg)", justifyItems: "start" }}>
<SegmentedRadioGroup label="Center view" options={VIEWS} value={view} onValueChange={setView} />
<p style={{ margin: 0, color: "var(--text-dim)" }}>{SHOWN[view]}</p>
<div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-lg)", alignItems: "center" }}>
<SegmentedRadioGroup
label="Chart orientation"
options={ORIENTATIONS}
value={orientation}
onValueChange={setOrientation}
iconOnly
/>
<SegmentedRadioGroup label="Radar range" options={RANGES} defaultValue="3" />
</div>
</div>
);
}
A segmented control chooses one of a few views or settings in place: the conning screen's center as rose or chart,
the chart's orientation, the wind as true or apparent. The options sit in one row inside a frame, and the chosen one
carries a raised mark with an accent line under it. It is a radio group: the group is named by label, each option
is a radio named by its own label, and only the chosen option is in the tab order. The arrow keys, in either axis,
move to the next or previous option and choose it, wrapping at the ends; a disabled option is skipped.
The mark slides to the next choice over the medium motion duration, so the eye follows
it across the row; under reduced motion it moves at once. Every option is at least --target-touch-size tall and
wide. With iconOnly the icons stand alone, each option keeps its label as its name, and the label shows in a
tooltip on hover and focus.
Examples
Labels
Code
import { SegmentedRadioGroup, type SegmentedRadioOption } from "@bosunkit/components/segmented-radio-group";
import type { ReactElement } from "react";
export const description: string =
"Two options as labels: the wind the dial shows, true or apparent. The arrow keys move between them and choose.";
const WIND: readonly SegmentedRadioOption[] = [
{ value: "true", label: "True" },
{ value: "apparent", label: "Apparent" },
];
export default function Labels(): ReactElement {
return <SegmentedRadioGroup label="Wind" options={WIND} defaultValue="apparent" />;
}
Icons alone
Code
import { SegmentedRadioGroup, type SegmentedRadioOption } from "@bosunkit/components/segmented-radio-group";
import { Compass, Navigation, Ship } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"Icons alone, for a toolbar over the chart: each option is named by its label, which shows in a tooltip.";
const ORIENTATIONS: readonly SegmentedRadioOption[] = [
{ value: "north", label: "North up", icon: <Compass size="1.25em" /> },
{ value: "head", label: "Head up", icon: <Ship size="1.25em" /> },
{ value: "course", label: "Course up", icon: <Navigation size="1.25em" /> },
];
export default function IconsOnly(): ReactElement {
return <SegmentedRadioGroup label="Chart orientation" options={ORIENTATIONS} defaultValue="north" iconOnly />;
}
Held by the caller
The control holds its choice itself from defaultValue, or the first option. With value, the caller holds it and
the control reports each choice through onValueChange and shows it only once value changes.
The alarm sounds beyond 35 m from the anchor.
Code
import { SegmentedRadioGroup, type SegmentedRadioOption } from "@bosunkit/components/segmented-radio-group";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"Held by the caller: the screen keeps the anchor watch's alarm radius in its state and shows what it set.";
const RADII: readonly SegmentedRadioOption[] = ["25", "35", "50", "75"].map((radius: string): SegmentedRadioOption => ({
value: radius,
label: `${radius} m`,
}));
export default function HeldByCaller(): ReactElement {
const [radius, setRadius]: [string, Dispatch<SetStateAction<string>>] = useState<string>("35");
return (
<div style={{ display: "grid", gap: "var(--space-sm)" }}>
<SegmentedRadioGroup label="Swing radius" options={RADII} value={radius} onValueChange={setRadius} />
<p style={{ margin: 0, color: "var(--text-dim)" }}>The alarm sounds beyond {radius} m from the anchor.</p>
</div>
);
}
A disabled option
No chart is installed for this area, so the chart is off.
Code
import { SegmentedRadioGroup, type SegmentedRadioOption } from "@bosunkit/components/segmented-radio-group";
import { Compass, Map as MapIcon } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"An option that cannot be chosen, skipped by the arrow keys, with the reason written beside the control.";
const VIEWS: readonly SegmentedRadioOption[] = [
{ value: "rose", label: "Rose", icon: <Compass size="1.25em" /> },
{ value: "chart", label: "Chart", icon: <MapIcon size="1.25em" />, disabled: true },
];
export default function DisabledOption(): ReactElement {
return (
<div style={{ display: "grid", gap: "var(--space-sm)" }}>
<SegmentedRadioGroup label="Center view" options={VIEWS} defaultValue="rose" />
<p style={{ margin: 0, color: "var(--text-dim)" }}>No chart is installed for this area, so the chart is off.</p>
</div>
);
}
Without motion
Code
import { SegmentedRadioGroup, type SegmentedRadioOption } from "@bosunkit/components/segmented-radio-group";
import type { ReactElement } from "react";
export const description: string =
"With motion={false} the chosen option draws its own mark, which moves at once to the next choice.";
const MODES: readonly SegmentedRadioOption[] = [
{ value: "sail", label: "Under sail" },
{ value: "power", label: "Under power" },
{ value: "anchor", label: "At anchor" },
];
export default function WithoutMotion(): ReactElement {
return <SegmentedRadioGroup label="Vessel activity" options={MODES} defaultValue="sail" motion={false} />;
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | undefined | – | Submitted field name. |
| form | string | undefined | – | ID of an owning form outside the component's ancestors. |
| required | boolean | undefined | – | Requires a choice before native form submission. |
| readOnly | boolean | undefined | – | Keeps the value focusable and submitted, but prevents changes. |
| optionsrequired | readonly SegmentedRadioOption[] | – | The choices, 2 to 5, in order. |
| labelrequired | string | – | What the choice is about, read as the group's name ("Center view"); it is not drawn. |
| value | string | undefined | – | The chosen option's value, for a caller that holds the choice. |
| defaultValue | string | undefined | – | The option chosen first, when the control holds the choice itself; the first option when unset. |
| onValueChange | RadioGroupProps["onValueChange"] | – | Called with an option's value when the crew choose it. |
| iconOnly | boolean | undefined | false | Draws each option's icon alone, with its label as its name and in a tooltip. Every option needs an icon. |
| disabled | boolean | undefined | false | Whether no option can be chosen. |
| motion | boolean | undefined | true | Whether the selection slides: on by default. The mark behind the chosen option moves to the next one over the
medium duration; under reduced motion it moves at once. false draws the mark on the chosen option alone. |
| className | string | undefined | – | Classes added to the root's own. |
| testId | string | undefined | – | Becomes data-testid on the root; each option is ${testId}--<value>. |
| ref | Ref<HTMLDivElement> | undefined | – | The root element. |