UIDex

Switch / Toggle

مفتاح التبديل

role="switch"

also calledtoggle، toggle switch، on/off switch، settings toggle، switch control، slider toggle، سويتش، توجل، مفتاح تشغيل وإيقاف، مفتاح الإعدادات، زرّ التفعيل، مفتاح منزلق

A switch is a two-state control, on or off, that applies its change the instant the user presses it, with no save step and no confirmation. It belongs on settings screens and anywhere a choice takes effect immediately: dark mode, notifications, background sync. It is not a checkbox: a checkbox is a value gathered with the rest of a form and sent when you press Save, so there is still room to change your mind before submitting, while a switch leaves nothing to reconsider because the change already landed. It is not a toggle button either: a toggle button carries aria-pressed and reports the state of a tool inside a toolbar, such as Bold in an editor, whereas a switch carries role="switch" with aria-checked and reports a setting that stands on its own. And it is not a pair of radio buttons, which offer one choice out of several named alternatives, where a switch offers only a thing and its opposite. One rule settles the argument in review: if the screen needs an Apply button next to the switch, you did not want a switch, you wanted a checkbox inside a form.

If you called it…

"the little slider in the settings list""the pill with a circle that slides across""the thing that lights up when you turn it on""the on off button that saves itself""like a light switch inside the app"

Live specimen

Interact with the demo. Every part is real and numbered.

الإعدادات

كل مفتاح يُطبَّق لحظة الضغط، ولا يوجد زرّ «حفظ» في هذه الشاشة.

RTLالقرص يسافر نحو النهاية المنطقية، فـ«مُفعّل» عند اليسار في العربية

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a switch for a settings row. Make the interactive element a real button carrying role="switch" and an aria-checked that reflects the value actually applied, never a <div> with an onClick. Associate the label with for/id or put the text and the track inside the button itself so pressing the words works, and keep the state out of the accessible name because aria-checked already announces it. Apply the change on press: no Apply and no Save button anywhere on the screen; if the operation is slow, leave the thumb where the user put it, add aria-busy="true" with a spinner, and only move it back on failure together with an explicit error message. Do not lean on colour alone: add two aria-hidden glyphs inside the track, or a visible state word beside the control. Guarantee a 24x24 CSS pixel touch target with padding rather than by enlarging the capsule. Use logical properties throughout (inset-inline-start, margin-inline, padding-inline) and never write left, right, or margin-left. Drive the thumb with transform: translateX(calc(var(--travel) * var(--dir))) and set --dir to -1 under [dir="rtl"] so the ON position lands at the left of the track in Arabic, animate the track fill with inline-size instead of scaleX, and never mirror the checkmark or the spinner when the page flips.

How it behaves right-to-left

This section is ours alone.

Which way the thumb travels

mirrors

The thumb always travels toward the logical end of the track, which means the ON position sits on the LEFT in Arabic after sitting on the right in English. The problem is that transform is a purely physical property with no logical counterpart anywhere in CSS, so translateX(22px) shoves the thumb clean out of the track in RTL. Write it as translateX(calc(22px * var(--dir))) with --dir set to -1 under [dir="rtl"], or drop the translation altogether by making the track a flex container and swapping justify-content between flex-start and flex-end, since the inline axis follows direction on its own. That version cannot be animated, so use the direction variable when you want a smooth transition.

How the track fills

mirrors

A track that fills with colour behind the thumb has to grow from the logical start, which in Arabic is the right. Build the fill as an inner element pinned with inset-inline-start: 0 and animate its inline-size from 0 to 100%: it mirrors on its own, because the edge it is anchored to is the one that moved. transform: scaleX() will not do here, because transform-origin accepts no logical keyword at all, on the horizontal axis it takes left, right, center, a length or a percentage and nothing else , so you end up hand-writing transform-origin: right under [dir="rtl"]. Same for a two-tone track painted with linear-gradient(to right, …): gradients have no logical keywords either, so flip it to to left.

Where the label sits

mirrors

In a settings row the label sits at the logical start and the control at the logical end, so the whole row swaps in Arabic: text on the right, switch on the left. Use a flex row with justify-content: space-between, or margin-inline-start: auto on the control. Never float: right and never margin-left: auto. And do not add flex-direction: row-reverse under [dir="rtl"] thinking you are fixing the direction: the inline axis already flipped once, the second flip puts the control back on the wrong side and leaves the visual order disagreeing with DOM order and with Tab order.

The glyphs inside the track

never mirrors

The two glyph positions swap with the track by themselves, because each one is pinned to a logical edge, but the glyphs must not be flipped: the Bidi_Mirrored property of the check character is No, and its meaning does not change with page direction. A blanket [dir="rtl"] svg { transform: scaleX(-1) } therefore breaks the checkmark, and breaks the spinner inside the thumb along with it, leaving both looking drawn back to front. Scope the flip to a class you put on arrows only.

The drag gesture

mirrors

If your switch can be dragged instead of tapped, then in Arabic it is a drag toward the LEFT that means "turn it on". Pointer events hand you a purely physical delta: movementX (or current clientX minus the start) is positive for rightward motion whatever the page direction, so a plain dx > threshold inverts the whole gesture in Arabic. Multiply the delta by a direction sign read from getComputedStyle(el).direction === "rtl" ? -1 : 1 before comparing, and put touch-action: pan-y on the track so page scrolling does not swallow the gesture.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIArole="switch" + aria-checkedThe pair that defines the control; it takes true or false only, never mixed.
HTML<input type="checkbox" switch>The native attribute started in Safari and browser support is still uneven; check it before relying on it, and otherwise stay on role="switch".
Radix UI<Switch.Root> + <Switch.Thumb>Renders a real button with role="switch", and onCheckedChange hands you the new value instead of an event; this is what shadcn/ui wraps.
Material UI<FormControlLabel control={<Switch />}>The wrapper is what makes the text itself clickable; a bare <Switch /> leaves the label unattached.
Ant Design<Switch loading checked />Ships a built-in pending state, which is exactly the one hand-rolled versions forget.
SwiftUIToggle("…", isOn: $isOn)Apple calls it a Toggle, not a switch; the capsule look comes from .toggleStyle(.switch).
CSStranslateX(calc(var(--travel) * var(--dir)))transform has no logical equivalent, so a direction variable is what mirrors the travel.

See also

Updated · 2026-08-19