Kurs JavaScript i React · Moduł 14: Zaawansowane stylowanie

Theming w Styled-components - Galaktyczny System Designu

5 min czytania
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

  1. Centralizuj kolory i rozmiary - nigdy nie hardcoduj wartości w komponentach, zawsze odwołuj się do motywu
  2. Użyj TypeScript - zdefiniuj typ dla motywu, aby mieć autouzupełnianie (w styled-components rozszerzasz interfejs DefaultTheme)
  3. Zachowaj spójność - wszystkie motywy powinny mieć te same klucze (colors, spacing, fontSize)
  4. 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. 1. Jaka jest rola komponentu ThemeProvider w styled-components?

  2. 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

Przydatne artykuły