Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Inputs

Switch

Inputs

Switch React component for Tailwind CSS v4

SliderTag Input
Live props
Field
label
description
error
required
invalid
disabled
readOnly
State
defaultChecked
loading
Content
errorText
Appearance
size
tone
color
variant
icons
Layout
labelPosition
Behavior
name
Code
import { Switch } from "@/components/ui/switch"

<Switch />
  1. Home
  2. Components
  3. Inputs
  4. Switch
Free

Switch React component for Tailwind CSS v4

v1.0.1·Updated in 1.9.0

A React toggle switch for settings that apply at once: a spring thumb that stretches and drags, thumb icons, and async saves that roll back.

Click the track, its label or the empty space of the row and the thumb springs across while the track fills in the tone. Hold it down and the thumb stretches toward where it is going, then settles as it lands; drag the thumb sideways like on an iPhone and it follows your finger, committing past half way and snapping back otherwise. A vertical swipe on a phone still scrolls the page. Tab reaches the switch with a ring around the track; Space and Enter toggle it, and holding Space stretches the thumb until you let go. When saving is async the switch flips at once and a spinner turns in the thumb; clicks and keys are ignored until it settles, and a failed save flips it back, shakes the row and shows the error under it (press Simulate an error in the playground). Read-only switches keep their value and ignore input, disabled ones dim. Cards take a ring while on and dip slightly when pressed. On a phone every row is at least 44px tall and the whole row is the target; a switch without a label gets a 44px hit area. With reduced motion the thumb jumps, nothing stretches or shakes, and the spinner turns slower.

Switch is a free React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add switch, together with UIProvider, Field, Spinner, useControllableState, Motion presets and 1 more and the npm packages @radix-ui/react-switch and motion.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add switch

Usage

Import Switch from @/components/ui/switch.

import { useState } from "react"
import { Switch } from "@/components/ui/switch"

async function saveSetting(key: string, on: boolean) {
  const res = await fetch("/api/settings", { method: "PATCH", body: JSON.stringify({ [key]: on }) })
  if (!res.ok) throw new Error("save failed")
}

export function NotificationSettings() {
  const [messages, setMessages] = useState(true)

  return (
    <div className="flex flex-col divide-y divide-foreground/10 rounded-2xl bg-surface shadow-raised">
      <Switch
        label="New messages"
        description="When someone writes to you about a listing"
        labelPosition="start"
        checked={messages}
        onCheckedChange={setMessages}
        classNames={{ field: "min-h-14 px-4 py-3" }}
      />
      <Switch
        label="Price offers"
        labelPosition="start"
        defaultChecked
        errorText="Could not save. Try again."
        onCheckedChange={(on) => saveSetting("offers", on)}
        classNames={{ field: "min-h-14 px-4 py-3", error: "px-4 pb-3" }}
      />
    </div>
  )
}

