Skip to content

UI — Add dark/light mode toggle with system preference detection #513

@Tinna23

Description

@Tinna23

Description

Stellar Explain is currently hardcoded dark. This issue adds a proper theme system with a toggle button in the AppShell header, respecting the user's system preference by default and persisting their choice in localStorage.

This is non-trivial — it requires converting all hardcoded color values to CSS custom properties and ensuring both themes look polished.

Design Tokens

Define all colors as CSS custom properties on :root and [data-theme="light"]:

/* Dark theme (default) */
:root {
  --bg-base: #080c12;
  --bg-card: rgba(255,255,255,0.04);
  --bg-card-hover: rgba(255,255,255,0.06);
  --border-subtle: rgba(255,255,255,0.08);
  --border-accent: rgba(56,189,248,0.3);
  --text-primary: rgba(255,255,255,0.90);
  --text-secondary: rgba(255,255,255,0.50);
  --text-muted: rgba(255,255,255,0.25);
  --text-mono: rgba(255,255,255,0.40);
  --accent-sky: #38bdf8;
  --accent-sky-dim: rgba(56,189,248,0.1);
  --accent-purple: #a78bfa;
  --accent-purple-dim: rgba(167,139,250,0.1);
  --pill-success-bg: rgba(34,197,94,0.12);
  --pill-success-text: #86efac;
  --pill-fail-bg: rgba(239,68,68,0.12);
  --pill-fail-text: #fca5a5;
}

/* Light theme */
[data-theme="light"] {
  --bg-base: #f0f4f8;
  --bg-card: rgba(0,0,0,0.03);
  --bg-card-hover: rgba(0,0,0,0.05);
  --border-subtle: rgba(0,0,0,0.08);
  --border-accent: rgba(2,132,199,0.3);
  --text-primary: rgba(0,0,0,0.90);
  --text-secondary: rgba(0,0,0,0.55);
  --text-muted: rgba(0,0,0,0.30);
  --text-mono: rgba(0,0,0,0.45);
  --accent-sky: #0284c7;
  --accent-sky-dim: rgba(2,132,199,0.08);
  --accent-purple: #7c3aed;
  --accent-purple-dim: rgba(124,58,237,0.08);
  --pill-success-bg: rgba(22,163,74,0.10);
  --pill-success-text: #15803d;
  --pill-fail-bg: rgba(220,38,38,0.10);
  --pill-fail-text: #b91c1c;
}

What To Build

src/hooks/useTheme.ts

export function useTheme(): {
  theme: 'dark' | 'light';
  toggleTheme: () => void;
  setTheme: (theme: 'dark' | 'light') => void;
}
  • Reads localStorage.getItem('stellar-explain-theme') on mount
  • Falls back to window.matchMedia('(prefers-color-scheme: dark)')
  • Sets document.documentElement.setAttribute('data-theme', theme) on change
  • Persists to localStorage on change

src/components/ThemeToggle.tsx

A small icon button in the AppShell header:

  • 🌙 moon icon in dark mode → click to switch to light
  • ☀️ sun icon in light mode → click to switch to dark
  • Smooth transition on switch (CSS transition: background 0.2s ease)

Update src/app/globals.css

Define all CSS custom properties as specified above.

Update all components

Replace all hardcoded color values with CSS variables:

  • src/components/Card.tsx
  • src/components/Pill.tsx
  • src/components/Label.tsx
  • src/components/AddressChip.tsx
  • src/components/AppShell.tsx
  • src/components/TransactionResult.tsx
  • src/components/AccountResult.tsx
  • src/components/SearchBar.tsx
  • src/components/TabSwitcher.tsx

Integration

Add useTheme to AppShell.tsx and render <ThemeToggle /> in the header between the logo and the other action buttons.

Add to src/app/layout.tsx:

<script
  dangerouslySetInnerHTML={{
    __html: `
      (function() {
        var t = localStorage.getItem('stellar-explain-theme');
        if (!t) t = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
        document.documentElement.setAttribute('data-theme', t);
      })();
    `
  }}
/>

This prevents flash of wrong theme on page load.

Key Files

New files:

  • src/hooks/useTheme.ts
  • src/components/ThemeToggle.tsx

Modified files:

  • src/app/globals.css
  • src/app/layout.tsx
  • src/components/AppShell.tsx
  • src/components/Card.tsx
  • src/components/Pill.tsx
  • src/components/Label.tsx
  • src/components/AddressChip.tsx
  • src/components/TransactionResult.tsx
  • src/components/AccountResult.tsx
  • src/components/SearchBar.tsx
  • src/components/TabSwitcher.tsx

Acceptance Criteria

  • Dark mode looks identical to current app (no visual regression)
  • Light mode is fully usable and aesthetically consistent
  • Toggle button visible in AppShell header on all app pages
  • Theme persists across page reloads
  • System preference respected when no explicit choice has been made
  • No flash of wrong theme on initial page load
  • All components use CSS variables — no hardcoded color values remain
  • Theme transitions are smooth (no jarring flash on toggle)
  • Landing page also respects theme
  • TypeScript compiles with no errors

Complexity: High · 200 pts

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official Campaignfrontendcreate high-quality web applications with the power of React components.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions