Dynamic Theme Support for Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Supporting Dynamic Website Themes

Dynamic theme switching isn't just a "dark/light mode" button. It's a design token management system that must work without flashing on load, persist correctly between sessions, respect user system settings, and support arbitrary themes without CSS duplication.

CSS Variables Architecture

The right foundation is CSS Custom Properties. The entire palette and typography are described via variables, components use only variables (no hardcoded #1a1a2e):

/* Base theme (light) */
:root {
  --color-bg-primary:    #ffffff;
  --color-bg-secondary:  #f5f5f5;
  --color-text-primary:  #1a1a1a;
  --color-text-muted:    #6b7280;
  --color-accent:        #3b82f6;
  --color-border:        #e5e7eb;
  --shadow-card:         0 1px 3px rgba(0,0,0,0.1);
}

/* Dark theme */
[data-theme="dark"] {
  --color-bg-primary:    #0f172a;
  --color-bg-secondary:  #1e293b;
  --color-text-primary:  #f1f5f9;
  --color-text-muted:    #94a3b8;
  --color-accent:        #60a5fa;
  --color-border:        #334155;
  --shadow-card:         0 1px 3px rgba(0,0,0,0.5);
}

/* Third theme (example: high contrast) */
[data-theme="high-contrast"] {
  --color-bg-primary:    #000000;
  --color-text-primary:  #ffffff;
  --color-accent:        #ffff00;
  --color-border:        #ffffff;
}

Theme switch is a single setAttribute:

document.documentElement.setAttribute('data-theme', 'dark');

The switch is instant, no page reload, no JavaScript-repainting every element.

Eliminating FOUC (Flash of Unstyled Content)

Main issue: if the theme loads via React after hydration — users see the light screen flashing before the dark theme. Solution — inline script in <head>, executed before render:

<!-- In <head>, before any styles -->
<script>
  (function() {
    var theme = localStorage.getItem('theme');
    var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    var resolved = theme || (prefersDark ? 'dark' : 'light');
    document.documentElement.setAttribute('data-theme', resolved);
  })();
</script>

This script is synchronous and tiny (~200 bytes). It executes immediately, setting the correct theme before any CSS renders.

React Context + Hook

type Theme = 'light' | 'dark' | 'high-contrast' | 'system';

interface ThemeContextValue {
  theme: Theme;
  resolvedTheme: 'light' | 'dark' | 'high-contrast';
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setThemeState] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'system';
    return (localStorage.getItem('theme') as Theme) || 'system';
  });

  const systemTheme = useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light';
  const resolvedTheme = theme === 'system' ? systemTheme : theme;

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', resolvedTheme);
  }, [resolvedTheme]);

  const setTheme = (newTheme: Theme) => {
    setThemeState(newTheme);
    localStorage.setItem('theme', newTheme);
  };

  return (
    <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
  return ctx;
};

Theme Toggle

const ThemeToggle: React.FC = () => {
  const { theme, setTheme } = useTheme();

  const options: { value: Theme; icon: React.ReactNode; label: string }[] = [
    { value: 'light',  icon: <SunIcon />,     label: 'Light' },
    { value: 'dark',   icon: <MoonIcon />,    label: 'Dark' },
    { value: 'system', icon: <MonitorIcon />, label: 'System' },
  ];

  return (
    <div className="theme-toggle" role="group" aria-label="Choose theme">
      {options.map(opt => (
        <button
          key={opt.value}
          onClick={() => setTheme(opt.value)}
          aria-pressed={theme === opt.value}
          title={opt.label}
        >
          {opt.icon}
        </button>
      ))}
    </div>
  );
};

Smooth Theme Transitions

Without animation, theme switching looks sharp:

*, *::before, *::after {
  transition:
    background-color 200ms ease,
    color 150ms ease,
    border-color 200ms ease,
    box-shadow 200ms ease;
}

Important nuance: disable this transition during initial load, otherwise returning to page shows animation from default colors:

// Remove transition for 1 frame after load
useEffect(() => {
  document.documentElement.classList.add('no-transition');
  requestAnimationFrame(() => {
    document.documentElement.classList.remove('no-transition');
  });
}, []);
.no-transition * { transition: none !important; }

Integration with Tailwind CSS

Tailwind 4 supports CSS variables natively. Mapping:

/* In tailwind.config or @theme */
@theme {
  --color-primary: var(--color-accent);
  --color-background: var(--color-bg-primary);
}

For Tailwind 3 — darkMode: 'class' in config, but better to fully switch to CSS variables and avoid dark: prefixes.

Custom Themes (Color Picker)

For advanced cases — user picks the accent color:

const AccentPicker: React.FC = () => {
  const handleChange = (color: string) => {
    document.documentElement.style.setProperty('--color-accent', color);
    // Automatically compute hover state
    document.documentElement.style.setProperty(
      '--color-accent-hover',
      adjustLightness(color, -10)
    );
    localStorage.setItem('accent-color', color);
  };

  return <input type="color" onChange={e => handleChange(e.target.value)} />;
};

Timeline

Task Time
CSS variables + 2 themes (light/dark) 0.5 day
FOUC fix + React Context 0.5 day
Toggle + localStorage persistence 0.5 day
Smooth transitions 0.5 day
Additional themes / color picker 1–2 days

Basic light/dark implementation: 1.5–2 days.