react-theme-toggle-bhavi
v1.0.0
Published
Zero-config dark mode toggle for React apps - Simple, lightweight, and customizable
Maintainers
Readme
react-theme-toggle-bhavi
A production-ready React library for implementing dark mode toggle functionality with zero configuration required. Lightweight, fully typed, and extensively customizable.
Overview
react-theme-toggle-bhavi provides an elegant solution for adding theme switching capabilities to React applications. It handles theme persistence, system preference detection, and provides a beautiful default UI component while remaining completely optional.
Key Statistics:
- Bundle Size: < 5KB (minified + gzipped)
- Zero Runtime Dependencies: Only requires React 18+
- Full TypeScript Support: Complete type safety
- Browser Support: All modern browsers (Chrome, Firefox, Safari, Edge)
✨ Features
- Zero Configuration Required - Works immediately after installation with sensible defaults
- Lightweight & Performant - Minimal bundle impact with optimized animations
- Multiple Theme Modes - Support for light, dark, auto (time-based), and system preference modes
- Persistent Storage - Automatically saves user theme preference to localStorage
- Flexible Implementation - Use included UI component or build your own with the hook
- Full TypeScript Support - Complete type definitions for a seamless development experience
- CSS Utility Classes - Pre-built classes for styling elements based on theme
- System Preference Detection - Respects OS-level dark mode settings
- Customizable Styling - Fine-grained control over button and panel appearance
- No External Dependencies - Pure React implementation with no additional libraries required
📋 Requirements
- React: 18.0.0 or higher
- React DOM: 18.0.0 or higher
- Node.js: 14.0.0 or higher
- Browsers: Chrome, Firefox, Safari, Edge (latest 2 versions)
📦 Installation
Install the package using your preferred package manager:
# npm
npm install react-theme-toggle-bhavi
# yarn
yarn add react-theme-toggle-bhavi
# pnpm
pnpm add react-theme-toggle-bhaviThat's it! CSS is automatically imported when you use the ThemeToggle component.
🚀 Getting Started
Basic Setup (Recommended)
Wrap your application with ThemeProvider and use the built-in ThemeToggle component:
import React from "react";
import { ThemeProvider, ThemeToggle } from "react-theme-toggle-bhavi";
export default function App() {
return (
<ThemeProvider defaultMode="light">
<Header />
<MainContent />
<Footer />
</ThemeProvider>
);
}
function Header() {
return (
<header
className="dark-bg dark-border"
style={{ padding: "1rem", borderBottom: "1px solid #ccc" }}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<h1
className="dark-text"
style={{ fontSize: "1.5rem", fontWeight: "bold" }}
>
My Application
</h1>
<ThemeToggle />
</div>
</header>
);
}Advanced: Custom Theme Toggle
For complete control over the UI, use the useTheme hook to build your own toggle:
import React from "react";
import { ThemeProvider, useTheme } from "react-theme-toggle-bhavi";
export default function App() {
return (
<ThemeProvider defaultMode="system">
<Content />
</ThemeProvider>
);
}
function Content() {
const { theme, mode, setMode } = useTheme();
return (
<div
className="dark-bg dark-text"
style={{ padding: "2rem", minHeight: "100vh" }}
>
<button
onClick={() => setMode(theme === "dark" ? "light" : "dark")}
style={{
padding: "0.5rem 1.5rem",
borderRadius: "0.5rem",
fontWeight: "bold",
border: "none",
cursor: "pointer",
backgroundColor: theme === "dark" ? "#fbbf24" : "#1f2937",
color: theme === "dark" ? "#111827" : "#ffffff",
}}
>
{theme === "dark" ? "☀️ Light Mode" : "🌙 Dark Mode"}
</button>
<div
className="dark-border dark-text"
style={{
marginTop: "1.5rem",
padding: "1rem",
border: "1px solid #999",
borderRadius: "0.5rem",
}}
>
<p>
Current mode: <strong>{mode}</strong>
</p>
<p>
Actual theme: <strong>{theme}</strong>
</p>
</div>
</div>
);
}🎨 CSS Utility Classes
Apply theme-aware styling using these utility classes:
| Class | Light Mode | Dark Mode | Example |
| -------------- | ---------- | --------------- | ---------------------------------------------------------------------- |
| .dark-text | Inherits | White (#ffffff) | <p className="dark-text">Text</p> |
| .dark-bg | Inherits | Black (#0f172a) | <div className="dark-bg">...</div> |
| .dark-border | Gray | Dark gray | <div className="dark-border" style={{border: '1px solid'}}>...</div> |
| .dark-card | White | Dark (#1e293b) | <div className="dark-card">Card</div> |
| .dark-input | White | Dark (#1e293b) | <input className="dark-input" /> |
| .dark-link | Inherit | Blue (#3b82f6) | <a className="dark-link">Link</a> |
Example Usage:
// Text that respects dark mode
<p className="dark-text">This text is white in dark mode</p>
// Background that adapts
<div className="dark-bg" style={{ padding: '1rem', borderRadius: '0.5rem' }}>
Dark background container
</div>
// Combination styling
<div className="dark-card dark-border dark-text" style={{ border: '1px solid', padding: '1.5rem', borderRadius: '0.5rem' }}>
<h2>Styled Component</h2>
<p>This component is fully theme-aware</p>
</div>🚫 Excluding Elements from Dark Mode
Sometimes you want certain elements to ignore the theme. Use these opt-out classes:
| Class | Purpose | Example |
| --------------- | ---------------------------------------------- | ------------------------------------------------- |
| .no-theme | Complete opt-out - element stays in light mode | <div className="no-theme">Always light</div> |
| .theme-ignore | Child elements not affected | <div className="theme-ignore">Content</div> |
| .theme-light | Force light mode | <div className="theme-light">Forced light</div> |
| .theme-dark | Force dark mode | <div className="theme-dark">Forced dark</div> |
Example:
<div>
<p className="dark-text">This respects theme</p>
<p className="no-theme dark-text">This is always light mode</p>
<p className="theme-dark">This is always dark mode</p>
</div>🎬 Circle Expand Animation
The ThemeToggle component includes a smooth circle expand animation when switching themes.
Features:
- Triggers automatically on theme change (light ↔ dark)
- Expands from the toggle button position
- Smooth 800ms duration animation
- Automatically adapts to current theme colors (white for dark, dark for light)
- GPU-accelerated for performance
How it works:
- User clicks theme option
- Circle overlay appears at button center
- Expands outward covering entire screen
- Theme updates during expansion
- Circle fades out
Customization:
To disable the animation, add this to your CSS:
.theme-circle-overlay {
display: none !important;
}To customize the animation speed, override the CSS variable:
:root {
--theme-animation-duration: 600ms;
}
.theme-circle-overlay {
animation-duration: var(--theme-animation-duration);
}🎨 Theme Modes
The library supports four distinct theme modes, each serving different use cases:
| Mode | Description | Use Case |
| -------------- | --------------------------------------------------- | ------------------------------------------- |
| 'light' | Always displays light theme | Daytime-only applications |
| 'dark' | Always displays dark theme | Nighttime or dark-mode-only apps |
| 'system' | Respects OS-level preference | Enterprise applications with OS integration |
| 'auto' | Switches based on time of day (6 AM - 6 PM = light) | Applications that adapt to user schedule |
📚 API Reference
<ThemeProvider />
The root component that provides theme context to your application.
Props:
interface ThemeProviderProps {
children: ReactNode;
defaultMode?: "light" | "dark" | "system" | "auto";
slideDirection?: "left" | "right";
customStyles?: ThemeStyles;
debug?: boolean;
}| Prop | Type | Default | Description |
| ---------------- | ----------- | ------------ | ------------------------------------ |
| children | ReactNode | Required | Your application components |
| defaultMode | string | 'light' | Initial theme mode on first load |
| slideDirection | string | 'left' | Direction of panel slide animation |
| customStyles | object | undefined | Custom styling configuration |
| debug | boolean | false | Enable console logging for debugging |
Example:
<ThemeProvider defaultMode="system" slideDirection="right" debug={true}>
<App />
</ThemeProvider><ThemeToggle />
Pre-built toggle button component with animation and dropdown menu.
Features:
- Built-in dropdown for theme selection
- Smooth circle expansion animation on theme change
- Responsive design (adapts to mobile and desktop)
- Customizable styling through context
- Click-outside handling for menu closure
Example:
import { ThemeToggle } from "react-theme-toggle-bhavi";
export function Header() {
return (
<header style={{ position: "sticky", top: 0 }}>
<ThemeToggle />
</header>
);
}useTheme()
Hook to access theme state and utilities. Must be used within a ThemeProvider.
Returns:
interface ThemeContextType {
theme: "light" | "dark";
mode: "light" | "dark" | "system" | "auto";
setMode: (mode: ThemeMode) => void;
slideDirection: "left" | "right";
customStyles?: ThemeStyles;
}| Property | Type | Description |
| ---------------- | ------------------- | -------------------------------------------- |
| theme | 'light' \| 'dark' | Current active theme (computed from mode) |
| mode | string | Current mode setting from user/system |
| setMode | function | Function to programmatically change the mode |
| slideDirection | string | Direction of animations |
| customStyles | object | Custom styling configuration |
Example:
import { useTheme } from "react-theme-toggle-bhavi";
function ThemeSwitcher() {
const { theme, mode, setMode } = useTheme();
return (
<div>
<p>Current theme: {theme}</p>
<p>Current mode: {mode}</p>
<button onClick={() => setMode("dark")}>Switch to Dark</button>
<button onClick={() => setMode("system")}>Use System Preference</button>
</div>
);
}Error Handling:
If useTheme() is called outside of ThemeProvider, it will throw an error:
Error: useTheme must be used within ThemeProviderAlways ensure your components are wrapped with ThemeProvider at a parent level.
💡 Practical Examples
Example 1: Complete Blog Application
A production-ready blog application with theme support:
import React from "react";
import { ThemeProvider, ThemeToggle, useTheme } from "react-theme-toggle-bhavi";
function BlogApp() {
return (
<ThemeProvider defaultMode="system">
<Header />
<ArticleList />
<Footer />
</ThemeProvider>
);
}
function Header() {
return (
<header
className="dark-bg dark-border"
style={{
borderBottom: "1px solid #ccc",
padding: "1rem",
position: "sticky",
top: 0,
}}
>
<div
style={{
maxWidth: "90rem",
margin: "0 auto",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<h1
className="dark-text"
style={{ fontSize: "1.875rem", fontWeight: "bold" }}
>
Tech Blog
</h1>
<ThemeToggle />
</div>
</header>
);
}
function Article({ title, excerpt }) {
return (
<article
className="dark-card dark-border dark-text"
style={{
border: "1px solid #ccc",
borderRadius: "0.5rem",
padding: "1.5rem",
marginBottom: "1rem",
}}
>
<h2
style={{
fontSize: "1.5rem",
fontWeight: "bold",
marginBottom: "0.5rem",
}}
>
{title}
</h2>
<p style={{ marginBottom: "1rem" }}>{excerpt}</p>
<a
href="#"
className="dark-link"
style={{ textDecoration: "none", fontWeight: "600" }}
>
Read More →
</a>
</article>
);
}
function Footer() {
const { theme } = useTheme();
return (
<footer
className="dark-bg dark-border dark-text"
style={{
borderTop: "1px solid #ccc",
marginTop: "3rem",
padding: "2rem",
textAlign: "center",
}}
>
<p>© 2026 Tech Blog. All rights reserved.</p>
<p style={{ fontSize: "0.875rem" }}>Current theme: {theme}</p>
</footer>
);
}Example 2: Dashboard with Theme Preferences
An admin dashboard with per-user theme preferences:
import React, { useState } from "react";
import { ThemeProvider, useTheme } from "react-theme-toggle-bhavi";
function Dashboard() {
return (
<ThemeProvider defaultMode="system">
<DashboardLayout />
</ThemeProvider>
);
}
function DashboardLayout() {
const { theme, mode, setMode } = useTheme();
const [showSettings, setShowSettings] = useState(false);
return (
<div className="dark-bg" style={{ display: "flex", height: "100vh" }}>
{/* Sidebar */}
<aside
className="dark-card dark-border"
style={{
width: "16rem",
borderRight: "1px solid #ccc",
padding: "1.5rem",
}}
>
<h2
className="dark-text"
style={{
fontSize: "1.25rem",
fontWeight: "bold",
marginBottom: "1.5rem",
}}
>
Admin Dashboard
</h2>
<nav
style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}
>
<NavItem href="/dashboard">Overview</NavItem>
<NavItem href="/users">Users</NavItem>
<NavItem href="/analytics">Analytics</NavItem>
<NavItem href="/settings">Settings</NavItem>
</nav>
</aside>
{/* Main Content */}
<main style={{ flex: 1, padding: "2rem" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "2rem",
}}
>
<h1
className="dark-text"
style={{ fontSize: "1.875rem", fontWeight: "bold" }}
>
Welcome Back
</h1>
<div style={{ display: "flex", gap: "1rem" }}>
<button
onClick={() => setShowSettings(!showSettings)}
className="dark-bg dark-text dark-border"
style={{
padding: "0.5rem 1rem",
borderRadius: "0.375rem",
border: "1px solid #999",
cursor: "pointer",
}}
>
⚙️ Theme Settings
</button>
</div>
</div>
{showSettings && <ThemeSettings mode={mode} setMode={setMode} />}
</main>
</div>
);
}
function ThemeSettings({ mode, setMode }) {
return (
<div
className="dark-card dark-border dark-text"
style={{
border: "1px solid #ccc",
borderRadius: "0.5rem",
padding: "1.5rem",
marginBottom: "2rem",
}}
>
<h2
style={{
fontSize: "1.25rem",
fontWeight: "bold",
marginBottom: "1rem",
}}
>
Theme Preferences
</h2>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "1rem",
}}
>
{["light", "dark", "system", "auto"].map((m) => (
<button
key={m}
onClick={() => setMode(m)}
className="dark-border dark-text"
style={{
padding: "1rem",
borderRadius: "0.375rem",
border: mode === m ? "2px solid #3b82f6" : "2px solid #999",
backgroundColor: mode === m ? "inherit" : "transparent",
cursor: "pointer",
fontSize: "1.125rem",
fontWeight: "600",
textTransform: "capitalize",
}}
>
{m}
</button>
))}
</div>
</div>
);
}
function NavItem({ href, children }) {
return (
<a
href={href}
className="dark-text dark-link"
style={{
display: "block",
padding: "0.5rem 1rem",
borderRadius: "0.375rem",
textDecoration: "none",
cursor: "pointer",
}}
>
{children}
</a>
);
}Example 3: Form with Themed Inputs
A contact form that respects the current theme:
import { useTheme } from "react-theme-toggle-bhavi";
function ContactForm() {
const { theme } = useTheme();
const [formData, setFormData] = React.useState({
name: "",
email: "",
message: "",
});
return (
<form
className="dark-card dark-border dark-text"
style={{
border: "1px solid #ccc",
borderRadius: "0.5rem",
padding: "2rem",
maxWidth: "28rem",
margin: "0 auto",
}}
>
<h2
style={{
fontSize: "1.5rem",
fontWeight: "bold",
marginBottom: "1.5rem",
}}
>
Contact Us
</h2>
<div style={{ marginBottom: "1.5rem" }}>
<label
className="dark-text"
style={{
display: "block",
marginBottom: "0.5rem",
fontWeight: "600",
}}
>
Name
</label>
<input
type="text"
className="dark-input dark-border"
placeholder="Your name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
style={{
width: "100%",
padding: "0.5rem 1rem",
border: "1px solid #999",
borderRadius: "0.375rem",
}}
/>
</div>
<div style={{ marginBottom: "1.5rem" }}>
<label
className="dark-text"
style={{
display: "block",
marginBottom: "0.5rem",
fontWeight: "600",
}}
>
Email
</label>
<input
type="email"
className="dark-input dark-border"
placeholder="[email protected]"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
style={{
width: "100%",
padding: "0.5rem 1rem",
border: "1px solid #999",
borderRadius: "0.375rem",
}}
/>
</div>
<div style={{ marginBottom: "1.5rem" }}>
<label
className="dark-text"
style={{
display: "block",
marginBottom: "0.5rem",
fontWeight: "600",
}}
>
Message
</label>
<textarea
className="dark-input dark-border"
placeholder="Your message..."
rows="4"
value={formData.message}
onChange={(e) =>
setFormData({ ...formData, message: e.target.value })
}
style={{
width: "100%",
padding: "0.5rem 1rem",
border: "1px solid #999",
borderRadius: "0.375rem",
}}
></textarea>
</div>
<button
type="submit"
style={{
width: "100%",
padding: "0.5rem",
borderRadius: "0.375rem",
fontWeight: "600",
border: "none",
cursor: "pointer",
backgroundColor: theme === "dark" ? "#2563eb" : "#3b82f6",
color: "#ffffff",
}}
>
Send Message
</button>
</form>
);
}✅ Best Practices
1. Always Wrap with ThemeProvider
Ensure ThemeProvider wraps your entire application:
// ✅ Correct
export default function App() {
return (
<ThemeProvider>
<YourApp />
</ThemeProvider>
);
}
// ❌ Incorrect - won't work
export default function App() {
return <YourApp />;
}
function YourApp() {
const { theme } = useTheme(); // Error: used outside ThemeProvider
}2. Choose the Right Theme Mode
| Scenario | Recommended Mode | Reason |
| ----------------------- | --------------------- | ----------------------- |
| User preference matters | 'system' | Respects OS settings |
| B2B/Enterprise apps | 'system' | Professional standard |
| Time-aware applications | 'auto' | Adapts to user schedule |
| Consistent branding | 'light' or 'dark' | Fixed appearance |
3. Avoid Inline Theme Checks
Instead of repeatedly checking theme, create composable components:
// ❌ Avoid
function Button() {
const { theme } = useTheme();
return (
<button
style={{
backgroundColor: theme === "dark" ? "#0f172a" : "#ffffff",
}}
>
Click me
</button>
);
}
// ✅ Better - use CSS classes
function Button() {
return (
<button
className="dark-bg dark-text"
style={{
padding: "0.5rem 1rem",
borderRadius: "0.375rem",
border: "none",
cursor: "pointer",
}}
>
Click me
</button>
);
}4. Performance Optimization
The library automatically memoizes theme changes. For optimal performance:
- Avoid creating ThemeProvider instances conditionally
- Keep ThemeProvider at the app root level
- Use CSS classes instead of JavaScript theme checks when possible
🔧 TypeScript Support
Full TypeScript definitions are included. For type-safe implementations:
import React from "react";
import type { ThemeMode, ThemeContextType } from "react-theme-toggle-bhavi";
import { ThemeProvider, useTheme } from "react-theme-toggle-bhavi";
// Type-safe component
interface AppProps {
initialMode?: ThemeMode;
}
const App: React.FC<AppProps> = ({ initialMode = "light" }) => {
return (
<ThemeProvider defaultMode={initialMode}>
<Content />
</ThemeProvider>
);
};
// Type-safe hook usage
const Content: React.FC = () => {
const { theme, mode, setMode }: ThemeContextType = useTheme();
const handleModeChange = (newMode: ThemeMode): void => {
setMode(newMode);
};
return (
<div>
<p>Theme: {theme}</p>
<button onClick={() => handleModeChange("dark")}>Use Dark Mode</button>
</div>
);
};
export default App;🔍 Troubleshooting
Issue: "useTheme must be used within ThemeProvider"
Cause: Using useTheme() in a component that's not wrapped by ThemeProvider.
Solution:
// ❌ Wrong
const App = () => {
const { theme } = useTheme();
return <div>{theme}</div>;
};
// ✅ Correct
const App = () => {
return (
<ThemeProvider>
<Content />
</ThemeProvider>
);
};
const Content = () => {
const { theme } = useTheme();
return <div>{theme}</div>;
};Issue: Theme Changes Not Persisting
Cause: localStorage might be disabled or blocked.
Solution:
<ThemeProvider
defaultMode="light"
debug={true} // Enable debug mode to see what's happening
>
<App />
</ThemeProvider>Check browser console for localStorage warnings.
Issue: CSS Classes Not Taking Effect
Cause: CSS file not imported, or selector specificity issues.
Solution:
// Ensure CSS is imported
import "react-theme-toggle-bhavi/styles";
// Check that your classes use the correct names
// ✅ Correct: .dark-text, .dark-bg, .dark-border
// ❌ Wrong: .darkText, .dark_text, .darkmode-textIssue: Performance Degradation with Many Components
Cause: Unnecessary re-renders due to theme context changes.
Solution: Use composition and memoization:
import React, { memo } from "react";
// Memoize components that don't need theme updates
const StaticHeader = memo(() => {
return <header style={{ position: "sticky", top: 0 }}>Header</header>;
});
// Only theme-aware component updates on theme change
const ThemeAwareButton = () => {
const { theme } = useTheme();
return <button>{theme === "dark" ? "☀️" : "🌙"}</button>;
};🌐 Browser Support
| Browser | Version | Support | | ------- | -------- | ---------------- | | Chrome | Latest 2 | ✅ Full | | Firefox | Latest 2 | ✅ Full | | Safari | Latest 2 | ✅ Full | | Edge | Latest 2 | ✅ Full | | IE 11 | Any | ❌ Not supported |
⚡ Performance Metrics
- Bundle Size: ~4.2KB minified + gzipped
- Initial Load: < 1ms
- Theme Switch: < 50ms (includes animation)
- Memory Footprint: < 50KB
- No Additional Dependencies: Only React 18+
🤝 Contributing
We welcome contributions! To contribute:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes and commit (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
# Clone and setup
git clone https://github.com/Bhaviraj2004/react-theme-toggle
cd react-theme-toggle
npm install
# Development
npm run dev
# Build for production
npm run build
# Run tests (if available)
npm testCode Standards
- TypeScript strict mode enabled
- ESLint for code quality
- Proper error handling and logging
- Unit tests for new features
📄 License
MIT © Bhavi
You are free to use this package in personal, educational, and commercial projects.
📞 Support & Resources
- GitHub Issues: Report bugs and request features
- Discussions: Ask questions and share ideas
- Documentation: Complete API reference in this README
- Examples: See the examples directory for more use cases
