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

Buttons & Actions

Theme Toggle

Buttons & Actions

Theme Toggle React component for Tailwind CSS v4

Delete ButtonToggle
Live props
Appearance
variant
size
radius
buttonVariant
tone
color
Behavior
includeSystem
transition
duration500ms
shortcut
responsive
Content
showLabels
Layout
fullWidth
align
State
disabled
defaultOpen
defaultValue
Code
import { ThemeToggle } from "@/components/ui/theme-toggle"

<ThemeToggle />
  1. Home
  2. Components
  3. Buttons & Actions
  4. Theme Toggle
Pro

Theme Toggle React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React theme toggle for light, dark and system mode on next-themes: an animated sun and moon button, a segmented control or a menu.

Press the icon button and the new theme grows over the page in a circle from the button, while the glyph morphs: going dark the sun's rays fold away, the disc swells and a shadow slides across it until a crescent moon is left; going light it runs backwards. The button cycles System, then the theme the device is not showing, then the device's own theme, then System again (a small monitor badge appears in the corner and the page follows the device), so from System the first press always changes what you see. With includeSystem off it flips between light and dark and never shows the badge. Hover or focus shows a tooltip with the current theme and the shortcut. The segmented variant slides its raised thumb to Light, Dark or System; the dropdown opens a menu with the three choices and a check on the current one, as a bottom sheet on phones. Keyboard: Tab to it, Enter or Space toggles the icon button; in the segmented control Left / Right (and Home / End) pick a theme; in the menu Up / Down move, Enter picks, Escape closes and focus returns to the button. A shortcut prop such as shift+t flips light and dark from anywhere. The new theme is announced to screen readers. With reduced motion the glyph crossfades and the page switches at once. On a phone every target is 44px.

Theme Toggle is a Pro React component in the React button components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add theme-toggle, together with Button, Dropdown Menu, Segmented Control, Tooltip, useControllableState and 5 more and the npm packages lucide-react, motion and next-themes.

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 theme-toggle

Theme Toggle is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import ThemeToggle from @/components/ui/theme-toggle.

"use client"

import { ThemeProvider } from "next-themes"
import { ThemeToggle } from "@/components/ui/theme-toggle"

// next-themes needs attribute="class" so the dark: variant and the reveal see the change
export function AppHeader() {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
      <header className="flex h-14 items-center justify-between px-4">
        <span className="font-semibold">Northwind CRM</span>
        <ThemeToggle
          shortcut="shift+t"
        />
      </header>
    </ThemeProvider>
  )
}

