ToggleButton
Switch for one on/off setting, with an optional label beside it. It takes effect the moment it is clicked, which is what separates it from a checkbox in a form the user submits later.
Use this when / not when
- Use for a setting that applies immediately: a feature switch in a preferences panel, a per-row toggle in a list.
- Not for a choice the user confirms by submitting a form, and not when the label is long
enough to wrap — use
Checkbox, whose label is in the flow and wraps. - Not for one choice out of several — that is
RadioButtonGroup. - Not to lay a setting out with its own label, description and error text; wrap this in
FieldContainerfor that.
Import
import { ToggleButton } from "@onlyoffice/apps-ui-kit/components/toggle-button";
Also exported from the root barrel @onlyoffice/apps-ui-kit.
Needs ThemeProvider from @onlyoffice/apps-ui-kit/providers/theme above it in the tree.
Without it the switch still renders, in the light palette, and ignores the portal accent
colour.
Stories
Default
A single switch with a label, for one setting that takes effect at once; click it to turn it on, and change any other prop live in the Controls panel below.
Checked States
The two positions side by side, so the track colours can be compared; each switch here can be clicked.
- Unchecked — grey track, knob at the start
- Checked — accent-coloured track, knob at the end (
isChecked) - The third switch has no
labeland takes only the width of the track
Disabled States
A setting that cannot be changed right now, shown in both positions so the reader still sees its value (isDisabled):
- Disabled off — faded grey track, greyed label
- Disabled on — faded accent track, greyed label; neither responds to a click
Loading State
A setting whose change is still being saved: the knob pulses until the work is done (isLoading). The switch still takes clicks, so the caller decides whether to ignore them.
- Loading unchecked — the knob pulses at the start of the grey track
- Loading checked — the knob pulses at the end of the accent track
Without Animation
For a list of many switches or a reduced-motion setting: click either switch and the knob jumps to the other end instead of sliding there (noAnimation).
Right To Left
The same switches in a right-to-left interface: the label moves to the left of the switch, and the track is mirrored, so the knob of the off switch sits at the right end and the knob of the on switch at the left. The wrapper carries dir="rtl" for the label's side; the mirrored track comes from the theme's interfaceDirection (the Direction toolbar).
Css Customization
Every overridable variable set on one wrapper -- the variables are listed under CSS variables on this page.
- Off is there for
--toggle-button-off-color; hover it to see--toggle-button-off-hover-color - On is there for
--toggle-button-checked-color - Both labels sit 16px from their switch (
--toggle-button-spacing)
Minimal example
The wrapper with an explicit height is not decoration — see the first behaviour note.
import { useState } from "react";
import { ToggleButton } from "@onlyoffice/apps-ui-kit/components/toggle-button";
export function NotificationSetting() {
const [enabled, setEnabled] = useState(false);
return (
<div style={{ height: 16 }}>
<ToggleButton
label="Email notifications"
isChecked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
/>
</div>
);
}
Props
Inherited from TextProps
Declared by components/text and accepted here too.
Recipes
Loading
isLoading pulses the knob while a request is in flight. It does not stop the user clicking
again, so hold the control disabled yourself if a second click would be wrong.
import { useState } from "react";
import { ToggleButton } from "@onlyoffice/apps-ui-kit/components/toggle-button";
export function SyncSetting({
save,
}: {
save: (on: boolean) => Promise<void>;
}) {
const [enabled, setEnabled] = useState(false);
const [saving, setSaving] = useState(false);
const change = async (next: boolean) => {
setSaving(true);
try {
await save(next);
setEnabled(next);
} finally {
setSaving(false);
}
};
return (
<div style={{ height: 16 }}>
<ToggleButton
label="Sync with desktop"
isChecked={enabled}
isLoading={saving}
isDisabled={saving}
onChange={(e) => {
void change(e.target.checked);
}}
/>
</div>
);
}
Disabled
import { ToggleButton } from "@onlyoffice/apps-ui-kit/components/toggle-button";
export function ReadOnlySetting({ enabled }: { enabled: boolean }) {
return (
<div style={{ height: 16 }}>
<ToggleButton
label="Managed by your administrator"
isChecked={enabled}
isDisabled
onChange={() => {}}
/>
</div>
);
}
Behaviour the types don't state
- It occupies no space. The outer element is
display: inline-blockwith no size of its own, and everything inside it — the switch and the label — sits in aposition: absolutebox. It paints where it would have been, so it looks right on its own and overlaps whatever follows it in a column. Give it a wrapper with an explicit height, at least the switch's 16px, or more if a label pushes the line taller. id,classNameandstyleare applied twice, to the outer element and again to the inner<label>. Anidtherefore appears twice in the document, which is invalid, and astylewith a margin or a padding is applied at both levels.isLoadingdoes not disable anything. It only animates the knob; the input stays live and a click during it still callsonChange.- The switch is a fixed 28×16 SVG. The gap between it and the label is
--toggle-button-spacing(8px), and the two sit in a two-column grid, so a long label neither wraps nor pushes the switch. - In RTL the SVG is mirrored with
transform: scaleX(-1), which is applied by the.rtlclass the theme provider sets, not by a prop. The off knob then sits on the right, and the label comes before the switch. - The animation comes from
framer-motion, a runtime dependency of this package rather than a peer, so it arrives with the kit whether or not anything else uses it.noAnimationmoves the knob to its new end instantly instead of sliding it; the loading pulse keeps its own duration and still animates. It does not remove the library.
CSS variables
Set these on an ancestor. Everything else the stylesheet defines is private to it — including
the --toggle-button-fill-* names an older version of this README listed, which the stylesheet
assigns to itself and does not read from the outside.
The two off colours apply in the light theme only: the dark theme sets its own track colours on
the control itself, so an ancestor's --toggle-button-off-color and
--toggle-button-off-hover-color are ignored there.
Accessibility
- The control is a real
<input type="checkbox">, visually hidden but focusable and in the tab order, inside a<label>— so a click anywhere on the label toggles it and a screen reader announces a checkbox with its state. - There is no way to give it an accessible name other than
label. The component accepts noaria-*props, and the one it sets itself,aria-checked, is on a plain<div>with no role, where it means nothing. A switch withoutlabelis announced as an unnamed checkbox. - The hidden input carries no focus styling of its own and is the element that receives focus,
so keyboard focus is effectively invisible. Tab reaches the input and Space toggles it. Supply a
:focus-withinoutline on your wrapper if the form is meant to be usable from the keyboard. isDisableddisables the input and setspointer-events: noneon the control, so it is skipped by the tab order as well as by the mouse.
Test ids
Related
Checkbox— for a choice submitted with a form, or a label that has to wrap.RadioButtonGroup— for one choice out of several.FieldContainer— to give the setting a label, a help tooltip and an error message.