Props

  • labeldefault ""
    ReactNode

    Text beside the track. The whole row is one <label>, so clicking the text, the description or the empty space toggles it. It names the switch through aria-labelledby. Leave it empty for a bare switch (then pass aria-label).

  • descriptiondefault ""
    ReactNode

    A second line under the label, in the muted caption style. It stays out of the switch's name and is linked with aria-describedby, so screen readers read it after the label and the state.

  • errordefault ""
    ReactNode

    A message under the row, lined up with the label, that slides in and marks the switch invalid (red ring, aria-invalid). "", false and null are no error; true only marks it invalid.

  • requireddefault false
    boolean

    A red mark after the label (hidden from screen readers) and required on the switch, so a native form will not submit while it is off.

  • invaliddefault false
    boolean

    The red ring and aria-invalid without a message, for forms that show errors elsewhere.

  • disableddefault false
    boolean

    Dims the row to 50% and blocks every input. A disabled Field or FieldGroup around it disables it too.

  • readOnlydefault false
    boolean

    Shows the value but ignores clicks, keys and drags, with no hover tint and aria-readonly. It stays focusable, so it is still read out.

  • defaultCheckeddefault false
    boolean

    The starting value while uncontrolled. Pass checked and onCheckedChange to control it.

  • loadingdefault false
    boolean

    A spinner turns inside the thumb and clicks, keys and drags are ignored, with aria-busy. For a save you track yourself; an async onCheckedChange does the same on its own.

  • errorTextdefault "Could not save. Try again."
    ReactNode

    Shown under the row when an async onCheckedChange rejects, and announced. It clears on the next change. Press Simulate an error in the playground to see it.

  • sizedefault "md"
    "sm" | "md" | "lg"

    Track 32x18 / 40x24 / 48x28px with a 14 / 20 / 24px thumb, label 13 / 14 / 15px. Rows grow to 44px on touch screens; the track keeps its size.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The track color while on. neutral is black in light mode and white in dark mode, where the thumb turns dark so it still stands out.

  • colordefault ""
    string

    Any CSS color for the track while on. Replaces the tone; the thumb turns black or white by the color's lightness. Leave empty to use the tone.

  • variantdefault "default"
    "default" | "card"

    card turns the row into a soft-filled tile (the checkbox card look) with the switch at the top end; it takes a ring in the tone while on and dips slightly when pressed. Pair it with icon.

  • iconsdefault false
    boolean

    A check inside the thumb while on and an x while off, swapping with a crossfade and a small turn. They stay undistorted while the thumb stretches.

  • labelPositiondefault "end"
    "end" | "start"

    end: the switch first, the text after it. start: the text first and the switch at the end of a full-width row, like an iOS settings list. Cards always put the switch at the end.

  • checked
    boolean

    The controlled value. With an async onCheckedChange the switch shows the value being saved until the promise settles, then follows checked again, so set checked after the save succeeds (or revert it yourself when it fails) for the rollback to show.

  • onCheckedChange
    (checked: boolean) => (void | Promise<unknown>)

    Called with the new value on every click, key or drag. Return a promise to save first: the switch flips at once, spins, ignores input until it settles, and a rejection flips it back, shakes the row and shows errorText. A late result after unmount is ignored.

  • checkedIcon
    ReactNode

    A custom icon inside the thumb while on, like a Moon. Unsized svgs follow the size (10 / 12 / 14px) and take the tone color.

  • uncheckedIcon
    ReactNode

    A custom icon inside the thumb while off, like a Sun.

  • icon
    ReactNode

    Cards: the leading icon tile, which takes the tone's soft color while on. Rows: an icon, thumbnail or avatar before the text, centered on the row.

  • namedefault ""
    string

    Form field name. Inside a <form> Radix renders a hidden checkbox that submits value while the switch is on (nothing while off), and a form reset returns it to defaultChecked. The demo prints what the form would submit.

  • valuedefault "on"
    string

    What the form submits while the switch is on.

  • id
    string

    The switch's id. Inside a Field it reuses the Field's, so the Field's label points at it.

  • classNames
    Partial<Record<"field" | "track" | "thumb" | "icon" | "content" | "label" | "description" | "error", string>>

    Extra classes for the parts: the row or card, the track, the thumb, the icon layer, the text column, the label, the description and the error.

  • className
    string

    Classes for the outermost element: the wrapper around the row and its message, or the track itself when the switch has no text. Override the tone vars here, e.g. [--tone:var(--success)].

  • ref
    RefObject<HTMLButtonElement | null> or (node) => void

    The track: the Radix switch <button role="switch">, which also receives every other prop.

Examples

Async save with rollback

Return a promise: the switch flips at once and spins. Turn on the error and the save fails, it flips back and shakes.

Sizes and tones

32, 40 and 48px tracks; any tone or CSS color for the on state.

Thumb icons

Your own icons (a moon and a sun) or the built-in check and x. They never distort while the thumb stretches.

Cards

A bordered tile with an icon; the ring follows the value and the tile dips when pressed.

Label position, disabled and read-only

end puts the switch first, start at the end of a full-width row. Read-only keeps the value and ignores input.

In a native form

name and value submit through FormData while on, required blocks the submit, and reset puts every switch back.

Without a label, in a list

A bare switch named with aria-label; on touch it still gets a 44px hit area.

Dependencies

@radix-ui/react-switchmotionui-configfieldspinneruse-controllable-statemotiontones

Files

  • components/ui/switch.tsx

FAQ

Is the React Switch component free?

Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.

How do I install Switch?

Run npx wingo-ui@latest add switch. It also adds the Wingo UI items it builds on (UIProvider, Field, Spinner, useControllableState, Motion presets and 1 more) and the npm packages @radix-ui/react-switch and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/switch.json.

How does Switch behave on phones?

A vertical swipe on a phone still scrolls the page. On a phone every row is at least 44px tall and the whole row is the target; a switch without a label gets a 44px hit area.

See also

CheckboxFieldToggleSpinnerToneslib

Used in

Onboarding FlowWorkflow CanvasDeal PipelineFilter panelCSV ImportSchema BuilderCookie ConsentPermissions MatrixRule BuilderAutomation Builder PageInvoice PageKPI Dashboard

and 11 more.

More React input components

Masked InputSegmented ControlDate Range PickerComboboxCountry SelectCron Picker
All 40 React input components

Articles using this component

  • React Controllable State Hook: Code and 3 Pitfalls

    Next.js and Tailwind CSS · Oct 16, 2026 · 10 min read

  • React Settings Page: Section Nav, Save Bar, Danger Zone

    Build it: screens and apps · Oct 16, 2026 · 11 min read

  • Framer Motion Spring Presets: Stiffness and Damping

    Component guides · Oct 13, 2026 · 9 min read

  • Web Vibration API: Haptic Feedback on Android and iPhone

    Mobile UI in React · Oct 9, 2026 · 11 min read

PreviousSliderNext Tag Input

Notifications

1 of 3 notifications on