Want a dark mode switch for Mediumish
Why add a dark mode switch?
Dark mode has moved beyond a trendy visual tweak — it improves legibility in low-light environments, reduces perceived glare on OLED/mobile displays, and can make reading long articles more comfortable at night. For a content-focused theme like Mediumish, a simple dark mode switch boosts user experience and can reduce bounce rates by letting readers choose the look that fits their environment.
Adding a dark mode switch also signals to users that your site is modern and accessible. It is an evergreen UX enhancement: once implemented correctly, it keeps working for many years with minimal maintenance.
How dark mode works (simple)
At a high level dark mode is just another set of visual rules (colors, shadows, borders) applied to the site. The usual pattern is:
- Define CSS variables for colors and other visual tokens.
- Provide a light theme (default) and a dark theme that overrides the variables.
- Use a toggle (HTML + JavaScript) to switch themes and persist the choice in localStorage or a cookie.
- Optionally respect the user's system preference using
prefers-color-scheme.
This keeps the HTML structure unchanged — only the CSS variables change. That approach is robust and easy to maintain.
Design and accessibility considerations
Before you implement, consider these important factors so dark mode is comfortable and usable:
- Contrast: Ensure text meets WCAG minimum contrast ratios against the background. For body text aim for a contrast ratio of at least 4.5:1. Use softer grays rather than pure white on black to avoid eye strain.
- Semantic markup: Keep semantics intact (headings, lists, code blocks) so assistive tech is unaffected.
- Focusable toggle: Make the switch keyboard accessible, labelled for screen readers, and visible when focused.
- Persist preference: Save the user choice so returning visitors see their chosen theme immediately.
- System preference: Respect
prefers-color-schemeby default, but allow the user to override it. - Images and media: Some images with transparent backgrounds or light logos may need alternate versions or CSS filters in dark mode.
- Syntax highlighting: If your theme features code blocks, provide a dark-friendly syntax theme to keep code readable and pleasant.
CSS approach: variables and theme rules
Use CSS custom properties (variables) at :root and an attribute on html (for example data-theme="dark") to switch values. This minimizes the number of selectors you need to change.
Example CSS to add to your main stylesheet (put near the top so it applies everywhere):
/* Theme variables: default (light) */
:root{
--bg: #ffffff;
--surface: #f7f7f7;
--text: #111827;
--muted: #6b7280;
--link: #1a73e8;
--border: #e5e7eb;
--code-bg: #f3f4f6;
--shadow: 0 1px 2px rgba(16,24,40,0.04);
--accent: #111827;
}
/* Dark theme overrides when data-theme="dark" */
:root[data-theme="dark"]{
--bg: #0b0f12;
--surface: #0f1720;
--text: #e6eef8;
--muted: #9aa7b5;
--link: #66a6ff;
--border: #1f2933;
--code-bg: #071126;
--shadow: none;
--accent: #e6eef8;
}
/* Respect system preference if user has not set anything yet */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]){
--bg: #0b0f12;
--surface: #0f1720;
--text: #e6eef8;
--muted: #9aa7b5;
--link: #66a6ff;
--border: #1f2933;
--code-bg: #071126;
}
}
/* Apply tokens to theme elements */
html, body{
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
line-height: 1.7;
}
/* Content card and post surface */
.post, .content, .container{
background: var(--surface);
border: 1px solid var(--border);
box-shadow: var(--shadow);
color: var(--text);
}
/* Links */
a{
color: var(--link);
}
/* Code blocks */
pre, code{
background: var(--code-bg);
color: var(--text);
border-radius: 6px;
padding: 0.2rem 0.4rem;
}
/* Invert SVG strokes if needed */
img.invert-on-dark{
filter: none;
}
:root[data-theme="dark"] img.invert-on-dark{
filter: invert(1) hue-rotate(180deg) brightness(1.1);
}Notes:
- Use
var(--...)everywhere instead of hard-coded colors in other rules. Replace existing hard-coded color values in Mediumish with variables. - Place the
@media (prefers-color-scheme: dark)block so users with no saved preference get a sensible default.
HTML markup: toggle button
Add a small, accessible toggle where your site header or utility area is rendered. Because you asked not to use <header> element, insert the button in your theme’s top right area (for example inside the theme’s existing site controls). The markup below is simple and screen-reader friendly.
<div class="theme-toggle-wrapper">
<button id="theme-toggle" aria-label="Toggle dark mode" aria-pressed="false">
<span class="sr-only">Toggle dark mode</span>
<svg class="icon-sun" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">...sun SVG path...</svg>
<svg class="icon-moon" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" hidden>...moon SVG path...</svg>
</button>
</div>CSS for the toggle (simple inline styles you can adapt):
.theme-toggle-wrapper{ display:inline-block; margin-left:12px; }
#theme-toggle{
background: transparent;
border: 1px solid var(--border);
padding:6px;
border-radius:8px;
display:inline-flex;
align-items:center;
justify-content:center;
cursor:pointer;
}
#theme-toggle:focus{ outline: 2px solid var(--link); outline-offset: 2px; }
.icon-moon[hidden], .icon-sun[hidden]{ display:none; }Notes:
- Use SVG icons for crisp visuals. Only one icon should be visible at a time.
- Set
aria-pressedto reflect the toggle state for assistive tech.
JavaScript: saving preference and toggling
The JS below toggles the data-theme attribute on the document element, updates the button state, and saves the preference to localStorage. Put this script before the closing <body> tag or as an inline script in your theme so it runs early enough to avoid a flash of the wrong theme.
(function(){
const storageKey = 'site-theme-preference';
const themeAttr = 'data-theme';
const toggle = document.getElementById('theme-toggle');
function applyTheme(theme){
if(theme){
document.documentElement.setAttribute(themeAttr, theme);
} else {
document.documentElement.removeAttribute(themeAttr);
}
}
function getSavedTheme(){
try{
return localStorage.getItem(storageKey);
}catch(e){
return null;
}
}
function saveTheme(theme){
try{
if(theme) localStorage.setItem(storageKey, theme);
else localStorage.removeItem(storageKey);
}catch(e){}
}
function userPrefersDark(){
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
// Init
const saved = getSavedTheme();
if(saved === 'dark'){
applyTheme('dark');
if(toggle) toggle.setAttribute('aria-pressed','true');
} else if(saved === 'light'){
applyTheme('light');
if(toggle) toggle.setAttribute('aria-pressed','false');
} else if(userPrefersDark()){
// Respect system default only when no saved preference
applyTheme('dark');
if(toggle) toggle.setAttribute('aria-pressed','true');
}
// Avoid flash: hide body until theme applied (optional progressive enhancement)
// document.documentElement.style.visibility = 'visible';
if(toggle){
toggle.addEventListener('click', function(){
const isPressed = toggle.getAttribute('aria-pressed') === 'true';
const next = isPressed ? 'light' : 'dark';
applyTheme(next === 'light' ? 'light' : 'dark');
saveTheme(next === 'light' ? 'light' : 'dark');
toggle.setAttribute('aria-pressed', String(!isPressed));
// switch visible icon
const sun = toggle.querySelector('.icon-sun');
const moon = toggle.querySelector('.icon-moon');
if(sun && moon){
if(next === 'dark'){ sun.setAttribute('hidden',''); moon.removeAttribute('hidden'); }
else { moon.setAttribute('hidden',''); sun.removeAttribute('hidden');}
}
});
}
})();Notes and improvements:
- You can choose to remove
data-theme="light"when in light mode and only setdata-theme="dark"for dark, keeping markup smaller. - If you want instant application before JS runs, render a small inline script in the document head that reads localStorage and sets the attribute synchronously to prevent a flash of theme (FOIT). Keep it tiny and safe.
- Be mindful of environments where
localStorageis not available (private browsing on some browsers). The try/catch guards help.
Integrating with the Mediumish theme
Mediumish typically uses a clean content container with classes like .post, .page, and .site. The safest integration approach:
- Find the theme's main stylesheet and replace color constants with CSS variables (search for hex values and substitute with
var(--...)where appropriate). - Add the variables block (the
:rootand:root[data-theme="dark"]) at the top of the stylesheet or in a small new stylesheet loaded before the theme stylesheet. - Insert the toggle button markup in the theme's site controls (top-right area). If Mediumish has a partial/template for site controls, edit that file so the toggle appears site-wide.
- Add the JavaScript snippet before the closing body tag, or inline in the theme’s head (the head inline snippet is recommended for preventing flash of wrong theme).
- Test all pages: homepage, single post, tag pages, archives, pagination, and mobile menus.
Example of a minimal inline head script that sets the attribute early (place it inside the <head>, as the very first inline script):
try{
const k='site-theme-preference';
const s=localStorage.getItem(k);
if(s==='dark'){ document.documentElement.setAttribute('data-theme','dark'); }
else if(s==='light'){ document.documentElement.removeAttribute('data-theme'); }
// if no saved preference, we let CSS @media handle the system preference
}catch(e){};Warning: If the theme bundles CSS with hard-coded colors in many places, you must audit and replace those colors with variables, otherwise the dark theme will be inconsistent.
Handling images and code syntax highlighting
Images: icons or logos with transparent backgrounds may look wrong on dark backgrounds. Options:
- Use alternate darker or lighter versions of the image and show them via CSS when
data-theme="dark". - Use the CSS filter trick for simple monochrome icons (see the
img.invert-on-darkrule earlier). - For complex illustrations, provide two files and switch the
srcvia CSS background-image or swap with small JavaScript when theme changes.
Code syntax: If your site features code blocks, adopt or include a dark theme for your syntax highlighter (Prism, Highlight.js, etc.). Example: add a dark version CSS and scope it to data-theme="dark".
/* Example: Prism light baseline */
pre[class*="language-"] { background: var(--code-bg); color: var(--text); }
/* Dark overrides */
:root[data-theme="dark"] pre[class*="language-"] {
background: #071126;
color: #e6eef8;
}
/* Load a dark prism theme file or provide token colors here */
Testing, troubleshooting, and deployment
Thorough testing steps:
- Test on mobile and desktop. Check pages with images, galleries, code blocks, tables, and forms.
- Verify keyboard focus states for the toggle and other interactive elements.
- Open the site in an incognito window to ensure localStorage behavior is graceful.
- Test with different system preferences (macOS and mobile) to see how the site responds when no user preference is saved.
- Check color contrast ratios with a tool (many browser extensions or automated tests) to ensure readability.
Common issues and fixes:
- Flash of wrong theme: Use a tiny inline head script to set
data-themefrom localStorage before stylesheets render. - Hard-coded colors still appear: Replace any remaining hard-coded colors in the theme CSS with variables, or add specific overrides under
:root[data-theme="dark"]for the problematic selectors. - Images look washed out: Provide alternate assets or use
filtersparingly; filters can degrade image quality. - Icons not switching: Confirm your toggle updates the visible icon and uses
hiddenattribute or toggles a class—SVG with contrast-aware fills helps.
FAQ
Q: Will dark mode hurt SEO or page speed?
A: No. Properly implemented dark mode uses CSS and a few bytes of JavaScript. It does not affect indexability. Avoid inlining large assets just for theme switching; keep the inline script minimal to prevent perceived flashes.
Q: Should I store the preference server-side?
A: For logged-in users you can store the preference in the user profile, which is handy across devices. For anonymous visitors, client-side storage (localStorage) is simple and effective.
Q: How do I test color contrast?
A: Use browser extensions like Accessibility Insights or Lighthouse audits. Check body text, headings, links, and interactive elements to ensure contrast ratios meet WCAG guidelines.
Q: Can I add an "auto" option that follows system preference?
A: Yes. Provide three states: light, dark, and auto. Store the user's explicit choice. If the user picks "auto", remove the saved entry and let prefers-color-scheme handle the default.
Q: Is there a recommended palette?
A: Use neutral backgrounds (not pure black) and off-white text to reduce strain. Blues and accent hues should remain readable in both modes. Test combined elements (links on surface, buttons on cards).
Practical example: Minimal patch checklist
- 1. Add CSS variables block to top of theme stylesheet.
- 2. Replace color tokens in core selectors with
var(--...). - 3. Add toggle HTML to the theme's global controls.
- 4. Add JS to read/save preference and toggle
data-theme. - 5. Add dark versions for charts, images, and code blocks if necessary.
- 6. Test across pages and fix any remaining hard-coded color issues.
Short checklist for production
| Action | Completed |
|---|---|
| Variables added | ☐ |
| Toggle UI added | ☐ |
| Persist preference | ☐ |
| Images handled | ☐ |
| Code style updated | ☐ |
| Accessibility check | ☐ |
Follow this pattern and your Mediumish-based site will have a reliable, accessible, and persistent dark mode that improves user comfort and modernizes your reading experience. The approach scales: once variables are in place, future visual changes are trivial.

Comments
Post a Comment