From 8fa4d61b4e89bb5f4052e5277e2fbc0255e7dc27 Mon Sep 17 00:00:00 2001 From: sHa Date: Wed, 19 Mar 2025 15:42:10 +0200 Subject: [PATCH] initial commit --- README.md | 115 +++++++++++ index.html | 95 +++++++++ moon.svg | 5 + script.js | 138 +++++++++++++ styles.css | 555 +++++++++++++++++++++++++++++++++++++++++++++++++++++ sun.svg | 5 + system.svg | 11 ++ 7 files changed, 924 insertions(+) create mode 100644 README.md create mode 100644 index.html create mode 100644 moon.svg create mode 100644 script.js create mode 100644 styles.css create mode 100644 sun.svg create mode 100644 system.svg diff --git a/README.md b/README.md new file mode 100644 index 0000000..c855209 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Theme Switcher Demo + +A lightweight, elegant theme switcher for web applications that allows users to toggle between light, dark, and system themes, along with custom accent colors. + +## Features + +- **Multiple theme options**: + - Light mode + - Dark mode + - System preference detection +- **Custom accent color** selection +- **Persistent settings** using localStorage +- **Responsive design** that works on all screen sizes +- **No flash of unstyled content** when loading in dark mode +- **Modern UI** with accessible controls +- **Smooth transitions** between themes + +## Demo + +The project includes a demo page with various UI elements to showcase the theme switching capabilities: +- Text elements (headings, paragraphs) +- Cards with different backgrounds +- Buttons +- Form elements +- Footer + +## How It Works + +### Theme Switching + +The theme switcher allows users to select between three theme options: +- **Light**: Forces light theme regardless of system settings +- **Dark**: Forces dark theme regardless of system settings +- **System**: Automatically follows the user's system preference + +### Accent Color + +Users can customize the accent color used throughout the interface: +1. Click on the color preview in the theme menu +2. Select a color using the color picker +3. The accent color is applied across buttons, headings, and UI accents + +### Persistence + +User preferences are stored in the browser's localStorage, so theme settings persist between visits. + +## Usage + +1. Include the CSS and JavaScript files in your project +2. Add the theme toggle HTML markup +3. Initialize the theme switcher with JavaScript + +```html + + +``` + +## Implementation Details + +### Preventing Flash + +To prevent flash of unstyled content when loading in dark mode, the project includes an inline script in the head that sets the theme before the page renders: + +```html + +``` + +### CSS Variables + +The theme switcher uses CSS custom properties (variables) to define theme colors: + +```css +:root { + --background-color: #ffffff; + --text-color: #333333; + --accent-color: #4a90e2; + /* more variables... */ +} + +[data-theme="dark"] { + --background-color: #1a1a1a; + --text-color: #f5f5f5; + /* more variables... */ +} +``` + +## Browser Support + +This theme switcher works in all modern browsers that support: +- CSS Custom Properties (variables) +- localStorage API +- matchMedia API + +## License + +This project is available for use under the MIT License. + +## Author + +Created by sHa. + +## Getting Started + +1. Clone the repository +2. Open index.html in your browser +3. Try switching between themes and selecting custom accent colors diff --git a/index.html b/index.html new file mode 100644 index 0000000..889677d --- /dev/null +++ b/index.html @@ -0,0 +1,95 @@ + + + + + + Theme Switcher Demo + + + + +
+

Theme Switcher Demo

+
+ + +
+ + + +
+
+
+ +
+
+

Welcome to the Theme Switcher

+

This is a demonstration of a theme switcher that allows you to choose between light, dark, or system theme preferences.

+

Your theme preference will be stored in local storage and remembered when you return.

+
+ +
+

Demo Elements

+ +
+

Sample Card

+

This is a sample card element to demonstrate theming capabilities.

+ +
+ +
+

Sample Form

