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}"], darkMode: "class", // Enable class-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 class ThemeService { private static instance: ThemeService; static getInstance(): ThemeService { if (!this.instance) { this.instance = new ThemeService(); } return this.instance; } toggleTheme(): void { const isDark = document.documentElement.classList.contains("dark"); this.setTheme(isDark ? "light" : "dark"); } setTheme(theme: "light" | "dark"): void { document.documentElement.classList.toggle("dark", theme === "dark"); localStorage.setItem("theme", theme); // Dispatch event for components that need to react document.dispatchEvent( new CustomEvent("theme-changed", { detail: { theme }, }), ); } getTheme(): "light" | "dark" { return document.documentElement.classList.contains("dark") ? "dark" : "light"; } initializeTheme(): void { const saved = localStorage.getItem("theme"); const prefersDark = window.matchMedia( "(prefers-color-scheme: dark)", ).matches; const theme = saved || (prefersDark ? "dark" : "light"); this.setTheme(theme as "light" | "dark"); // Listen for system theme changes window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", (e) => { if (!localStorage.getItem("theme")) { this.setTheme(e.matches ? "dark" : "light"); } }); } }
// src/web-components/theme-toggle.ts import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { SketchTailwindElement } from "./sketch-tailwind-element.js"; import { ThemeService } from "./theme-service.js"; @customElement("theme-toggle") export class ThemeToggle extends SketchTailwindElement { @state() private isDark = false; private themeService = ThemeService.getInstance(); connectedCallback() { super.connectedCallback(); this.isDark = document.documentElement.classList.contains("dark"); // 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.isDark = e.detail.theme === "dark"; }; private toggleTheme() { this.themeService.toggleTheme(); } 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="Toggle theme" aria-label="Toggle between light and dark mode" > ${this.isDark ? "☀️" : "🌙"} </button> `; } } declare global { interface HTMLElementTagNameMap { "theme-toggle": ThemeToggle; } }
Add theme initialization to the main app shell component:
// In sketch-app-shell.ts or similar import { ThemeService } from "./theme-service.js"; connectedCallback() { super.connectedCallback(); ThemeService.getInstance().initializeTheme(); }
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 ├── theme-toggle.ts # Theme toggle component ├── sketch-tailwind-element.ts # Base class (existing) └── [other components].ts # Updated with dark mode variants
SketchTailwindElement (not LitElement)