├── .distignore ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── create-release-and-deploy.yml ├── .gitignore ├── .wordpress-org ├── banner-1544x500.png ├── banner-772x250.png ├── icon-128x128.png └── icon-256x256.png ├── README.md ├── dark-mode-toggle-block.php ├── package-lock.json ├── package.json ├── readme.txt └── src ├── block.json ├── deprecated.js ├── edit.js ├── icons.js ├── icons ├── index.js └── library │ ├── circle-dark.js │ ├── circle-light.js │ ├── eye-dark.js │ ├── eye-light.js │ ├── filled-dark.js │ ├── filled-light.js │ ├── stroke-dark.js │ └── stroke-light.js ├── index.js ├── save.js ├── style.scss └── view.js /.distignore: -------------------------------------------------------------------------------- 1 | # Directories 2 | /.git 3 | /.github 4 | /.wordpress-org 5 | /node_modules 6 | /src 7 | 8 | # Files 9 | *.log 10 | *.swp 11 | *.tar.gz 12 | *.zip 13 | .DS_Store 14 | .distignore 15 | .editorconfig 16 | .eslintignore 17 | .eslintrc.js 18 | .eslintrc.json 19 | .git 20 | .gitattributes 21 | .gitignore 22 | .npmignore 23 | .prettierrc.js 24 | .stylelintignore 25 | .stylelintrc.json 26 | LICENSE.md 27 | behat.yml 28 | circle.yml 29 | composer.json 30 | composer.lock 31 | package-lock.json 32 | package.json 33 | phpcs.xml.dist 34 | webpack.config.js 35 | README.md 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/create-release-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Create Release and Deploy to WordPress.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '20.11.0' 18 | 19 | - name: Install dependencies 20 | run: npm install 21 | 22 | - name: Build plugin zip 23 | run: npm run plugin-zip 24 | 25 | - name: Create Release 26 | id: create_release 27 | uses: actions/create-release@v1 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | with: 31 | tag_name: ${{ github.ref }} 32 | release_name: ${{ github.ref }} 33 | draft: false 34 | prerelease: false 35 | 36 | - name: Upload Release Asset 37 | uses: actions/upload-release-asset@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | upload_url: ${{ steps.create_release.outputs.upload_url }} 42 | asset_path: ./dark-mode-toggle-block.zip 43 | asset_name: dark-mode-toggle-block.zip 44 | asset_content_type: application/zip 45 | 46 | - name: Deploy to WordPress.org 47 | uses: 10up/action-wordpress-plugin-deploy@stable 48 | with: 49 | generate-zip: true 50 | dry-run: false 51 | env: 52 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 53 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 54 | SLUG: dark-mode-toggle-block 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # System 9 | .DS_Store 10 | 11 | # Coverage directory used by tools like istanbul 12 | coverage 13 | 14 | # Compiled binary addons (https://nodejs.org/api/addons.html) 15 | build/ 16 | 17 | # Dependency directories 18 | node_modules/ 19 | 20 | # Optional npm cache directory 21 | .npm 22 | 23 | # Optional eslint cache 24 | .eslintcache 25 | 26 | # Output of `npm pack` 27 | *.tgz 28 | 29 | # Output of `wp-scripts plugin-zip` 30 | *.zip 31 | 32 | # dotenv environment variables file 33 | .env 34 | -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtabor/dark-mode-toggle-block/6dcb7d1ea18762c65246ec5b05815e031967156b/.wordpress-org/banner-1544x500.png -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtabor/dark-mode-toggle-block/6dcb7d1ea18762c65246ec5b05815e031967156b/.wordpress-org/banner-772x250.png -------------------------------------------------------------------------------- /.wordpress-org/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtabor/dark-mode-toggle-block/6dcb7d1ea18762c65246ec5b05815e031967156b/.wordpress-org/icon-128x128.png -------------------------------------------------------------------------------- /.wordpress-org/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtabor/dark-mode-toggle-block/6dcb7d1ea18762c65246ec5b05815e031967156b/.wordpress-org/icon-256x256.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dark Mode Toggle Block 2 | 3 | [![Create Release and Deploy to WordPress.org](https://github.com/richtabor/dark-mode-toggle-block/actions/workflows/create-release-and-deploy.yml/badge.svg)](https://github.com/richtabor/dark-mode-toggle-block/actions/workflows/create-release-and-deploy.yml) 4 | 5 | A WordPress block to add a toggle between light and dark appearances, as seen on [my blog](https://rich.blog). Adds a `theme-dark` class to the body element, when toggled on. The user's preference is then saved in local storage. 6 | 7 | [Read about it on my blog →](https://rich.blog/dark-mode-toggle-block/) 8 | 9 | ### Visual 10 | 11 | https://github.com/richtabor/dark-mode-toggle-block/assets/1813435/f7255865-6328-4f54-8284-6bb2432d8ab2 12 | 13 | ### How it works 14 | When toggled, the block will add a `.theme-dark` class to the body of the site. You can add CSS variables to target dark styles. 15 | 16 | I did it this way on [my blog](https://rich.blog), which uses the theme.json `settings.custom.color` values for each color, unless there is a color created within the Site Editor with corresponding slug (i.e. `theme-1-dark`). I used this method so that a user could manipulate any given color without having to modify theme.json. 17 | 18 | ``` 19 | /* Dark styles */ 20 | .theme-dark body { 21 | --wp--preset--color--theme-1: var(--wp--preset--color--custom-theme-1-dark, var(--wp--custom--color--theme-1-dark)); 22 | --wp--preset--color--theme-2: var(--wp--preset--color--custom-theme-2-dark, var(--wp--custom--color--theme-2-dark)); 23 | --wp--preset--color--theme-3: var(--wp--preset--color--custom-theme-3-dark, var(--wp--custom--color--theme-3-dark)); 24 | --wp--preset--color--theme-4: var(--wp--preset--color--custom-theme-4-dark, var(--wp--custom--color--theme-4-dark)); 25 | --wp--preset--color--theme-5: var(--wp--preset--color--custom-theme-5-dark, var(--wp--custom--color--theme-5-dark)); 26 | --wp--preset--color--theme-6: var(--wp--preset--color--custom-theme-6-dark, var(--wp--custom--color--theme-6-dark)); 27 | } 28 | ``` 29 | 30 | ### Development 31 | 32 | 1. Clone the repository into your WordPress plugins directory. 33 | 2. Run `npm install` to install dependencies. 34 | 3. Run `npm start` to start the development server. 35 | 4. Activate the plugin on your local WordPress site. 36 | 5. Add the block to a post or page. 37 | -------------------------------------------------------------------------------- /dark-mode-toggle-block.php: -------------------------------------------------------------------------------- 1 | { 26 | const { className, icon, size } = attributes; 27 | const colorProps = getColorClassesAndStyles( attributes ); 28 | const borderProps = getBorderClassesAndStyles( attributes ); 29 | const spacingProps = getSpacingClassesAndStyles( attributes ); 30 | 31 | // Dynamically determine which icon to use for each style. 32 | const LightIcon = Icons[ icon ]?.light || Icons.filled?.light; 33 | const DarkIcon = Icons[ icon ]?.dark || Icons.filled?.dark; 34 | 35 | const classes = classnames( className, { 36 | [ `is-${ size }` ]: size, 37 | } ); 38 | 39 | return ( 40 |
41 | 87 |
88 | ); 89 | }, 90 | }, 91 | ]; 92 | 93 | export default deprecated; 94 | -------------------------------------------------------------------------------- /src/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classnames from 'classnames'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { 10 | useBlockProps, 11 | InspectorControls, 12 | __experimentalUseColorProps as useColorProps, 13 | __experimentalUseBorderProps as useBorderProps, 14 | __experimentalGetSpacingClassesAndStyles as useSpacingProps, 15 | } from '@wordpress/block-editor'; 16 | import { 17 | Disabled, 18 | Icon, 19 | __experimentalToggleGroupControl as ToggleGroupControl, 20 | __experimentalToggleGroupControlOption as ToggleGroupControlOption, 21 | __experimentalToolsPanel as ToolsPanel, 22 | __experimentalToolsPanelItem as ToolsPanelItem, 23 | } from '@wordpress/components'; 24 | import { useEffect } from '@wordpress/element'; 25 | import { __ } from '@wordpress/i18n'; 26 | 27 | /** 28 | * Internal dependencies 29 | */ 30 | import { Icons } from './icons'; 31 | 32 | /** 33 | * The edit function describes the structure of your block in the context of the 34 | * editor. This represents what the editor will render when the block is used. 35 | * 36 | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit 37 | */ 38 | export default function Edit( { attributes, setAttributes } ) { 39 | const { className, icon, size } = attributes; 40 | const blockProps = useBlockProps( { 41 | className: classnames( { 42 | [ `is-${ size }` ]: size, 43 | } ), 44 | } ); 45 | const colorProps = useColorProps( attributes ); 46 | const borderProps = useBorderProps( attributes ); 47 | const spacingProps = useSpacingProps( attributes ); 48 | 49 | // Dynamically determine which icon to use for each style. 50 | const LightIcon = Icons[ icon ]?.light || Icons.filled?.light; 51 | const DarkIcon = Icons[ icon ]?.dark || Icons.filled?.dark; 52 | 53 | useEffect( () => { 54 | // Map class names to icon attribute. 55 | const styleToIcons = { 56 | 'is-style-stroke': 'stroke', 57 | 'is-style-circle': 'circle', 58 | 'is-style-eye': 'eye', 59 | }; 60 | 61 | // Find the first matching style and set the corresponding icon. 62 | const iconString = Object.keys( styleToIcons ).find( ( style ) => 63 | className?.includes( style ) 64 | ); 65 | 66 | if ( iconString ) { 67 | setAttributes( { icon: styleToIcons[ iconString ] } ); 68 | } else { 69 | // Reset or handle the attribute when no styles are matched. 70 | setAttributes( { icon: undefined } ); 71 | } 72 | }, [ className, setAttributes ] ); 73 | 74 | return ( 75 | <> 76 | 77 | 78 | !! size } 82 | onDeselect={ () => 83 | setAttributes( { size: undefined } ) 84 | } 85 | > 86 | { 91 | setAttributes( { 92 | size: value, 93 | } ); 94 | } } 95 | isBlock 96 | size={ '__unstable-large' } 97 | __nextHasNoMarginBottom 98 | > 99 | 108 | 117 | 126 | 127 | 128 | 129 | 130 |
131 | 132 | 175 | 176 |
177 | 178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /src/icons.js: -------------------------------------------------------------------------------- 1 | import * as Icon from './icons/index.js'; 2 | 3 | export const Icons = { 4 | filled: { 5 | light: Icon.FilledLight, 6 | dark: Icon.FilledDark, 7 | }, 8 | stroke: { 9 | light: Icon.StrokeLight, 10 | dark: Icon.StrokeDark, 11 | }, 12 | circle: { 13 | light: Icon.CircleLight, 14 | dark: Icon.CircleDark, 15 | }, 16 | eye: { 17 | light: Icon.EyeLight, 18 | dark: Icon.EyeDark, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | export { default as FilledLight } from './library/filled-light'; 2 | export { default as FilledDark } from './library/filled-dark'; 3 | 4 | export { default as StrokeLight } from './library/stroke-light'; 5 | export { default as StrokeDark } from './library/stroke-dark'; 6 | 7 | export { default as CircleLight } from './library/circle-light'; 8 | export { default as CircleDark } from './library/circle-dark'; 9 | 10 | export { default as EyeLight } from './library/eye-light'; 11 | export { default as EyeDark } from './library/eye-dark'; 12 | -------------------------------------------------------------------------------- /src/icons/library/circle-dark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | const svg = ( 7 | 14 | 20 | 21 | ); 22 | 23 | export default svg; 24 | -------------------------------------------------------------------------------- /src/icons/library/circle-light.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | const svg = ( 7 | 14 | 20 | 21 | ); 22 | 23 | export default svg; 24 | -------------------------------------------------------------------------------- /src/icons/library/eye-dark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | const svg = ( 7 | 14 | 18 | 19 | ); 20 | 21 | export default svg; 22 | -------------------------------------------------------------------------------- /src/icons/library/eye-light.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | const svg = ( 7 | 14 | 20 | 21 | ); 22 | 23 | export default svg; 24 | -------------------------------------------------------------------------------- /src/icons/library/filled-dark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | const svg = ( 7 | 14 | 18 | 19 | ); 20 | 21 | export default svg; 22 | -------------------------------------------------------------------------------- /src/icons/library/filled-light.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | const svg = ( 7 | 14 | 18 | 19 | ); 20 | 21 | export default svg; 22 | -------------------------------------------------------------------------------- /src/icons/library/stroke-dark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | const svg = ( 7 | 14 | 20 | 21 | ); 22 | 23 | export default svg; 24 | -------------------------------------------------------------------------------- /src/icons/library/stroke-light.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | const svg = ( 7 | 14 | 20 | 21 | ); 22 | 23 | export default svg; 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Registers a new block provided a unique name and an object defining its behavior. 3 | * 4 | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ 5 | */ 6 | import { registerBlockType } from '@wordpress/blocks'; 7 | 8 | /** 9 | * WordPress dependencies 10 | */ 11 | import { Path, SVG } from '@wordpress/components'; 12 | 13 | /** 14 | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. 15 | * All files containing `style` keyword are bundled together. The code used 16 | * gets applied both to the front of your site and to the editor. 17 | * 18 | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css 19 | */ 20 | import './style.scss'; 21 | 22 | /** 23 | * Internal dependencies 24 | */ 25 | import edit from './edit'; 26 | import metadata from './block.json'; 27 | import save from './save'; 28 | import deprecated from './deprecated'; 29 | 30 | /** 31 | * Every block starts by registering a new block type definition. 32 | * 33 | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ 34 | */ 35 | registerBlockType( metadata.name, { 36 | icon: ( 37 | 44 | 48 | 49 | ), 50 | example: { 51 | viewportWidth: 300, 52 | attributes: { 53 | size: 'large', 54 | }, 55 | }, 56 | deprecated, 57 | edit, 58 | save, 59 | } ); 60 | -------------------------------------------------------------------------------- /src/save.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classnames from 'classnames'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { 10 | useBlockProps, 11 | __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, 12 | __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, 13 | __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles, 14 | } from '@wordpress/block-editor'; 15 | import { __ } from '@wordpress/i18n'; 16 | 17 | /** 18 | * Internal dependencies 19 | */ 20 | import { Icons } from './icons'; 21 | 22 | export default function Save( { attributes } ) { 23 | const { className, icon, size } = attributes; 24 | const colorProps = getColorClassesAndStyles( attributes ); 25 | const borderProps = getBorderClassesAndStyles( attributes ); 26 | const spacingProps = getSpacingClassesAndStyles( attributes ); 27 | 28 | // Dynamically determine which icon to use for each style. 29 | const LightIcon = Icons[ icon ]?.light || Icons.filled?.light; 30 | const DarkIcon = Icons[ icon ]?.dark || Icons.filled?.dark; 31 | 32 | const classes = classnames( className, { 33 | [ `is-${ size }` ]: size, 34 | } ); 35 | 36 | return ( 37 |
38 | 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | .wp-block-tabor-dark-mode-toggle { 2 | --icon-size: 16px; 3 | 4 | &.is-medium { 5 | --icon-size: 18px; 6 | } 7 | 8 | &.is-large { 9 | --icon-size: 20px; 10 | } 11 | } 12 | 13 | .wp-block-tabor-dark-mode-toggle__label { 14 | align-items: center; 15 | display: flex; 16 | margin: 0; 17 | } 18 | 19 | .wp-block-tabor-dark-mode-toggle__input { 20 | clip-path: inset(50%); 21 | clip: rect(0 0 0 0); 22 | height: 1px; 23 | overflow: hidden; 24 | position: absolute; 25 | white-space: nowrap; 26 | width: 1px; 27 | } 28 | 29 | .wp-block-tabor-dark-mode-toggle__track { 30 | align-items: center; 31 | border-radius: 100px; 32 | cursor: pointer; 33 | display: flex; 34 | height: calc(var(--icon-size) * 1.75); 35 | line-height: 1; 36 | padding: 0 calc(var(--icon-size) * 0.25); 37 | position: relative; 38 | transition: background-color var(--wp--custom--transition--duration, 200ms) ease-out; 39 | width: calc(var(--icon-size) * 2.5); 40 | 41 | &:not(.has-background) { 42 | background: rgba(195, 195, 195, 0.3); 43 | } 44 | 45 | &:not(.has-background):hover { 46 | background: rgba(195, 195, 195, 0.5); 47 | } 48 | } 49 | 50 | .wp-block-tabor-dark-mode-toggle__selector { 51 | align-items: center; 52 | border-radius: 100%; 53 | display: flex; 54 | height: calc(var(--icon-size) * 1.25); 55 | justify-content: center; 56 | transition: transform var(--wp--custom--transition--duration, 200ms) ease-out; 57 | width: calc(var(--icon-size) * 1.25); 58 | will-change: transform; 59 | } 60 | 61 | .wp-block-tabor-dark-mode-toggle__icon { 62 | backface-visibility: hidden; 63 | border-radius: 100%; 64 | color: currentcolor; 65 | opacity: 1; 66 | position: absolute; 67 | 68 | &, 69 | svg { 70 | width: var(--icon-size); 71 | height: var(--icon-size); 72 | } 73 | } 74 | 75 | .theme-dark .wp-block-tabor-dark-mode-toggle__input + .wp-block-tabor-dark-mode-toggle__track .wp-block-tabor-dark-mode-toggle__selector { 76 | transform: translateX(calc(var(--icon-size) * 1.25)); 77 | } 78 | 79 | .wp-block-tabor-dark-mode-toggle__icon--dark { 80 | visibility: hidden; 81 | } 82 | 83 | .theme-dark { 84 | 85 | .wp-block-tabor-dark-mode-toggle__icon--light { 86 | visibility: hidden; 87 | } 88 | 89 | .wp-block-tabor-dark-mode-toggle__icon--dark { 90 | visibility: visible; 91 | } 92 | } 93 | 94 | // Update to use :focus-visible instead of :focus 95 | .wp-block-tabor-dark-mode-toggle__input:focus-visible + .wp-block-tabor-dark-mode-toggle__track { 96 | outline: -webkit-focus-ring-color auto 1px; 97 | } 98 | -------------------------------------------------------------------------------- /src/view.js: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | 3 | // Cache the document element outside of the function to avoid repeated DOM queries. 4 | const body = document.documentElement; 5 | const darkModeToggles = document.querySelectorAll('.wp-block-tabor-dark-mode-toggle__input'); 6 | 7 | function updateToggleState(toggle, isDark) { 8 | toggle.checked = isDark; 9 | toggle.setAttribute('aria-checked', isDark.toString()); 10 | toggle.setAttribute( 11 | 'aria-label', 12 | isDark 13 | ? __('Switch to light mode, currently dark', 'dark-mode-toggle-block') 14 | : __('Switch to dark mode, currently light', 'dark-mode-toggle-block') 15 | ); 16 | } 17 | 18 | function toggleTheme() { 19 | // Toggle the 'theme-dark' class. 20 | body.classList.toggle('theme-dark'); 21 | const isDark = body.classList.contains('theme-dark'); 22 | 23 | // Update localStorage based on the presence of the class. 24 | localStorage.setItem('darkMode', isDark ? 'enabled' : 'disabled'); 25 | 26 | // Update all toggles' states 27 | darkModeToggles.forEach(toggle => updateToggleState(toggle, isDark)); 28 | } 29 | 30 | // Initialize all toggles 31 | darkModeToggles.forEach(toggle => { 32 | // Set initial state 33 | const isDark = body.classList.contains('theme-dark'); 34 | updateToggleState(toggle, isDark); 35 | 36 | // Attach event listeners 37 | toggle.addEventListener('click', toggleTheme); 38 | toggle.addEventListener('keydown', (e) => { 39 | if (e.code === 'Space' || e.code === 'Enter') { 40 | e.preventDefault(); 41 | toggleTheme(); 42 | } 43 | }); 44 | }); 45 | --------------------------------------------------------------------------------