+
+ + +
+
+ + +
+ +
+
+
+ + + + + + diff --git a/moon.svg b/moon.svg new file mode 100644 index 0000000..ea55850 --- /dev/null +++ b/moon.svg @@ -0,0 +1,5 @@ + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..093cb5c --- /dev/null +++ b/script.js @@ -0,0 +1,138 @@ +document.addEventListener('DOMContentLoaded', () => { + const themeMenuToggle = document.getElementById('theme-menu-toggle'); + const toggleIcon = document.getElementById('toggle-icon'); + const themeMenu = document.getElementById('theme-menu'); + const themeButtons = document.querySelectorAll('.theme-button'); + const accentColorInput = document.getElementById('accent-color'); + const accentColorPreview = document.getElementById('accent-color-preview'); + + console.log('Theme switcher initialized'); + + // Function to update toggle icon based on theme + const updateToggleIcon = (theme) => { + const iconPath = theme === 'light' ? 'sun.svg' : + theme === 'dark' ? 'moon.svg' : 'system.svg'; + toggleIcon.src = iconPath; + console.log(`Updated toggle icon to: ${iconPath}`); + }; + + // Function to set theme + const setTheme = (theme) => { + console.log(`Setting theme preference to: ${theme}`); + let appliedTheme; + + if (theme === 'system') { + // Check system preference + const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches; + appliedTheme = prefersDarkScheme ? 'dark' : 'light'; + console.log(`System preference detected: ${appliedTheme}`); + } else { + // Set specific theme + appliedTheme = theme; + } + + // Apply the determined theme + document.documentElement.setAttribute('data-theme', appliedTheme); + + // Update toggle icon to reflect current theme + updateToggleIcon(theme); + + // Update active state in button group + themeButtons.forEach(button => { + if (button.getAttribute('data-theme') === theme) { + button.classList.add('active'); + + // Apply accent color dynamically to active button + button.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--accent-color'); + } else { + button.classList.remove('active'); + button.style.backgroundColor = ''; + } + }); + + console.log(`Applied theme: ${appliedTheme}`); + + // Save to localStorage + localStorage.setItem('preferred-theme', theme); + }; + + // Function to set accent color + const setAccentColor = (color) => { + console.log(`Setting accent color to: ${color}`); + document.documentElement.style.setProperty('--accent-color', color); + + // Convert hex to RGB for the accent-color-rgb variable + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + document.documentElement.style.setProperty('--accent-color-rgb', `${r}, ${g}, ${b}`); + + // Update active button with new accent color + const activeButton = document.querySelector('.theme-button.active'); + if (activeButton) { + activeButton.style.backgroundColor = color; + } + + accentColorInput.value = color; + accentColorPreview.style.backgroundColor = color; + localStorage.setItem('accent-color', color); + }; + + // Toggle theme menu + themeMenuToggle.addEventListener('click', (e) => { + themeMenu.classList.toggle('active'); + e.stopPropagation(); + }); + + // Close menu when clicking outside + document.addEventListener('click', (e) => { + if (!themeMenu.contains(e.target) && e.target !== themeMenuToggle) { + themeMenu.classList.remove('active'); + } + }); + + // Theme button selection + themeButtons.forEach(button => { + button.addEventListener('click', () => { + const theme = button.getAttribute('data-theme'); + console.log(`Theme changed by user to: ${theme}`); + setTheme(theme); + }); + }); + + // Accent color preview click opens color picker + accentColorPreview.addEventListener('click', () => { + accentColorInput.click(); + }); + + // Event listener for accent color change + accentColorInput.addEventListener('change', (e) => { + console.log(`Accent color changed by user to: ${e.target.value}`); + setAccentColor(e.target.value); + }); + + // Load saved theme or use system default + const savedTheme = localStorage.getItem('preferred-theme') || 'system'; + console.log(`Loading saved theme from localStorage: ${savedTheme}`); + setTheme(savedTheme); + + // Load saved accent color or use default + const savedAccentColor = localStorage.getItem('accent-color') || '#4a90e2'; + console.log(`Loading saved accent color from localStorage: ${savedAccentColor}`); + setAccentColor(savedAccentColor); + + // Listen for system theme changes when in "system" mode + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + console.log(`System theme preference changed. Dark mode: ${e.matches}`); + const currentTheme = localStorage.getItem('preferred-theme'); + if (currentTheme === 'system') { + setTheme('system'); + } + }); + + // Enable transitions after initial load (prevents flash of white) + setTimeout(() => { + document.documentElement.classList.add('transitions-enabled'); + console.log('Theme transitions enabled'); + }, 100); +}); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..fa93431 --- /dev/null +++ b/styles.css @@ -0,0 +1,555 @@ +html { + transition: none; +} + +:root { + /* Light theme variables (default) */ + --background-color: #ffffff; + --text-color: #333333; + --text-invert-color: #f5f5f5; + --header-bg: #f5f5f5; + --footer-bg: #f5f5f5; + --card-bg: #ffffff; + --border-color: #dddddd; + --accent-color: #4a90e2; /* Default accent color */ + --accent-color-rgb: 74, 144, 226; /* RGB values of accent color */ + --button-bg: var(--accent-color); + --button-text: #ffffff; + --input-bg: #ffffff; + --input-border: #cccccc; + --form-bg: #f9f9f9; +} + +/* Dark theme variables */ +[data-theme="dark"] { + --background-color: #1a1a1a; + --text-color: #f5f5f5; + --text-invert-color: #33333; + --header-bg: #2c2c2c; + --footer-bg: #2c2c2c; + --card-bg: #2c2c2c; + --border-color: #444444; + --accent-color: #4a90e2; /* Default accent color */ + --accent-color-rgb: 74, 144, 226; /* RGB values of accent color */ + --button-bg: var(--accent-color); + --button-text: #ffffff; + --input-bg: #333333; + --input-border: #555555; + --form-bg: #252525; +} + +/* Add a class to re-enable transitions after page load */ +html.transitions-enabled { + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Global styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Global layout structure - using grid */ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: var(--background-color); + color: var(--text-color); + line-height: 1.6; + transition: background-color 0.3s ease, color 0.3s ease; + display: grid; + grid-template-areas: + "header" + "main" + "footer"; + grid-template-rows: auto 1fr auto; + min-height: 100vh; + margin: 0; +} + +header { + grid-area: header; + background-color: var(--header-bg); + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border-color); + flex-wrap: wrap; + gap: 1rem; +} + +.theme-controls { + position: relative; + display: flex; + align-items: center; +} + +.theme-menu-toggle { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 50%; + width: 40px; + height: 40px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + transition: background-color 0.3s, box-shadow 0.3s; +} + +.theme-menu-toggle:hover { + background-color: var(--background-color); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); +} + +.theme-menu-toggle svg { + fill: var(--accent-color); +} + +.theme-menu-toggle img, +.theme-button img { + width: 24px; + height: 24px; + fill: var(--accent-color); + transition: all 0.2s; +} + +.theme-menu-toggle img, +.theme-button img { + filter: invert(50%) sepia(50%) saturate(1000%) hue-rotate(190deg) brightness(90%) contrast(95%); +} + +.theme-button.active img { + filter: invert(100%); +} + +.theme-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + width: 320px; + padding: 20px; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: opacity 0.3s, transform 0.3s, visibility 0s linear 0.3s; + z-index: 100; +} + +.theme-menu.active { + opacity: 1; + visibility: visible; + transform: translateY(0); + transition: opacity 0.3s, transform 0.3s, visibility 0s; +} + +.menu-section { + margin-bottom: 20px; + width: 100%; +} + +.menu-section:last-child { + margin-bottom: 0; +} + +.menu-section h4 { + margin-bottom: 15px; + font-size: 14px; + opacity: 0.8; +} + +.theme-button-group { + display: flex; + background-color: var(--background-color); + border-radius: 8px; + padding: 4px; + border: 1px solid var(--border-color); + width: 100%; + box-sizing: border-box; + margin-bottom: 8px; +} + +.theme-button { + flex: 1; + padding: 10px 12px; /* Slightly more vertical padding */ + border: none; + background: none; + border-radius: 6px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + color: var(--text-color); + transition: all 0.2s; +} + +.theme-button svg { + width: 18px; + height: 18px; + fill: var(--text-color); + transition: fill 0.2s; +} + +/* Active button uses accent color */ +.theme-button.active { + background-color: var(--accent-color); + color: white; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +/* Override active button styling */ +.theme-button.active { + background-color: var(--accent-color); + color: white; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +.theme-button.active svg { + fill: white; /* White icon when button is active */ +} + +/* Theme-specific icon styling for better visual cues */ +.theme-button[data-theme="light"] svg { + color: #f9a825; /* Sunny yellow for light theme */ +} + +.theme-button[data-theme="dark"] svg { + color: #9fa8da; /* Soft blue for dark theme */ +} + +.theme-button[data-theme="system"] svg { + color: #78909c; /* Neutral blue-grey for system */ +} + +/* Override fill when active */ +.theme-button.active[data-theme="light"] svg, +.theme-button.active[data-theme="dark"] svg, +.theme-button.active[data-theme="system"] svg { + fill: white; +} + +/* Accent Color Styles */ +.accent-color-control { + display: flex; + align-items: center; + gap: 15px; + width: 100%; /* Ensure full width */ +} + +#accent-color { + width: 0; + height: 0; + opacity: 0; + position: absolute; +} + +.accent-color-preview { + width: 30px; + height: 30px; + border-radius: 50%; + background-color: var(--accent-color); + border: 2px solid var(--border-color); + cursor: pointer; + transition: transform 0.2s; +} + +.accent-color-preview:hover { + transform: scale(1.1); +} + +.accent-color-control label { + font-size: 14px; + cursor: pointer; +} + +/* Main content styles - using grid */ +main { + grid-area: main; + max-width: 1200px; + margin: 2rem auto; + padding: 0 2rem; + width: 100%; + display: grid; + grid-template-columns: 1fr; + gap: 2rem; +} + +/* For wider screens, use two columns for content sections */ +@media (min-width: 768px) { + main { + grid-template-columns: repeat(2, 1fr); + } + + /* Make some sections span full width */ + .content { + grid-column: 1 / -1; + } +} + +/* Section styles - using flexbox */ +section { + margin-bottom: 2rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +h1, h2, h3, h4 { + margin-bottom: 1rem; + color: var(--accent-color); +} + +p { + margin-bottom: 1rem; +} + +a { + color: var(--accent-color); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Card styles - using flexbox */ +.card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1.5rem; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Button styles */ +.btn { + background-color: var(--button-bg); + color: var(--button-text); + border: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s; +} + +.btn:hover { + background-color: var(--button-bg); + opacity: 0.9; +} + +/* Demo form - using grid */ +.demo-form { + background-color: var(--form-bg); + padding: 1.5rem; + border-radius: 8px; + border: 1px solid var(--border-color); + display: grid; + grid-template-columns: 1fr; + gap: 1rem; +} + +@media (min-width: 576px) { + .demo-form { + grid-template-columns: repeat(2, 1fr); + } + + .form-group { + grid-column: span 1; + } + + .demo-form h4 { + grid-column: 1 / -1; + } + + .demo-form button { + grid-column: 1 / -1; + justify-self: start; + } +} + +/* Form group styles - using flexbox */ +.form-group { + margin-bottom: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--input-border); + border-radius: 4px; + background-color: var(--input-bg); + color: var(--text-color); +} + +/* Footer styles - using flexbox */ +footer { + grid-area: footer; + background-color: var(--footer-bg); + text-align: center; + padding: 0.5rem 1.5rem; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: center; + align-items: center; +} + +footer p { + margin: 0; + color: var(--text-color); +} + +/* Theme Dropdown Styles */ +.theme-dropdown { + position: relative; + display: inline-block; +} + +.theme-toggle { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 50%; + width: 40px; + height: 40px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + transition: background-color 0.3s, box-shadow 0.3s; +} + +.theme-toggle:hover { + background-color: var(--background-color); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); +} + +.theme-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.theme-icon svg { + fill: var(--text-color); + transition: fill 0.3s; +} + +/* Hide all icons by default */ +.theme-toggle .theme-icon { + display: none; +} + +/* Show only the current theme icon */ +[data-theme="light"] .theme-toggle .light-icon, +.theme-toggle .light-icon { + display: flex; +} + +[data-theme="dark"] .theme-toggle .light-icon { + display: none; +} + +[data-theme="dark"] .theme-toggle .dark-icon { + display: flex; +} + +[data-theme="system"] .theme-toggle .light-icon, +[data-theme="system"] .theme-toggle .dark-icon { + display: none; +} + +[data-theme="system"] .theme-toggle .system-icon { + display: flex; +} + +.theme-button[data-theme="dark"]{ + color: var(--text-invert-color) !important; +} +[data-theme="light"] .theme-button[data-theme="dark"]>svg { + fill: var(--text-invert-color); +} + +.theme-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: opacity 0.3s, transform 0.3s, visibility 0s linear 0.3s; + z-index: 100; + color: var(--text-color) !important; +} + +.theme-dropdown.active .theme-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); + transition: opacity 0.3s, transform 0.3s, visibility 0s; +} + +/* Clean slate for theme option styling */ +.theme-option { + display: flex; + align-items: center; + padding: 10px 15px; + border: none; + background: none; + width: 100%; + text-align: left; + cursor: pointer; + transition: background-color 0.2s; + + /* Force the text color to match the parent document theme */ + color: var(--text-color); +} + +/* Make icons match text color */ +.theme-option .theme-icon svg { + fill: var(--text-color); +} + +/* Hover states */ +.theme-option:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] .theme-option:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +/* Active state styling */ +.theme-option.active { + background-color: var(--accent-color); +} + +.theme-option.active, +.theme-option.active .theme-icon svg { + /* Force white for active option regardless of theme */ + color: white; + fill: white; +} diff --git a/sun.svg b/sun.svg new file mode 100644 index 0000000..2acb0ba --- /dev/null +++ b/sun.svg @@ -0,0 +1,5 @@ + + + + diff --git a/system.svg b/system.svg new file mode 100644 index 0000000..3207688 --- /dev/null +++ b/system.svg @@ -0,0 +1,11 @@ + + + + + + + + +