Props

  • variantdefault "icon"
    "icon" | "segmented" | "dropdown"

    icon: an animated sun / moon button for headers. segmented: Light / Dark / System side by side for settings pages. dropdown: a button that opens a menu (a sheet on phones).

  • includeSystemdefault true
    boolean

    Offers System, which follows the device. The icon button then cycles system -> the opposite of the device -> the device's theme -> system, so each press but the last visibly changes the page; without it the button flips light <-> dark, and a page on system shows as the theme it resolves to (no badge). Segmented and dropdown show a third option.

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

    32 / 40 / 48px, like the button (md is 44px on touch). The glyph is 16 / 18 / 20px; the segmented track and the menu follow.

  • radiusdefault "full"
    "none" | "sm" | "md" | "lg" | "full"

    Corner shape of the icon button, the dropdown trigger and the segmented track. Scales with the size.

  • buttonVariantdefault "ghost"
    "solid" | "soft" | "outline" | "ghost" | "link"

    The look of the icon button and the dropdown trigger. ghost sits quietly in a header.

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

    Color of the icon button and the dropdown trigger, and the fill of the segmented thumb (neutral is a raised white thumb).

  • colordefault ""
    string

    Any CSS color; replaces the tone. Leave empty to use the tone.

  • showLabelsdefault false
    boolean

    segmented: icons with labels (icons only by default, each with a tooltip). dropdown: the current theme's name and a chevron on the trigger.

  • fullWidthdefault false
    boolean

    segmented: the segments share the full width of the parent, the usual layout for a settings row on a phone.

  • transitiondefault "circle"
    "none" | "fade" | "circle"

    How the new theme appears: a circle growing from the control, a crossfade, or at once. Uses the View Transitions API (Chrome, Edge, Safari 18+), falls back to switching at once, and is off under reduced motion. The file also exports switchThemeWithTransition(apply, { transition?, duration?, origin? }), which runs any theme change inside the same reveal (origin is an element or a { x, y } point) and skips it while another one runs.

  • durationdefault 500
    number

    Length of the reveal in ms.

  • shortcutdefault ""
    string

    A key combo that flips light / dark from anywhere, e.g. "shift+t". Shown in the tooltip and set as aria-keyshortcuts. Empty binds nothing.

  • disableddefault false
    boolean

    Dims the control and blocks it. It is also disabled on pages that force a theme (next-themes forcedTheme).

  • defaultOpendefault false
    boolean

    dropdown: starts with the menu open.

  • aligndefault "end"
    "start" | "center" | "end"

    dropdown: which edge of the menu lines up with the trigger. end suits a toggle at the end of a header.

  • responsivedefault true
    boolean

    dropdown: the menu becomes a bottom sheet below 768px.

  • value
    "light" | "dark" | "system"

    Controlled use without next-themes: the toggle shows this theme and only calls onValueChange. Left out, it reads and writes next-themes' useTheme(). For your own UI, useThemeToggle (see Parts) gives the same logic without the button.

  • defaultValuedefault ""
    "light" | "dark" | "system"

    Uncontrolled use without next-themes: the starting theme. The toggle keeps its own state and reports picks through onValueChange. Left out (with value), it reads and writes next-themes; the playground's empty option means left out.

  • onValueChange
    (theme: ThemeValue) => void

    Fires with every pick, inside the view transition, so the new theme you apply is what the reveal shows.

  • open
    boolean

    dropdown: controls the menu.

  • onOpenChange
    (open: boolean) => void

    dropdown: fires when the menu opens or closes.

  • labels
    { light?, dark?, system?, toggle?, menu?, announceLight?, announceDark?, announceSystem? }

    Every string, merged over DEFAULT_THEME_TOGGLE_LABELS: "Light", "Dark", "System", "Toggle theme" (the icon button's name), "Theme" (group name, tooltip prefix, sheet title) and the screen reader announcements "Light theme", "Dark theme", "System theme".

  • classNames
    Partial<Record<"icon" | "badge" | "label" | "chevron" | "item" | "thumb" | "content", string>>

    Extra classes for inner parts: the glyph, the system badge, the trigger label and chevron, segments or menu rows, the segmented thumb, the menu surface.

  • className
    string

    Extra classes for the root: the button, the segmented track or the dropdown trigger.

Parts

  • useThemeToggle

    hook

    The toggle's logic without its UI, for user menus and settings pages. Reads and writes next-themes, or a value / onValueChange pair, and runs the reveal from an element or a point.

    • value
      "light" | "dark" | "system"

      Controlled theme without next-themes; picks only call onValueChange.

    • defaultValue
      "light" | "dark" | "system"

      Uncontrolled starting theme without next-themes.

    • onValueChange
      (theme: ThemeValue) => void

      Fires with every pick, inside the view transition.

    • transitiondefault "circle"
      "none" | "fade" | "circle"

      How the new theme comes in.

    • durationdefault 500
      number

      Milliseconds of the reveal.

    • returns
      { theme, resolvedTheme, setTheme(theme, origin?), toggle(origin?), mounted, forced }

      theme and resolvedTheme stay undefined until mounted when they come from next-themes; forced is true when the page pins a theme.

  • switchThemeWithTransition

    function

    Runs your theme change inside a view transition, so the new theme grows as a circle from a button or fades in. Falls back to an instant switch without view transitions or with reduced motion.

    • apply*
      () => void

      Applies the theme, e.g. () => setTheme("dark").

    • options.transitiondefault "circle"
      "none" | "fade" | "circle"

      The reveal style.

    • options.durationdefault 500
      number

      Milliseconds of the reveal.

    • options.origin
      Element | { x: number; y: number }

      Where the circle starts; defaults to the top end corner.

  • DEFAULT_THEME_TOGGLE_LABELS

    constant

    The English strings: Light, Dark, System, Toggle theme, Theme and the three screen reader announcements. Spread it to translate.

  • ThemeValue

    type

    The theme a person picks: "light" | "dark" | "system".

Examples

Three variants

An icon button for headers, a segmented control for settings, a menu when space is tight.

Settings row

Segmented with labels in an Appearance row; on a phone it stacks under the text and fills the row (fullWidth).

Light and dark only, brand tone

includeSystem off flips between two themes; ghost, soft and solid brand buttons in three sizes.

Transitions compared

At once, a crossfade, or a circle that grows out of the button.

In a user menu

useThemeToggle wires the theme row of a user menu; the reveal starts where you tapped.

Controlled, without next-themes

value and onValueChange drive a box that flips its own .dark class; the page stays as it is.

Dependencies

lucide-reactmotionnext-themesbuttondropdown-menusegmented-controltooltipuse-controllable-stateuse-media-queryuse-shortcutmotiontonesui-config

Files

  • components/ui/theme-toggle.tsx

FAQ

Is the React Theme Toggle component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Theme Toggle?

Run npx wingo-ui@latest add theme-toggle. It also adds the Wingo UI items it builds on (Button, Dropdown Menu, Segmented Control, Tooltip, useControllableState and 5 more) and the npm packages lucide-react, motion and next-themes. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/theme-toggle.

How does Theme Toggle behave on phones?

dropdown: the menu becomes a bottom sheet below 768px. The responsive prop controls this and is on by default.

See also

Segmented ControlDropdown MenuUser MenuButton

Used in

Admin ShellMarketplace Shell

More React button components

Toggle GroupDelete ButtonToggleButton GroupCopy Button
All 7 React button components

Articles using this component

  • next-themes Hydration Error: Fix the Flash in Next.js

    Next.js and Tailwind CSS · Oct 13, 2026 · 8 min read

  • Next.js Component Library Setup for the App Router

    Next.js and Tailwind CSS · Oct 13, 2026 · 8 min read

  • Tailwind v4 Dark Mode: Class Toggle, Tokens, next-themes

    Next.js and Tailwind CSS · Oct 13, 2026 · 9 min read

PreviousDelete ButtonNext Toggle
Northwind CRM
ClientsInvoicesReports

Hi, Emma

Thursday, Sep 24, 2026 · 3 invoices due today

Synced
Collected in September$48,920.50+12% vs. August
Overdue$7,315.20Greenleaf Pharmacy

theme: …state: next-themesresolved: …reveal: circle 500msshortcut: nonemenu: align end, sheet on phones

The showcase binds t; the demo uses shift+t when you set a shortcut.