Dark Mode Toggle In React App
Written by Lucie Zdeňková on 2025-03-27
reactjavascriptcssA few weeks ago, we published implementation of darkmode for static website without any javascript part 1, and part 2. This week, we would like to show you, how this useful feature can be implemented in any React application. Good news is, that coding such feature in React is way easier and requires less code!
App.js
Following code snippet shows the App()
implemetation. Boolean status of
mode preference is stored in state variable const [isDark, setIsDark] =
useState(false);
. We detect the initial dark mode preference by
window.matchMedia("(prefers-color-scheme: dark)").matches
and following
if
confition sets isDark
to true if applicable. The method
mq.addEventListener("change", (evt) => setIsDark(evt.matches));
listens
to the user's preference changes and performs color scheme changes without reload. The
empty dependency array ([])
ensures the effect runs only on mount.
Let's explain the JSX part, the data-bs-theme={isDark ? "dark" :
"light"}
attribute on a wrapper element defines changes accross whole app. The
<Toggle />
component represents the visual knob, more details comes
in next code snippet.
import React, { useState, useEffect } from "react";
import { Navbar, Container } from "react-bootstrap";
function App() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (mq) {
setIsDark(true);
}
mq.addEventListener("change", (evt) => setIsDark(evt.matches));
}, []);
return (
<Container fluid className='App' data-bs-theme={isDark ? "dark" : "light"} >
<Navbar>
<Toggle isChecked={isDark} handleChange={() => setIsDark(!isDark)} />
</Navbar>
</Container >
)
}
The <Toggle />
component requires parametres
handleChange
for changing the state and isChecked
for knowing
the current state. The key feature is hidden input
element with styled
label
element (see CSS part below). Input controls the behaviour and
requires onChange
and checked
attributes. Used icons
<IoSunny />
and <IoIosMoon />
are from React Icons. Different icon sets might
be under different licenses, <IoSunny />
and <IoIosMoon
/>
belongs to io
and io5
sets and are under
MIT license.
export const Toggle = ({ handleChange, isChecked }) => {
return (
<span>
<input
type="checkbox"
id="darkmode-toggle"
onChange={handleChange}
checked={isChecked}
/>
<label htmlFor="darkmode-toggle" className="darkmode-label">
<span className="sun"><IoSunny /></span>
<span className="moon"><IoIosMoon /></span>
</label>
</span>
);
};
style.css
Next part describes all necessary CSS code to implement visual toggle together with color schemes.
Following code defines the color schemes. Fist selector :root
defines
colors and other css properties for the light mode whereas selector
[data-bs-theme="dark"]
defines color and properties for dark mode. We are
using so called CSS varibles, which allows to alter values for different selectors.
:root {
--background-color: #f6f6f6;
--primary-text-color: #0a0a0a;
--gradient-bg1: #ffcc89;
--gradient-bg2: #d8860b;
--label-left: 2px;
}
[data-bs-theme="dark"] {
--background-color: #0a0a0a;
--primary-text-color: #f6f6f6;
--gradient-bg1: #777;
--gradient-bg2: #3a3a3a;
--label-left: 98px;
--transformantion: translateX(-100%);
}
As mentioned above, the principle of the solution is hidden input. This CSS code hides the input visually.
input#darkmode-toggle {
width: 0;
height: 0;
visibility: hidden;
display: none;
}
Following code is styling of the toggle and very much overlaps with css explanation described in Darkmode without javascript part 2.
This section defines the main visual container of the toggle, designed to resemble a
rounded switch. The display: block
property ensures it behaves as a
block-level element, allowing precise control over its width and height. The
position: relative
is essential for correctly positioning the knob in the
next step. The cursor: pointer
signals interactivity, while
border-radius
and box-shadow
shape the toggle with smooth
edges and subtle depth.
label.darkmode-label {
display: block;
position: relative;
width: 100px;
height: 35px;
border-radius: 40px;
box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.4), inset 0px -1px 3px rgba(255, 255, 255, 0.4);
cursor: pointer;
background-color: #242424;
}
The knob is designed to transition smoothly between light and dark modes. With
position: absolute
it is positioned relative to the toggle container
defined in the previous code snippet. The content: ""
property ensures the
:after
pseudo-element is rendered. Horizontal positioning is dynamically
controlled using the CSS variable left: var(--label-left)
, while the
knob’s background is set with a gradient using background:
linear-gradient(180deg, var(--gradient-bg1), var(--gradient-bg2))
. Movement
animations are handled by transform: var(--transformation)
. Other
properties like width, height, border-radius
, and box-shadow
follow the same styling principles as before. Finally, top: 2px
ensures
proper vertical alignment.
label.darkmode-label:after {
content: "";
width: 36px;
height: 31px;
position: absolute;
top: 2px;
left: var(--label-left);
background: linear-gradient(180deg, var(--gradient-bg1), var(--gradient-bg2));
border-radius: 36px;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2);
transform: var(--transformantion);
}
Next code snippet adds smooth animations to the toggle's movements and interactions.
First property transition: 0.3s
ensures a 0.3-second animation for the
toggle changes. Property width: 52px
with selector
label.darkmode-label:active:after
slightly enlarges the knob during
interaction for a tactile feedback effect.
label.darkmode-label,
label.darkmode-label:after {
transition: 0.3s
}
label.darkmode-label:active:after {
width: 52px;
}
Icons styling is simple. The property display: block
ensures it behaves
like a block-level element, position: absolute
places it relative to its
parent .darkmode-label. transition: 0.3s
enables a smooth transition when
changing themes. z-index: 1
ensures it appears above other elements.
Properties top
and left
defines vertical and horizontal
positions.
label.darkmode-label span.sun {
display: block;
position: absolute;
width: 24px;
transition: 0.3s;
z-index: 1;
left: 11px;
top: 3px;
}
label.darkmode-label span.moon {
display: block;
position: absolute;
width: 24px;
transition: 0.3s;
z-index: 1;
left: 72px;
top: 3px;
}
In this post, we built a dark mode toggle in React that respects system preferences
while allowing manual control. Using window.matchMedia("(prefers-color-scheme:
dark)")
, we initialized the theme state and listened for changes efficiently
with useEffect
. We also explained the CSS behind the toggle, including
smooth transitions, dynamic positioning using CSS variables, and the placement of sun
and moon icons for visual feedback. With this approach, the app seamlessly adapts to
user preferences while maintaining a clean and interactive design.
Thank you for reading and see ya next time!