diff --git a/packages/cra-template-typescript/template/src/App.tsx b/packages/cra-template-typescript/template/src/App.tsx
index a53698aab3..c5f36160ae 100644
--- a/packages/cra-template-typescript/template/src/App.tsx
+++ b/packages/cra-template-typescript/template/src/App.tsx
@@ -1,15 +1,19 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
+import logo from './logo.svg'
+import './App.css'
+import Header from './components/Header'
function App() {
return (
- );
+ )
}
-export default App;
+export default App
diff --git a/packages/cra-template-typescript/template/src/components/Header.tsx b/packages/cra-template-typescript/template/src/components/Header.tsx
new file mode 100644
index 0000000000..c5f36160ae
--- /dev/null
+++ b/packages/cra-template-typescript/template/src/components/Header.tsx
@@ -0,0 +1,30 @@
+import logo from './logo.svg'
+import './App.css'
+import Header from './components/Header'
+
+function App() {
+ return (
+
+ )
+}
+
+export default App
diff --git a/packages/cra-template-typescript/template/src/hooks/usDarkMode.ts b/packages/cra-template-typescript/template/src/hooks/usDarkMode.ts
new file mode 100644
index 0000000000..82a487fe74
--- /dev/null
+++ b/packages/cra-template-typescript/template/src/hooks/usDarkMode.ts
@@ -0,0 +1,71 @@
+import { useState, useEffect, useCallback } from 'react'
+
+type Theme = 'light' | 'dark'
+
+const STORAGE_KEY = 'theme-preference'
+
+function getSystemTheme(): Theme {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light'
+}
+
+function getStoredTheme(): Theme | null {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (stored === 'light' || stored === 'dark') {
+ return stored
+ }
+ return null
+}
+
+function applyTheme(theme: Theme) {
+ document.documentElement.setAttribute('data-theme', theme)
+}
+
+export function useDarkMode() {
+ const [theme, setThemeState] = useState(() => {
+ const stored = getStoredTheme()
+ return stored ?? getSystemTheme()
+ })
+
+ useEffect(() => {
+ applyTheme(theme)
+ localStorage.setItem(STORAGE_KEY, theme)
+ }, [theme])
+
+ useEffect(() => {
+ const stored = getStoredTheme()
+ if (stored !== null) return
+
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+
+ const handler = (e: MediaQueryListEvent) => {
+ setThemeState(e.matches ? 'dark' : 'light')
+ }
+
+ if (mediaQuery.addEventListener) {
+ mediaQuery.addEventListener('change', handler)
+ return () => mediaQuery.removeEventListener('change', handler)
+ }
+
+ mediaQuery.addListener(handler)
+ return () => mediaQuery.removeListener(handler)
+ }, [])
+
+ const toggleTheme = useCallback(() => {
+ setThemeState(prev => (prev === 'light' ? 'dark' : 'light'))
+ }, [])
+
+ const setTheme = useCallback((theme: Theme) => {
+ setThemeState(theme)
+ }, [])
+
+ return {
+ theme,
+ isDark: theme === 'dark',
+ toggleTheme,
+ setTheme
+ }
+}
+
+export default useDarkMode
diff --git a/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts b/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts
new file mode 100644
index 0000000000..872b1e4ad5
--- /dev/null
+++ b/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts
@@ -0,0 +1,78 @@
+import { useEffect, useState } from 'react'
+
+type Theme = 'light' | 'dark'
+
+const STORAGE_KEY = 'theme'
+const MEDIA_QUERY = '(prefers-color-scheme: dark)'
+
+function getStoredTheme(): Theme | null {
+ try {
+ const value = window.localStorage.getItem(STORAGE_KEY)
+ return value === 'light' || value === 'dark' ? value : null
+ } catch {
+ return null
+ }
+}
+
+function getSystemTheme(): Theme {
+ return window.matchMedia(MEDIA_QUERY).matches ? 'dark' : 'light'
+}
+
+export function useDarkMode() {
+ const [theme, setThemeState] = useState(() => {
+ if (typeof window === 'undefined') {
+ return 'light'
+ }
+
+ return getStoredTheme() ?? getSystemTheme()
+ })
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme)
+
+ try {
+ window.localStorage.setItem(STORAGE_KEY, theme)
+ } catch {
+ // ignore storage errors
+ }
+ }, [theme])
+
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ return
+ }
+
+ const mediaQuery = window.matchMedia(MEDIA_QUERY)
+
+ const handleChange = (event: MediaQueryListEvent) => {
+ const storedTheme = getStoredTheme()
+
+ if (!storedTheme) {
+ setThemeState(event.matches ? 'dark' : 'light')
+ }
+ }
+
+ if (typeof mediaQuery.addEventListener === 'function') {
+ mediaQuery.addEventListener('change', handleChange)
+ return () => mediaQuery.removeEventListener('change', handleChange)
+ }
+
+ mediaQuery.addListener(handleChange)
+ return () => mediaQuery.removeListener(handleChange)
+ }, [])
+
+ const setTheme = (nextTheme: Theme) => {
+ setThemeState(nextTheme)
+ }
+
+ const toggleTheme = () => {
+ setThemeState(prev => (prev === 'light' ? 'dark' : 'light'))
+ }
+
+ return {
+ theme,
+ isDark: theme === 'dark',
+ setTheme,
+ toggleTheme
+ }
+}
diff --git a/packages/cra-template-typescript/template/src/index.css b/packages/cra-template-typescript/template/src/index.css
index ec2585e8c0..a9f17f065c 100644
--- a/packages/cra-template-typescript/template/src/index.css
+++ b/packages/cra-template-typescript/template/src/index.css
@@ -1,13 +1,73 @@
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
+ 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
+ 'Helvetica Neue', sans-serif;
+
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+
+ background-color: #ffffff;
+ color: #333333;
+
+ transition: background-color 0.3s ease, color 0.3s ease;
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
+ font-family: source-code-pro, Menlo, Monaco, Consolas,
+ 'Courier New', monospace;
+}
+
+body[data-theme='dark'] {
+ background-color: #1a1a2e;
+ color: #eaeaea;
+}
+
+.app-header {
+ padding: 12px 24px;
+ background-color: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+ transition: background-color 0.3s ease;
+}
+
+body[data-theme='dark'] .app-header {
+ background-color: #16213e;
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+.theme-toggle {
+ background: none;
+ border: 2px solid #dee2e6;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+
+ cursor: pointer;
+
+ font-size: 1.1rem;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ transition: border-color 0.2s ease, transform 0.2s ease;
+}
+
+.theme-toggle:hover {
+ border-color: #61dafb;
+ transform: scale(1.05);
+}
+
+body[data-theme='dark'] .theme-toggle {
+ border-color: #495057;
}