frontend-ui-dark-ts

$npx mdskill add tranhieutt/software_development_department/frontend-ui-dark-ts

Builds dark-themed TypeScript UIs with accessible color systems and responsive patterns.

  • Implements dark mode with user toggle and localStorage to prevent flash on hydration.
  • Uses HSL color tokens and CSS variables for programmatic lightness adjustments.
  • Enforces WCAG contrast ratios (4.5:1 text, 3:1 UI) and avoids pure black backgrounds.
  • Generates accessible, responsive UI components with TypeScript and Tailwind CSS.

SKILL.md

.github/skills/frontend-ui-dark-tsView on GitHub ↗
---
name: frontend-ui-dark-ts
type: reference
description: "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."
paths: ["**/*.tsx", "**/*.ts", "**/*.css", "**/globals.css", "**/tailwind.config.*"]
effort: 3
allowed-tools: Read, Glob, Grep, Write, Edit, Bash
user-invocable: true
when_to_use: "When implementing dark mode, designing accessible color systems, or building TypeScript UI components"
---

# 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 `prefers-color-scheme` media query alone** — users need a toggle; sync with `localStorage` to avoid flash on hydration
- **HSL for dark themes**: use `hsl(220 15% 10%)` not `#1a1a2e` — HSL lets you programmatically adjust lightness
- **Avoid pure black (`#000`)** for dark backgrounds — causes eye strain; use `hsl(220 15% 8%)` instead
- **`color-scheme: dark`** on `:root` makes browser UI (scrollbars, inputs) follow dark theme

## CSS variable token system

```css
/* 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)

```typescript
// 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

```typescript
// 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

```typescript
// 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)

```javascript
// 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

```typescript
// 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 `<head>` before hydration |
| Using `opacity` for text variants | Use separate CSS token with correct contrast ratio |
| Dark text (`gray-900`) on dark bg | Always test contrast; use `--text-primary` token |
| Hover states not visible in dark mode | Ensure hover has ≥3:1 contrast vs default state |
| `currentColor` for icons | Verify icon color passes 3:1 contrast vs background |

More from tranhieutt/software_development_department

SkillDescription
agent-healthReads production/traces/agent-metrics.jsonl and displays a per-agent performance summary table for the current or a specified session. Highlights agents with high error rates or OPEN circuit breaker state.
agent-styleProvides the vendored agent-style v0.3.5 prose rule pack as a portable Claude skill. Use when installing, syncing, applying, or auditing SDD Agent-Style support in another project, or when a style-review workflow needs the bundled rules.
angular-best-practicesProvides Angular best practices for components, modules, services, and reactive patterns. Use when working with Angular TypeScript files, component templates, NgModules, RxJS observables, or when the user mentions Angular, ng, or Angular CLI.
annotateRecords unexpected API behaviors, undocumented caveats, version bugs, or non-obvious workarounds into .claude/memory/annotations.md. Use immediately when an undocumented behavior or surprising caveat is discovered during development.
aws-serverlessProvides AWS serverless architecture patterns for Lambda, API Gateway, DynamoDB, SQS, and SAM/CDK. Use when working with AWS serverless files (serverless.yml, CDK stacks) or when the user mentions Lambda, API Gateway, serverless, or AWS SAM.
backend-architectDesigns scalable backend architectures covering microservices, event-driven systems, API gateways, and data stores. Use when designing a backend system or when the user mentions backend architecture, scalability, or distributed systems.
cloud-run-puppeteerDeploys Puppeteer browser automation on Google Cloud Run with Docker. Use when running headless browser tasks on Cloud Run, or when the user mentions Cloud Run, Puppeteer, headless Chrome, or serverless browser automation.
codex-sddAdapts SDD for Codex while preserving Claude Code behavior. Use when working in Codex, setting up Codex compatibility, mapping Claude tools to Codex tools, or explaining how SDD should run outside Claude Code.
database-architectDesigns relational and NoSQL database schemas, indexing strategies, migration plans, and data modeling patterns. Use when designing a database or when the user mentions database architecture, schema design, or data modeling.
db-reviewReviews database schemas, queries, and migrations for correctness, performance, security, and best practices. Use when reviewing SQL migration files or when the user mentions database review, schema review, or query audit.