This document outlines the plan to implement dark mode for Sketch's web UI using Tailwind CSS's built-in dark mode capabilities.
Sketch's web UI currently uses:
SketchTailwindElementcreateRenderRoot() { return this; })bg-white, text-gray-600, border-gray-200// tailwind.config.js export default { content: ["./src/**/*.{js,ts,jsx,tsx,html}", "./src/test-theme.html"], darkMode: "selector", // Enable selector-based dark mode plugins: ["@tailwindcss/container-queries"], theme: { extend: { // Custom colors for better dark mode support colors: { // Define semantic color tokens surface: { DEFAULT: "#ffffff", dark: "#1f2937", }, "surface-secondary": { DEFAULT: "#f9fafb", dark: "#374151", }, }, animation: { "fade-in": "fadeIn 0.3s ease-in-out", }, keyframes: { fadeIn: { "0%": { opacity: "0", transform: "translateX(-50%) translateY(10px)", }, "100%": { opacity: "1", transform: "translateX(-50%) translateY(0)", }, }, }, }, }, };
// src/web-components/theme-service.ts export type ThemeMode = "light" | "dark" | "system"; export class ThemeService { private static instance: ThemeService; private systemPrefersDark = false; private systemMediaQuery: MediaQueryList; static getInstance(): ThemeService { if (!this.instance) { this.instance = new ThemeService(); } return this.instance; } /** * Cycle through theme modes: light -> dark -> system -> light */ toggleTheme(): void { const currentTheme = this.getTheme(); let nextTheme: ThemeMode; switch (currentTheme) { case "light": nextTheme = "dark"; break; case "dark": nextTheme = "system"; break; case "system": nextTheme = "light"; break; default: nextTheme = "light"; } this.setTheme(nextTheme); } setTheme(theme: ThemeMode): void { // Store the theme preference if (theme === "system") { localStorage.removeItem("theme"); } else { localStorage.setItem("theme", theme); } // Apply the theme this.applyTheme(); // Dispatch event for components that need to react document.dispatchEvent( new CustomEvent("theme-changed", { detail: { theme, effectiveTheme: this.getEffectiveTheme(), systemPrefersDark: this.systemPrefersDark, }, }), ); } getTheme(): ThemeMode { const saved = localStorage.getItem("theme"); if (saved === "light" || saved === "dark") { return saved; } return "system"; } getEffectiveTheme(): "light" | "dark" { const theme = this.getTheme(); if (theme === "system") { return this.systemPrefersDark ? "dark" : "light"; } return theme; } initializeTheme(): void { this.applyTheme(); } }
// src/web-components/sketch-theme-toggle.ts import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { SketchTailwindElement } from "./sketch-tailwind-element.js"; import { ThemeService, ThemeMode } from "./theme-service.js"; @customElement("sketch-theme-toggle") export class SketchThemeToggle extends SketchTailwindElement { @state() private currentTheme: ThemeMode = "system"; @state() private effectiveTheme: "light" | "dark" = "light"; private themeService = ThemeService.getInstance(); connectedCallback() { super.connectedCallback(); this.updateThemeState(); // Listen for theme changes from other sources document.addEventListener("theme-changed", this.handleThemeChange); } disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener("theme-changed", this.handleThemeChange); } private handleThemeChange = (e: CustomEvent) => { this.currentTheme = e.detail.theme; this.effectiveTheme = e.detail.effectiveTheme; }; private toggleTheme() { this.themeService.toggleTheme(); } private getThemeIcon(): string { switch (this.currentTheme) { case "light": return "☀️"; // Sun case "dark": return "🌙"; // Moon case "system": return "💻"; // Computer/Laptop default: return "💻"; } } render() { return html` <button @click=${this.toggleTheme} class="p-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500" title="${this.currentTheme} theme - Click to cycle themes" aria-label="Cycle between light, dark, and system theme" > ${this.getThemeIcon()} </button> `; } } declare global { interface HTMLElementTagNameMap { "sketch-theme-toggle": SketchThemeToggle; } }
Add theme initialization to the main app shell component. This needs to be implemented:
// In sketch-app-shell.ts or sketch-app-shell-base.ts import { ThemeService } from "./theme-service.js"; connectedCallback() { super.connectedCallback(); ThemeService.getInstance().initializeTheme(); }
Note: This initialization is not yet implemented in the app shell components.
Identify all components using color classes
bg-, text-, border-, ring-, divide- classesAdd dark mode variants systematically
Start with core components (app shell, chat input, etc.)
Update classes following the pattern:
// Before: class="bg-white text-gray-900 border-gray-200" // After: class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 border-gray-200 dark:border-gray-700"
Priority order for component updates:
sketch-app-shell - Main containersketch-chat-input - Primary interaction componentsketch-container-status - Status indicatorssketch-call-status - Call indicators// Light -> Dark mappings bg-white -> bg-gray-900 bg-gray-50 -> bg-gray-800 bg-gray-100 -> bg-gray-800 bg-gray-200 -> bg-gray-700 text-gray-900 -> text-gray-100 text-gray-800 -> text-gray-200 text-gray-700 -> text-gray-300 text-gray-600 -> text-gray-400 text-gray-500 -> text-gray-500 (neutral) border-gray-200 -> border-gray-700 border-gray-300 -> border-gray-600 ring-gray-300 -> ring-gray-600
transition-colors to interactive elementssrc/web-components/ ├── theme-service.ts # Theme management service (✅ implemented) ├── sketch-theme-toggle.ts # Theme toggle component (✅ implemented) ├── sketch-tailwind-element.ts # Base class (✅ existing) └── [other components].ts # Need dark mode variants added
SketchTailwindElement classSketchTailwindElement (not LitElement) ✅