Software_development_department frontend-ui-dark-ts
Builds dark-themed TypeScript UIs with accessible color systems, contrast compliance, and responsive design patterns. Use when implementing dark mode or building accessible TypeScript UI components.
install
source · Clone the upstream repo
git clone https://github.com/tranhieutt/software_development_department
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/tranhieutt/software_development_department "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/frontend-ui-dark-ts" ~/.claude/skills/tranhieutt-software-development-department-frontend-ui-dark-ts && rm -rf "$T"
manifest:
.claude/skills/frontend-ui-dark-ts/SKILL.mdsource content
Frontend UI Dark (TypeScript)
Critical rules (non-obvious)
- WCAG contrast minimums: text on bg requires 4.5:1 (AA) or 7:1 (AAA); UI elements (borders, icons) require 3:1
- Never use
media query alone — users need a toggle; sync withprefers-color-scheme
to avoid flash on hydrationlocalStorage - HSL for dark themes: use
nothsl(220 15% 10%)
— HSL lets you programmatically adjust lightness#1a1a2e - Avoid pure black (
) for dark backgrounds — causes eye strain; use#000
insteadhsl(220 15% 8%)
oncolor-scheme: dark
makes browser UI (scrollbars, inputs) follow dark theme:root
CSS variable token system
/* globals.css */ :root { /* HSL values only (no hsl() wrapper) — allows opacity modifiers */ --bg-base: 222 47% 8%; --bg-surface: 222 47% 12%; --bg-elevated: 222 47% 16%; --text-primary: 220 20% 95%; --text-secondary: 220 15% 70%; --text-muted: 220 10% 50%; --brand: 220 90% 60%; --brand-hover: 220 90% 65%; --border: 220 20% 20%; --error: 0 85% 60%; --success: 142 70% 45%; color-scheme: dark; } /* Light mode override */ [data-theme="light"] { --bg-base: 0 0% 100%; --bg-surface: 220 14% 96%; --bg-elevated: 0 0% 100%; --text-primary: 222 47% 11%; --text-secondary: 220 14% 40%; color-scheme: light; }
Theme provider (React + no flash)
// providers/ThemeProvider.tsx export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState<"dark" | "light">(() => typeof window !== "undefined" ? (localStorage.getItem("theme") as "dark" | "light") ?? "dark" : "dark" ); useEffect(() => { document.documentElement.dataset.theme = theme; localStorage.setItem("theme", theme); }, [theme]); return ( <ThemeContext.Provider value={{ theme, toggle: () => setTheme(t => t === "dark" ? "light" : "dark") }}> {children} </ThemeContext.Provider> ); } // Prevent flash — add to <head> before React hydrates const themeScript = ` (function() { var t = localStorage.getItem('theme') || 'dark'; document.documentElement.dataset.theme = t; })(); `;
Accessible component patterns
// Button with all a11y attributes interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: "primary" | "ghost" | "danger"; isLoading?: boolean; } export function Button({ variant = "primary", isLoading, children, disabled, ...props }: ButtonProps) { return ( <button {...props} disabled={disabled || isLoading} aria-busy={isLoading} aria-disabled={disabled || isLoading} className={cn( "inline-flex items-center gap-2 rounded-md px-4 py-2 font-medium transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--brand))]", "disabled:pointer-events-none disabled:opacity-50", variant === "primary" && "bg-[hsl(var(--brand))] text-white hover:bg-[hsl(var(--brand-hover))]", variant === "ghost" && "hover:bg-[hsl(var(--bg-surface))]", variant === "danger" && "bg-[hsl(var(--error))] text-white", )} > {isLoading && <Spinner aria-hidden="true" />} {children} </button> ); }
Color utility function
// Use CSS variables with alpha function token(variable: string, alpha?: number): string { return alpha !== undefined ? `hsl(var(--${variable}) / ${alpha})` : `hsl(var(--${variable}))`; } // Usage: token("brand", 0.2) → "hsl(var(--brand) / 0.2)"
Tailwind dark theme config (v3)
// tailwind.config.ts export default { darkMode: ["class", '[data-theme="dark"]'], // class-based, controlled by JS theme: { extend: { colors: { bg: { base: "hsl(var(--bg-base) / <alpha-value>)", surface: "hsl(var(--bg-surface) / <alpha-value>)", elevated: "hsl(var(--bg-elevated) / <alpha-value>)", }, text: { primary: "hsl(var(--text-primary) / <alpha-value>)", secondary: "hsl(var(--text-secondary) / <alpha-value>)", }, brand: "hsl(var(--brand) / <alpha-value>)", } } } }
Contrast checker utility
// Quick WCAG contrast ratio check function getContrastRatio(fg: string, bg: string): number { // parse HSL → luminance → ratio // Use online tool: https://webaim.org/resources/contrastchecker/ // Or: colord(fg).contrast(colord(bg)) } // Minimum ratios: // 4.5:1 → AA normal text // 3.0:1 → AA large text (18pt+ or 14pt bold), UI components // 7.0:1 → AAA normal text
Common pitfalls
| Pitfall | Fix |
|---|---|
| Flash of wrong theme on page load | Add inline script to before hydration |
Using for text variants | Use separate CSS token with correct contrast ratio |
Dark text () on dark bg | Always test contrast; use token |
| Hover states not visible in dark mode | Ensure hover has ≥3:1 contrast vs default state |
for icons | Verify icon color passes 3:1 contrast vs background |