diff --git a/packages/cra-template-typescript/template/src/App.css b/packages/cra-template-typescript/template/src/App.css
index 74b5e05345..c35fa58c06 100644
--- a/packages/cra-template-typescript/template/src/App.css
+++ b/packages/cra-template-typescript/template/src/App.css
@@ -14,24 +14,54 @@
}
.App-header {
- background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
- color: white;
+ transition: background-color 0.2s ease, color 0.2s ease;
}
.App-link {
color: #61dafb;
}
+.App-theme-toggle {
+ margin-top: 20px;
+ padding: 10px 20px;
+ font-size: 16px;
+ cursor: pointer;
+ border-radius: 4px;
+ border: none;
+ transition: background-color 0.2s ease, color 0.2s ease;
+}
+
+[data-theme='light'] .App-header {
+ background-color: #282c34;
+ color: white;
+}
+
+[data-theme='light'] .App-theme-toggle {
+ background-color: #61dafb;
+ color: #282c34;
+}
+
+[data-theme='dark'] .App-header {
+ background-color: #121212;
+ color: #f5f5f5;
+}
+
+[data-theme='dark'] .App-theme-toggle {
+ background-color: #f5f5f5;
+ color: #121212;
+}
+
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
+
to {
transform: rotate(360deg);
}
diff --git a/packages/cra-template-typescript/template/src/App.tsx b/packages/cra-template-typescript/template/src/App.tsx
index a53698aab3..4a190acba3 100644
--- a/packages/cra-template-typescript/template/src/App.tsx
+++ b/packages/cra-template-typescript/template/src/App.tsx
@@ -1,26 +1,17 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
+import { Header } from './components/Header'
+import './App.css'
-function App() {
- return (
-
- );
+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..a3b1361424
--- /dev/null
+++ b/packages/cra-template-typescript/template/src/components/Header.tsx
@@ -0,0 +1,33 @@
+import logo from '../logo.svg'
+import { useDarkMode } from '../hooks/useDarkMode'
+
+export function Header() {
+ const { theme, toggleTheme } = useDarkMode()
+
+ return (
+
+ )
+}
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/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts b/packages/cra-template-typescript/template/src/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts
new file mode 100644
index 0000000000..2c672859e1
--- /dev/null
+++ b/packages/cra-template-typescript/template/src/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts
@@ -0,0 +1,79 @@
+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
+ }
+}
+