Kurs JavaScript i React · Moduł 14: Zaawansowane stylowanie
Theming w Styled-components - Galaktyczny System Designu
W tej lekcji7
Wyobraź sobie flotę, w której każdy statek ma inny odcień fioletu na przyciskach alarmowych, bo każdy inżynier wpisał kolor z pamięci. Gdy dowództwo postanowi zmienić barwy floty, ktoś będzie musiał przeszukać setki plików i poprawić każdą wartość #7c4dff ręcznie. Tak wygląda projekt bez motywu. ThemeProvider w styled-components pozwala na stworzenie spójnego systemu kolorów, rozmiarów i typografii - jak ujednolicony interfejs dla całej floty kosmicznej.
Zanim zaczniemy, jedna uczciwa uwaga. W marcu 2025 opiekun biblioteki ogłosił, że styled-components przechodzi w tryb utrzymania: dostaje poprawki błędów, ale nie nowe funkcje, a do nowych projektów autor sam go nie poleca. Biblioteka (aktualnie wersja 6) nadal działa w ogromnej liczbie istniejących aplikacji, więc warto ją znać, a idea motywu, której się tu nauczysz, przenosi się jeden do jednego na CSS Variables i inne narzędzia.
ThemeProvider
ThemeProvider to komponent, który udostępnia motyw (theme) wszystkim komponentom wewnątrz. Pod spodem korzysta z kontekstu React, więc motyw dociera na dowolną głębokość bez przekazywania propsów. Sam motyw to zwykły obiekt JavaScript, w którym grupujemy wartości według kategorii: kolory, odstępy, zaokrąglenia i rozmiary czcionek:
1import { ThemeProvider } from 'styled-components';
2
3const spaceTheme = {
4 colors: {
5 primary: '#7c4dff',
6 secondary: '#64b5f6',
7 background: '#0a0a2e',
8 surface: '#1a1a4e',
9 text: '#e0e0e0',
10 success: '#00e676',
11 warning: '#ffa726',
12 danger: '#f44336',
13 },
14 spacing: {
15 xs: '4px',
16 sm: '8px',
17 md: '16px',
18 lg: '24px',
19 xl: '32px',
20 },
21 borderRadius: {
22 sm: '4px',
23 md: '8px',
24 lg: '12px',
25 round: '50%',
26 },
27 fontSize: {
28 sm: '12px',
29 md: '16px',
30 lg: '20px',
31 xl: '28px',
32 },
33};
34
35function App() {
36 return (
37 <ThemeProvider theme={spaceTheme}>
38 <Dashboard />
39 </ThemeProvider>
40 );
41}ThemeProvider niczego nie renderuje w DOM i nie zmienia wyglądu Dashboard sam z siebie. Tylko udostępnia obiekt, a komponenty muszą z niego skorzystać. Nazwy kluczy, takie jak colors czy spacing, wymyślasz sam, biblioteka nie narzuca żadnej struktury.
Używanie motywu w komponentach
Każdy styled-component ma automatyczny dostęp do motywu przez props.theme. Wewnątrz szablonu stylu wstawiasz funkcję w ${...}, a biblioteka wywoła ją z propsami komponentu:
1const Panel = styled.div`
2 background: ${props => props.theme.colors.surface};
3 border-radius: ${props => props.theme.borderRadius.lg};
4 padding: ${props => props.theme.spacing.lg};
5 color: ${props => props.theme.colors.text};
6`;
7
8const Heading = styled.h2`
9 color: ${({ theme }) => theme.colors.primary};
10 font-size: ${({ theme }) => theme.fontSize.xl};
11 margin-bottom: ${({ theme }) => theme.spacing.md};
12`;Zwróć uwagę na destrukturyzację - ({ theme }) to skrót od (props) => props.theme. Oba zapisy działają identycznie, ale polecam drugi, bo przy wielu wartościach jest krótszy i czytelniejszy. W żadnym z tych komponentów nie ma już ani jednego zakodowanego koloru.
Przełączanie motywów (Dark/Light)
Jedno z najpopularniejszych zastosowań ThemeProvider to przełączanie między ciemnym a jasnym motywem. Definiujemy dwa obiekty o tych samych kluczach, a stan isDark wybiera, który trafi do providera:
1const darkTheme = {
2 colors: {
3 background: '#0a0a2e',
4 surface: '#1a1a4e',
5 text: '#e0e0e0',
6 primary: '#7c4dff',
7 },
8};
9
10const lightTheme = {
11 colors: {
12 background: '#f5f5f5',
13 surface: '#ffffff',
14 text: '#212121',
15 primary: '#5c6bc0',
16 },
17};
18
19function App() {
20 const [isDark, setIsDark] = useState(true);
21 const theme = isDark ? darkTheme : lightTheme;
22
23 return (
24 <ThemeProvider theme={theme}>
25 <AppContainer>
26 <button onClick={() => setIsDark(!isDark)}>
27 Toggle Theme
28 </button>
29 <Dashboard />
30 </AppContainer>
31 </ThemeProvider>
32 );
33}Po kliknięciu zmienia się tylko obiekt przekazany do theme, a wszystkie styled-components przeliczają swoje style. Kod komponentów pozostaje nietknięty. To działa jedynie wtedy, gdy oba motywy mają identyczne klucze, bo brakujący klucz oznacza undefined w CSS.
Global Styles
createGlobalStyle pozwala na zdefiniowanie globalnych stylów CSS, które również mają dostęp do motywu. Zwraca komponent, który renderujesz raz, wewnątrz ThemeProvider:
1import { createGlobalStyle } from 'styled-components';
2
3const GlobalStyles = createGlobalStyle`
4 * {
5 margin: 0;
6 padding: 0;
7 box-sizing: border-box;
8 }
9
10 body {
11 background: ${({ theme }) => theme.colors.background};
12 color: ${({ theme }) => theme.colors.text};
13 font-family: 'Segoe UI', sans-serif;
14 }
15`;
16
17function App() {
18 return (
19 <ThemeProvider theme={spaceTheme}>
20 <GlobalStyles />
21 <Dashboard />
22 </ThemeProvider>
23 );
24}GlobalStyles musi stać wewnątrz providera, inaczej theme byłby pusty. Dzięki temu tło całej strony zmienia się razem z motywem, a nie tylko tło pojedynczych paneli.
Hook useTheme
Jeśli potrzebujesz dostępu do motywu w logice komponentu (nie tylko w stylach), użyj hooka useTheme. Zwraca ten sam obiekt, który przekazałeś do providera:
1import { useTheme } from 'styled-components';
2
3function StatusChart({ data }) {
4 const theme = useTheme();
5
6 // Użyj kolorów motywu w logice np. do Canvas, SVG, lub warunków
7 const chartColor = data.isHealthy
8 ? theme.colors.success
9 : theme.colors.danger;
10
11 return (
12 <div>
13 <svg width="100" height="100">
14 <circle cx="50" cy="50" r="40" fill={chartColor} />
15 </svg>
16 <p style={{ color: theme.colors.text }}>
17 Status: {data.isHealthy ? 'OK' : 'Alert'}
18 </p>
19 </div>
20 );
21}useTheme jest przydatny, gdy potrzebujesz wartości z motywu poza styled-components - na przykład do rysowania na Canvas, konfigurowania bibliotek wykresów lub warunkowej logiki opartej na kolorach motywu.
Zagnieżdżone motywy
ThemeProvider można zagnieżdżać, aby różne sekcje aplikacji miały różne motywy. Najbliższy provider wygrywa:
1function App() {
2 return (
3 <ThemeProvider theme={darkTheme}>
4 <Header />
5 <ThemeProvider theme={lightTheme}>
6 <ContentArea /> {/* Tu obowiązuje lightTheme */}
7 </ThemeProvider>
8 <Footer /> {/* Tu wraca darkTheme */}
9 </ThemeProvider>
10 );
11}To jak różne sekcje statku kosmicznego z różnymi trybami oświetlenia - mostek dowodzenia może być ciemny, a laboratorium jasne. Wewnętrzny provider może też dostać funkcję theme={outer => ({ ...outer, ... })}, która rozszerza motyw zewnętrzny zamiast go zastępować.
Najlepsze praktyki themingu
- Centralizuj kolory i rozmiary - nigdy nie hardcoduj wartości w komponentach, zawsze odwołuj się do motywu
- Użyj TypeScript - zdefiniuj typ dla motywu, aby mieć autouzupełnianie (w styled-components rozszerzasz interfejs
DefaultTheme) - Zachowaj spójność - wszystkie motywy powinny mieć te same klucze (colors, spacing, fontSize)
- Testuj oba motywy - upewnij się, że każdy komponent wygląda dobrze zarówno w ciemnym, jak i jasnym trybie
W kolejnej lekcji zobaczysz responsywne wzorce, a w projekcie głównym połączysz ten motyw z CSS Modules i Tailwindem w jednym pulpicie misji.
Pamiętaj: motyw to wspólna paleta floty, zmieniasz ją w jednym miejscu, a każdy statek od razu świeci nowymi barwami.
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
3
4// Dwa motywy -- ciemny (kosmiczny) i jasny
5const darkTheme = {
6 colors: {
7 primary: '#7c4dff',
8 secondary: '#64b5f6',
9 background: '#0a0a2e',
10 surface: '#1a1a4e',
11 text: '#e0e0e0',
12 textMuted: '#9e9e9e',
13 border: '#3949ab',
14 success: '#00e676',
15 warning: '#ffa726',
16 danger: '#f44336',
17 },
18 spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' },
19 borderRadius: { sm: '4px', md: '8px', lg: '12px' },
20 fontSize: { sm: '12px', md: '16px', lg: '20px', xl: '28px' },
21};
22
23const lightTheme = {
24 colors: {
25 primary: '#5c6bc0',
26 secondary: '#1976d2',
27 background: '#f5f5f5',
28 surface: '#ffffff',
29 text: '#212121',
30 textMuted: '#757575',
31 border: '#e0e0e0',
32 success: '#4caf50',
33 warning: '#ff9800',
34 danger: '#f44336',
35 },
36 spacing: darkTheme.spacing,
37 borderRadius: darkTheme.borderRadius,
38 fontSize: darkTheme.fontSize,
39};
40
41const GlobalStyles = createGlobalStyle`
42 * { margin: 0; padding: 0; box-sizing: border-box; }
43 body {
44 background: ${({ theme }) => theme.colors.background};
45 color: ${({ theme }) => theme.colors.text};
46 font-family: 'Segoe UI', sans-serif;
47 transition: background 0.3s ease, color 0.3s ease;
48 }
49`;
50
51const Container = styled.div`
52 max-width: 600px;
53 margin: 0 auto;
54 padding: ${({ theme }) => theme.spacing.lg};
55`;
56
57const Header = styled.header`
58 display: flex;
59 justify-content: space-between;
60 align-items: center;
61 margin-bottom: ${({ theme }) => theme.spacing.xl};
62`;
63
64const Title = styled.h1`
65 color: ${({ theme }) => theme.colors.primary};
66 font-size: ${({ theme }) => theme.fontSize.xl};
67`;
68
69const ThemeToggle = styled.button`
70 padding: ${({ theme }) => theme.spacing.sm} ${({ theme }) => theme.spacing.md};
71 background: ${({ theme }) => theme.colors.surface};
72 color: ${({ theme }) => theme.colors.text};
73 border: 2px solid ${({ theme }) => theme.colors.border};
74 border-radius: ${({ theme }) => theme.borderRadius.md};
75 cursor: pointer;
76 font-size: ${({ theme }) => theme.fontSize.md};
77 transition: all 0.2s ease;
78 &:hover { border-color: ${({ theme }) => theme.colors.primary}; }
79`;
80
81const Card = styled.div`
82 background: ${({ theme }) => theme.colors.surface};
83 border: 1px solid ${({ theme }) => theme.colors.border};
84 border-radius: ${({ theme }) => theme.borderRadius.lg};
85 padding: ${({ theme }) => theme.spacing.lg};
86 margin-bottom: ${({ theme }) => theme.spacing.md};
87 transition: all 0.3s ease;
88`;
89
90const CardTitle = styled.h3`
91 color: ${({ theme }) => theme.colors.primary};
92 margin-bottom: ${({ theme }) => theme.spacing.sm};
93`;
94
95const Badge = styled.span`
96 display: inline-block;
97 padding: 2px 10px;
98 border-radius: 12px;
99 font-size: ${({ theme }) => theme.fontSize.sm};
100 font-weight: bold;
101 background: ${({ theme, variant }) =>
102 variant === 'success' ? theme.colors.success + '33' :
103 variant === 'warning' ? theme.colors.warning + '33' :
104 theme.colors.danger + '33'};
105 color: ${({ theme, variant }) =>
106 variant === 'success' ? theme.colors.success :
107 variant === 'warning' ? theme.colors.warning :
108 theme.colors.danger};
109`;
110
111export default function App() {
112 const [isDark, setIsDark] = useState(true);
113
114 return (
115 <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
116 <GlobalStyles />
117 <Container>
118 <Header>
119 <Title>Theme Demo</Title>
120 <ThemeToggle onClick={() => setIsDark(!isDark)}>
121 {isDark ? 'Light' : 'Dark'}
122 </ThemeToggle>
123 </Header>
124
125 <Card>
126 <CardTitle>Silniki jonowe</CardTitle>
127 <p>Status: <Badge variant="success">Aktywne</Badge></p>
128 <p style={{ marginTop: '8px', fontSize: '14px' }}>
129 Pelna moc -- gotowe do skoku nadprzestrzennego.
130 </p>
131 </Card>
132
133 <Card>
134 <CardTitle>Oslony energetyczne</CardTitle>
135 <p>Status: <Badge variant="warning">Ostrzezenie</Badge></p>
136 <p style={{ marginTop: '8px', fontSize: '14px' }}>
137 Poziom energii: 42%. Wymagane doladowanie.
138 </p>
139 </Card>
140
141 <Card>
142 <CardTitle>System podtrzymania zycia</CardTitle>
143 <p>Status: <Badge variant="danger">Krytyczny</Badge></p>
144 <p style={{ marginTop: '8px', fontSize: '14px' }}>
145 Zapasy tlenu na 72h. Natychmiastowa interwencja!
146 </p>
147 </Card>
148 </Container>
149 </ThemeProvider>
150 );
151}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaka jest rola komponentu ThemeProvider w styled-components?
2. Do czego służy createGlobalStyle w styled-components?
Zadania praktyczne w grze
- Układanie w pionie
Ułóż kolejność elementów w prawidłowej hierarchii ThemeProvider:
- Edytor kodu
Zaimplementuj przełączanie motywów z ThemeProvider
- Klikanie w kolejności
Ułóż składnię dostępu do koloru primary z motywu w styled-component:
- Edytor kodu
Zaimplementuj nawigację z dynamicznym stylem aktywnego linku