W naszej kosmicznej podróży przez świat stylowania w React, docieramy do fascynującej technologii, która zaciera granice między kodem a stylem. Styled Components to jak zaawansowany statek międzygwiezdny, który łączy w sobie DNA dwóch światów - JavaScriptu i CSS - tworząc nową formę życia, idealnie dostosowaną do ekosystemu React.
Styled Components to jedna z najpopularniejszych bibliotek CSS-in-JS, która pozwala na pisanie faktycznego CSS bezpośrednio w plikach JavaScript/JSX, jednocześnie tworząc normalne komponenty React. To rewolucyjne podejście do stylowania pozwala nam myśleć o naszych komponentach jako w pełni zamkniętych, autonomicznych jednostkach, gdzie zarówno logika, jak i styl są zebrane w jednym miejscu.
Wyobraź sobie komponenty jak moduły statku kosmicznego - każdy zawiera zarówno swoją funkcję (silniki, systemy podtrzymywania życia, nawigacja), jak i swój wygląd (panele sterowania, wskaźniki, interfejsy) - wszystko hermetycznie zamknięte w jednej, spójnej całości.
Zanim rozpoczniemy naszą przygodę ze Styled Components, musimy zainstalować bibliotekę:
1npm install styled-components
2# lub
3yarn add styled-components
4# lub
5pnpm add styled-componentsDla użytkowników TypeScript, warto dodać również typy:
1npm install @types/styled-componentsTworzenie stylizowanych komponentów jest niezwykle intuicyjne. Wystarczy użyć funkcji
styled zaimportowanej z biblioteki, a następnie nazwę tagu HTML, do którego chcemy aplikować style:1import styled from 'styled-components';
2
3// Tworzenie stylizowanego przycisku
4const Button = styled.button`
5 background-color: #4a90e2;
6 color: white;
7 padding: 10px 15px;
8 border: none;
9 border-radius: 4px;
10 font-weight: bold;
11 cursor: pointer;
12 transition: all 0.3s ease;
13
14 &:hover {
15 background-color: #357ae8;
16 transform: translateY(-2px);
17 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
18 }
19
20 &:active {
21 transform: translateY(1px);
22 }
23`;
24
25// Użycie komponentu
26function App() {
27 return (
28 <div>
29 <h1>Centrum Dowodzenia</h1>
30 <Button>Uruchom procedurę startową</Button>
31 </div>
32 );
33}W powyższym przykładzie stworzyliśmy stylizowany komponent
Button, który działa jak normalny przycisk HTML, ale ma predefiniowane style. Zauważ charakterystyczną składnię z użyciem backticks (``), która pozwala na pisanie wieloliniowego CSS. Co więcej, możemy używać selektora & jak w Sass, aby odwoływać się do samego komponentu.Możemy rozszerzać istniejące komponenty stylizowane, dodając do nich nowe style:
1// Bazowy przycisk
2const Button = styled.button`
3 padding: 10px 15px;
4 border: none;
5 border-radius: 4px;
6 font-weight: bold;
7 cursor: pointer;
8 transition: all 0.3s ease;
9`;
10
11// Rozszerzenie bazowego przycisku
12const PrimaryButton = styled(Button)`
13 background-color: #4a90e2;
14 color: white;
15
16 &:hover {
17 background-color: #357ae8;
18 }
19`;
20
21// Kolejne rozszerzenie
22const DangerButton = styled(Button)`
23 background-color: #e74c3c;
24 color: white;
25
26 &:hover {
27 background-color: #c0392b;
28 }
29`;Jedną z najpotężniejszych funkcji Styled Components jest możliwość dynamicznego stylowania w oparciu o propsy przekazywane do komponentu:
1const Button = styled.button`
2 padding: 10px 15px;
3 border: none;
4 border-radius: 4px;
5 font-weight: bold;
6 cursor: pointer;
7 transition: all 0.3s ease;
8
9 /* Dynamiczne kolory na podstawie props */
10 background-color: props => props.primary ? '#4a90e2' : props.danger ? '#e74c3c' : props.success ? '#2ecc71' : '#95a5a6';
11 color: white;
12
13 /* Dynamiczny rozmiar */
14 font-size: props => (props.large ? '18px' : '14px');
15 padding: props => (props.large ? '12px 20px' : '8px 12px');
16
17 /* Zablokowany przycisk */
18 opacity: props => (props.disabled ? 0.5 : 1);
19 cursor: props => (props.disabled ? 'not-allowed' : 'pointer');
20
21 &:hover {
22 ${props => !props.disabled && `
23 transform: translateY(-2px);
24 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
25 `}
26 }
27`;
28
29// Użycie z różnymi propsami
30function ActionPanel() {
31 return (
32 <div>
33 <Button>Normalny</Button>
34 <Button primary>Podstawowy</Button>
35 <Button danger>Usuń</Button>
36 <Button success large>Zapisz</Button>
37 <Button disabled>Niedostępny</Button>
38 </div>
39 );
40}W przykładzie powyżej, styl przycisku zmienia się w zależności od przekazanych propsów. Możemy przekazywać dowolne propsy do naszych stylizowanych komponentów, a następnie używać ich w wyrażeniach wewnątrz literałów szablonowych.
Styled Components automatycznie przekazują wszystkie nieużyte propsy do elementu DOM:
1// Ten przycisk będzie miał typ "submit" i obsługę onClick
2function FormSubmit() {
3 return (
4 <Button
5 type="submit"
6 onClick={() => console.log('Formularz wysłany!')}
7 >
8 Wyślij
9 </Button>
10 );
11}Możemy również stylizować istniejące komponenty React, o ile przyjmują one prop
className:1// Zwykły komponent React
2function CustomInput({ className, ...props }) {
3 return (
4 <input
5 className={className}
6 {...props}
7 />
8 );
9}
10
11// Stylizacja istniejącego komponentu
12const StyledCustomInput = styled(CustomInput)`
13 border: 2px solid #3498db;
14 border-radius: 4px;
15 padding: 8px 12px;
16 width: 100%;
17 font-size: 16px;
18
19 &:focus {
20 outline: none;
21 border-color: #2980b9;
22 box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
23 }
24`;Styled Components oferuje również komponent
GlobalStyle do definiowania globalnych stylów:1import { createGlobalStyle } from 'styled-components';
2
3const GlobalStyle = createGlobalStyle`
4 /* Reset CSS */
5 *, *::before, *::after {
6 box-sizing: border-box;
7 margin: 0;
8 padding: 0;
9 }
10
11 /* Zmienne globalne */
12 :root {
13 --primary-color: #4a90e2;
14 --secondary-color: #8a2be2;
15 --success-color: #2ecc71;
16 --danger-color: #e74c3c;
17 --warning-color: #f1c40f;
18 --text-light: #ecf0f1;
19 --text-dark: #2c3e50;
20 --background-dark: #1a1a2e;
21 --background-light: #f5f5f5;
22 }
23
24 body {
25 font-family: 'Space Grotesk', sans-serif;
26 line-height: 1.6;
27 background-color: var(--background-dark);
28 color: var(--text-light);
29 }
30
31 a {
32 color: var(--primary-color);
33 text-decoration: none;
34 }
35
36 button {
37 font-family: inherit;
38 }
39`;
40
41// Użycie GlobalStyle w aplikacji
42function App() {
43 return (
44 <>
45 <GlobalStyle />
46 <div>
47 {/* Zawartość aplikacji */}
48 </div>
49 </>
50 );
51}Jedną z największych zalet Styled Components jest wsparcie dla tematów:
1import { ThemeProvider } from 'styled-components';
2
3// Definicja motywu
4const darkTheme = {
5 colors: {
6 primary: '#4a90e2',
7 secondary: '#8a2be2',
8 success: '#2ecc71',
9 danger: '#e74c3c',
10 warning: '#f1c40f',
11 background: '#1a1a2e',
12 text: '#ecf0f1',
13 muted: '#95a5a6'
14 },
15 fonts: {
16 body: "'Space Grotesk', sans-serif",
17 heading: "'Nova Mono', monospace"
18 },
19 spacing: {
20 xs: '4px',
21 sm: '8px',
22 md: '16px',
23 lg: '24px',
24 xl: '32px'
25 },
26 borderRadius: {
27 small: '4px',
28 medium: '8px',
29 large: '12px',
30 round: '50%'
31 },
32 shadows: {
33 small: '0 2px 4px rgba(0, 0, 0, 0.1)',
34 medium: '0 4px 8px rgba(0, 0, 0, 0.2)',
35 large: '0 8px 16px rgba(0, 0, 0, 0.3)'
36 }
37};
38
39// Alternatywny motyw
40const lightTheme = {
41 colors: {
42 primary: '#3498db',
43 secondary: '#9b59b6',
44 success: '#27ae60',
45 danger: '#c0392b',
46 warning: '#f39c12',
47 background: '#f5f5f5',
48 text: '#2c3e50',
49 muted: '#7f8c8d'
50 },
51 // Pozostałe właściwości takie same jak w darkTheme
52 fonts: darkTheme.fonts,
53 spacing: darkTheme.spacing,
54 borderRadius: darkTheme.borderRadius,
55 shadows: darkTheme.shadows
56};
57
58// Stylizowany komponent używający motywu
59const Card = styled.div`
60 background-color: props => props.theme.colors.background === '#1a1a2e' ? '#252941' : '#ffffff';
61 color: props => props.theme.colors.text;
62 border-radius: props => props.theme.borderRadius.medium;
63 padding: props => props.theme.spacing.md;
64 box-shadow: props => props.theme.shadows.small;
65
66 transition: all 0.3s ease;
67
68 &:hover {
69 box-shadow: props => props.theme.shadows.medium;
70 }
71`;
72
73// Stylizowany przycisk używający motywu
74const Button = styled.button`
75 background-color: ${props => props.theme.colors.primary};
76 color: white;
77 padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md};
78 border: none;
79 border-radius: ${props => props.theme.borderRadius.small};
80 font-family: ${props => props.theme.fonts.body};
81 cursor: pointer;
82
83 &:hover {
84 background-color: ${props => {
85 const color = props.theme.colors.primary;
86 // Prostą funkcję ciemniejszego koloru
87 return color.replace(/rgb((d+), (d+), (d+))/, (_, r, g, b) =>
88 `rgb(${Math.max(0, r - 20)}, ${Math.max(0, g - 20)}, ${Math.max(0, b - 20)})`
89 );
90 }};
91 }
92`;
93
94// Użycie ThemeProvider i komponentów tematycznych
95function App() {
96 const [darkMode, setDarkMode] = useState(true);
97 const theme = darkMode ? darkTheme : lightTheme;
98
99 return (
100 <ThemeProvider theme={theme}>
101 <GlobalStyle />
102 <AppContainer>
103 <Header>
104 <h1>Kosmiczne Centrum Dowodzenia</h1>
105 <Button onClick={() => setDarkMode(!darkMode)}>
106 Przełącz tryb {darkMode ? "jasny" : "ciemny"}
107 </Button>
108 </Header>
109
110 <Card>
111 <h2>Panel Kontrolny</h2>
112 <p>Tutaj możesz zarządzać swoją kosmiczną misją.</p>
113 <Button>Uruchom</Button>
114 </Card>
115 </AppContainer>
116 </ThemeProvider>
117 );
118}Styled Components pozwala na łatwe tworzenie animacji z użyciem helper funkcji
keyframes:1import styled, { keyframes } from 'styled-components';
2
3// Definicja animacji
4const pulse = keyframes`
5 0% {
6 opacity: 1;
7 transform: scale(1);
8 }
9 50% {
10 opacity: 0.8;
11 transform: scale(1.05);
12 }
13 100% {
14 opacity: 1;
15 transform: scale(1);
16 }
17`;
18
19const rotate = keyframes`
20 from {
21 transform: rotate(0deg);
22 }
23 to {
24 transform: rotate(360deg);
25 }
26`;
27
28// Użycie animacji w komponencie
29const PulsingButton = styled.button`
30 background-color: #e74c3c;
31 color: white;
32 padding: 12px 24px;
33 border: none;
34 border-radius: 4px;
35 font-weight: bold;
36
37 /* Aplikacja animacji */
38 animation: ${pulse} 2s infinite ease-in-out;
39`;
40
41const Spinner = styled.div`
42 width: 40px;
43 height: 40px;
44 border: 4px solid rgba(255, 255, 255, 0.3);
45 border-top: 4px solid #3498db;
46 border-radius: 50%;
47
48 /* Animacja rotacji */
49 animation: ${rotate} 1s linear infinite;
50`;
51
52// Animacje warunkowe
53const AlertBadge = styled.span`
54 display: inline-block;
55 padding: 4px 8px;
56 background-color: red;
57 color: white;
58 border-radius: 10px;
59 font-size: 12px;
60
61 /* Animacja tylko gdy jest ważny alert */
62 animation: ${props => props.critical ?
63 css`${pulse} 1s infinite` : 'none'};
64`;Styled Components obsługują wszystkie pseudoelementy i pseudoklasy CSS:
1const HoverCard = styled.div`
2 position: relative;
3 padding: 20px;
4 background-color: #252941;
5 border-radius: 8px;
6 transition: all 0.3s ease;
7
8 /* Pseudoklasy */
9 &:hover {
10 transform: translateY(-5px);
11 box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
12 }
13
14 &:active {
15 transform: translateY(-2px);
16 }
17
18 /* Pseudoelementy */
19 &::before {
20 content: "";
21 position: absolute;
22 top: -10px;
23 left: -10px;
24 right: -10px;
25 bottom: -10px;
26 border-radius: 12px;
27 background: linear-gradient(45deg, #4a90e2, #8a2be2);
28 z-index: -1;
29 opacity: 0;
30 transition: opacity 0.3s ease;
31 }
32
33 &:hover::before {
34 opacity: 1;
35 }
36
37 /* Zagnieżdżone selektory */
38 h3 {
39 margin-bottom: 10px;
40 color: #ecf0f1;
41 }
42
43 p {
44 color: #bdc3c7;
45 }
46
47 /* Złożone selektory */
48 &:hover h3 {
49 color: #4a90e2;
50 }
51`;Styled Components doskonale obsługuje media queries, umożliwiając tworzenie responsywnych komponentów:
1const ResponsiveContainer = styled.div`
2 padding: 20px;
3 background-color: #252941;
4 border-radius: 8px;
5
6 /* Bazowa szerokość */
7 width: 100%;
8
9 /* Media queries */
10 @media (min-width: 768px) {
11 width: 750px;
12 margin: 0 auto;
13 }
14
15 @media (min-width: 992px) {
16 width: 970px;
17 }
18
19 @media (min-width: 1200px) {
20 width: 1170px;
21 }
22
23 /* Responsywne zmiany stylów */
24 h1 {
25 font-size: 24px;
26
27 @media (min-width: 768px) {
28 font-size: 32px;
29 }
30 }
31
32 /* Możemy używać media queries wewnątrz warunków */
33 display: ${props => props.hidden ?
34 `
35 none;
36 @media (min-width: 768px) {
37 display: block;
38 }
39 ` : 'block'
40 };
41`;Styled Components dostarcza kilka użytecznych narzędzi, jak
css do wielokrotnego używania fragmentów stylu:1import styled, { css } from 'styled-components';
2
3// Tworzenie reużywalnych fragmentów stylów
4const flexCenter = css`
5 display: flex;
6 justify-content: center;
7 align-items: center;
8`;
9
10const buttonBase = css`
11 padding: 10px 15px;
12 border: none;
13 border-radius: 4px;
14 font-weight: bold;
15 cursor: pointer;
16 transition: all 0.3s ease;
17`;
18
19const dangerStyles = css`
20 background-color: #e74c3c;
21 color: white;
22
23 &:hover {
24 background-color: #c0392b;
25 }
26`;
27
28// Użycie fragmentów w komponentach
29const FlexContainer = styled.div`
30 ${flexCenter}
31 gap: 20px;
32 flex-wrap: wrap;
33`;
34
35const Button = styled.button`
36 ${buttonBase}
37
38 background-color: #4a90e2;
39 color: white;
40
41 &:hover {
42 background-color: #357ae8;
43 }
44
45 ${props => props.danger && dangerStyles}
46`;Jedną z najlepszych praktyk w Styled Components jest tworzenie reużywalnych komponentów bazowych z wariacjami:
1// Bazowy komponent przycisku
2const BaseButton = styled.button`
3 padding: 10px 15px;
4 border: none;
5 border-radius: 4px;
6 font-weight: bold;
7 cursor: pointer;
8 transition: all 0.3s ease;
9 font-size: 14px;
10`;
11
12// Warianty przycisków
13const PrimaryButton = styled(BaseButton)`
14 background-color: ${props => props.theme.colors.primary};
15 color: white;
16
17 &:hover {
18 background-color: ${props => darken(0.1, props.theme.colors.primary)};
19 }
20`;
21
22const SecondaryButton = styled(BaseButton)`
23 background-color: transparent;
24 color: ${props => props.theme.colors.primary};
25 border: 2px solid ${props => props.theme.colors.primary};
26
27 &:hover {
28 background-color: ${props => rgba(props.theme.colors.primary, 0.1)};
29 }
30`;
31
32const DangerButton = styled(BaseButton)`
33 background-color: ${props => props.theme.colors.danger};
34 color: white;
35
36 &:hover {
37 background-color: ${props => darken(0.1, props.theme.colors.danger)};
38 }
39`;
40
41// Rozmiary przycisków
42const SmallButton = css`
43 padding: 6px 10px;
44 font-size: 12px;
45`;
46
47const LargeButton = css`
48 padding: 12px 20px;
49 font-size: 16px;
50`;
51
52// Komponent wyższego rzędu dla rozmiaru
53const sizedButton = (Component) => styled(Component)`
54 ${props => props.small && SmallButton}
55 ${props => props.large && LargeButton}
56`;
57
58// Tworzenie wariantów z rozmiarami
59const SizedPrimaryButton = sizedButton(PrimaryButton);
60const SizedSecondaryButton = sizedButton(SecondaryButton);
61const SizedDangerButton = sizedButton(DangerButton);
62
63// Użycie
64function ActionPanel() {
65 return (
66 <div>
67 <SizedPrimaryButton>Normalne</SizedPrimaryButton>
68 <SizedPrimaryButton small>Małe</SizedPrimaryButton>
69 <SizedPrimaryButton large>Duże</SizedPrimaryButton>
70
71 <SizedSecondaryButton>Normalne</SizedSecondaryButton>
72 <SizedDangerButton large>Duży alarm</SizedDangerButton>
73 </div>
74 );
75}asStyled Components pozwala na zmianę elementu HTML używanego przez komponent za pomocą propsu
as:1const Button = styled.button`
2 background-color: #4a90e2;
3 color: white;
4 padding: 10px 15px;
5 border: none;
6 border-radius: 4px;
7 font-weight: bold;
8 cursor: pointer;
9 display: inline-flex;
10 align-items: center;
11 justify-content: center;
12 text-decoration: none;
13`;
14
15function App() {
16 return (
17 <div>
18 <Button>Normalny przycisk</Button>
19
20 {/* Renderowanie jako link */}
21 <Button as="a" href="/destination">
22 Przycisk jako link
23 </Button>
24
25 {/* Renderowanie jako inny element */}
26 <Button as="div" role="button" tabIndex={0}>
27 Przycisk jako div
28 </Button>
29
30 {/* Możemy użyć innego komponentu React */}
31 <Button as={Link} to="/dashboard">
32 Przycisk jako router Link
33 </Button>
34 </div>
35 );
36}Styled Components pozwala na bardzo szczegółową warunkową stylizację:
1const AlertBox = styled.div`
2 padding: 15px;
3 border-radius: 8px;
4 margin-bottom: 20px;
5 display: flex;
6 align-items: center;
7
8 /* Zmiana koloru tła na podstawie typu alertu */
9 background-color: ${props =>
10 props.type === 'success' ? '#d4edda' :
11 props.type === 'danger' ? '#f8d7da' :
12 props.type === 'warning' ? '#fff3cd' :
13 props.type === 'info' ? '#d1ecf1' :
14 '#e2e3e5'
15 };
16
17 /* Zmiana koloru tekstu */
18 color: ${props =>
19 props.type === 'success' ? '#155724' :
20 props.type === 'danger' ? '#721c24' :
21 props.type === 'warning' ? '#856404' :
22 props.type === 'info' ? '#0c5460' :
23 '#383d41'
24 };
25
26 /* Zmiana obramowania */
27 border: 1px solid ${props =>
28 props.type === 'success' ? '#c3e6cb' :
29 props.type === 'danger' ? '#f5c6cb' :
30 props.type === 'warning' ? '#ffeeba' :
31 props.type === 'info' ? '#bee5eb' :
32 '#d6d8db'
33 };
34
35 /* Zmiana ikon */
36 &::before {
37 content: "${props =>
38 props.type === 'success' ? '✅' :
39 props.type === 'danger' ? '❌' :
40 props.type === 'warning' ? '⚠️' :
41 props.type === 'info' ? 'ℹ️' :
42 'ℹ️'
43 }";
44 margin-right: 10px;
45 font-size: 18px;
46 }
47
48 /* Możemy również używać zagnieżdżonych warunków i funkcji */
49 ${props => {
50 if (props.dismissable) {
51 return `
52 padding-right: 40px;
53 position: relative;
54 `;
55 }
56 }}
57`;
58
59// Przycisk zamknięcia dla odrzucalnych alertów
60const CloseButton = styled.button`
61 position: absolute;
62 top: 10px;
63 right: 10px;
64 background: none;
65 border: none;
66 font-size: 20px;
67 cursor: pointer;
68 color: inherit;
69 opacity: 0.5;
70
71 &:hover {
72 opacity: 1;
73 }
74`;
75
76// Użycie komponentu
77function Notifications() {
78 return (
79 <div>
80 <AlertBox type="success">
81 Misja zakończona powodzeniem!
82 </AlertBox>
83
84 <AlertBox type="danger" dismissable>
85 Uwaga: Wykryto awarię systemu!
86 <CloseButton>×</CloseButton>
87 </AlertBox>
88
89 <AlertBox type="warning">
90 Ostrzeżenie: Niski poziom paliwa.
91 </AlertBox>
92
93 <AlertBox type="info">
94 Informacja: Zbliżasz się do stacji kosmicznej.
95 </AlertBox>
96 </div>
97 );
98}Zobaczmy, jak można stworzyć bardziej złożony interfejs używając Styled Components:
1import React, { useState } from 'react';
2import styled, { ThemeProvider, createGlobalStyle, keyframes } from 'styled-components';
3
4// ========== Theme Definition ==========
5const theme = {
6 colors: {
7 primary: '#4a90e2',
8 secondary: '#8a2be2',
9 success: '#2ecc71',
10 danger: '#e74c3c',
11 warning: '#f1c40f',
12 info: '#3498db',
13 background: '#0f1429',
14 cardBg: '#1a1f3c',
15 text: '#ecf0f1',
16 textMuted: '#95a5a6',
17 border: '#2c3e50'
18 },
19 spacing: {
20 xs: '4px',
21 sm: '8px',
22 md: '16px',
23 lg: '24px',
24 xl: '32px'
25 },
26 fonts: {
27 body: "'Space Grotesk', sans-serif",
28 mono: "'JetBrains Mono', monospace"
29 },
30 borderRadius: {
31 sm: '4px',
32 md: '8px',
33 lg: '16px'
34 }
35};
36
37// ========== Global Style ==========
38const GlobalStyle = createGlobalStyle`
39 * {
40 box-sizing: border-box;
41 margin: 0;
42 padding: 0;
43 }
44
45 body {
46 font-family: ${props => props.theme.fonts.body};
47 background-color: ${props => props.theme.colors.background};
48 color: ${props => props.theme.colors.text};
49 line-height: 1.6;
50 }
51
52 h1, h2, h3, h4, h5, h6 {
53 margin-bottom: ${props => props.theme.spacing.md};
54 }
55`;
56
57// ========== Animations ==========
58const fadeIn = keyframes`
59 from {
60 opacity: 0;
61 transform: translateY(10px);
62 }
63 to {
64 opacity: 1;
65 transform: translateY(0);
66 }
67`;
68
69const pulse = keyframes`
70 0% {
71 transform: scale(1);
72 }
73 50% {
74 transform: scale(1.05);
75 }
76 100% {
77 transform: scale(1);
78 }
79`;
80
81const rotate = keyframes`
82 from {
83 transform: rotate(0deg);
84 }
85 to {
86 transform: rotate(360deg);
87 }
88`;
89
90// ========== Layout Components ==========
91const AppContainer = styled.div`
92 max-width: 1200px;
93 margin: 0 auto;
94 padding: ${props => props.theme.spacing.lg};
95`;
96
97const Header = styled.header`
98 display: flex;
99 justify-content: space-between;
100 align-items: center;
101 margin-bottom: ${props => props.theme.spacing.xl};
102 padding-bottom: ${props => props.theme.spacing.md};
103 border-bottom: 1px solid ${props => props.theme.colors.border};
104
105 animation: ${fadeIn} 0.5s ease-out forwards;
106`;
107
108const MainTitle = styled.h1`
109 color: ${props => props.theme.colors.primary};
110 font-size: 28px;
111 display: flex;
112 align-items: center;
113
114 &::before {
115 content: "🚀";
116 margin-right: ${props => props.theme.spacing.sm};
117 }
118`;
119
120const Grid = styled.div`
121 display: grid;
122 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
123 gap: ${props => props.theme.spacing.lg};
124 margin-bottom: ${props => props.theme.spacing.xl};
125
126 animation: ${fadeIn} 0.5s ease-out forwards;
127 animation-delay: 0.2s;
128 opacity: 0;
129`;
130
131const WideSection = styled.section`
132 grid-column: 1 / -1;
133 animation: ${fadeIn} 0.5s ease-out forwards;
134 animation-delay: 0.4s;
135 opacity: 0;
136`;
137
138// ========== Card Components ==========
139const Card = styled.div`
140 background-color: ${props => props.theme.colors.cardBg};
141 border-radius: ${props => props.theme.borderRadius.md};
142 padding: ${props => props.theme.spacing.lg};
143 box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
144
145 ${props => props.interactive && `
146 cursor: pointer;
147 transition: all 0.3s ease;
148
149 &:hover {
150 transform: translateY(-5px);
151 box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
152 }
153 `}
154
155 ${props => props.primary && `
156 border-top: 4px solid ${props.theme.colors.primary};
157 `}
158
159 ${props => props.warning && `
160 border-top: 4px solid ${props.theme.colors.warning};
161 `}
162
163 ${props => props.danger && `
164 border-top: 4px solid ${props.theme.colors.danger};
165 `}
166
167 ${props => props.success && `
168 border-top: 4px solid ${props.theme.colors.success};
169 `}
170`;
171
172const CardTitle = styled.h2`
173 font-size: 18px;
174 display: flex;
175 align-items: center;
176 margin-bottom: ${props => props.theme.spacing.md};
177
178 ${props => props.icon && `
179 &::before {
180 content: "${props.icon}";
181 margin-right: ${props.theme.spacing.sm};
182 }
183 `}
184`;
185
186const CardContent = styled.div`
187 margin-bottom: ${props => props.theme.spacing.md};
188`;
189
190// ========== Button Components ==========
191const Button = styled.button`
192 background-color: ${props =>
193 props.primary ? props.theme.colors.primary :
194 props.danger ? props.theme.colors.danger :
195 props.success ? props.theme.colors.success :
196 props.warning ? props.theme.colors.warning :
197 '#95a5a6'
198 };
199 color: white;
200 border: none;
201 border-radius: ${props => props.theme.borderRadius.sm};
202 padding: ${props => props.small ? '6px 12px' : '10px 20px'};
203 font-family: ${props => props.theme.fonts.body};
204 font-size: ${props => props.small ? '12px' : '14px'};
205 font-weight: bold;
206 cursor: pointer;
207 transition: all 0.3s ease;
208
209 &:hover {
210 filter: brightness(1.1);
211 transform: translateY(-2px);
212 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
213 }
214
215 &:active {
216 transform: translateY(0);
217 }
218
219 ${props => props.ghost && `
220 background-color: transparent;
221 color: ${
222 props.primary ? props.theme.colors.primary :
223 props.danger ? props.theme.colors.danger :
224 props.success ? props.theme.colors.success :
225 props.warning ? props.theme.colors.warning :
226 props.theme.colors.text
227 };
228 border: 1px solid currentColor;
229
230 &:hover {
231 background-color: rgba(255, 255, 255, 0.05);
232 }
233 `}
234
235 ${props => props.icon && `
236 display: inline-flex;
237 align-items: center;
238
239 &::before {
240 content: "${props.icon}";
241 margin-right: 8px;
242 }
243 `}
244
245 ${props => props.block && `
246 display: block;
247 width: 100%;
248 `}
249
250 ${props => props.disabled && `
251 opacity: 0.5;
252 cursor: not-allowed;
253 pointer-events: none;
254 `}
255
256 & + & {
257 margin-left: props => props.theme.spacing.sm;
258 }
259`;
260
261// ========== Data Display Components ==========
262const Statistic = styled.div`
263 text-align: center;
264`;
265
266const StatValue = styled.div`
267 font-size: 36px;
268 font-weight: bold;
269 color: props => props.primary
270 ? props.theme.colors.primary
271 : props.danger
272 ? props.theme.colors.danger
273 : props.success
274 ? props.theme.colors.success
275 : props.warning
276 ? props.theme.colors.warning
277 : props.theme.colors.text;
278 margin-bottom: props => props.theme.spacing.xs;
279
280 ${props => props.pulse && `
281 animation: ${pulse} 2s infinite ease-in-out;
282 `}
283`;
284
285const StatLabel = styled.div`
286 font-size: 14px;
287 color: props => props.theme.colors.textMuted;
288 text-transform: uppercase;
289`;
290
291const ProgressBar = styled.div`
292 height: 8px;
293 background-color: rgba(0, 0, 0, 0.2);
294 border-radius: 4px;
295 overflow: hidden;
296 margin-bottom: props => props.theme.spacing.md;
297
298 &::after {
299 content: '';
300 display: block;
301 height: 100%;
302 width: ${props => `${props.value}%`};
303 background-color: props => props.primary
304 ? props.theme.colors.primary
305 : props.danger
306 ? props.theme.colors.danger
307 : props.success
308 ? props.theme.colors.success
309 : props.warning
310 ? props.theme.colors.warning
311 : props.theme.colors.primary;
312 transition: width 0.5s ease-in-out;
313 }
314`;
315
316const StatusIndicator = styled.span`
317 display: inline-block;
318 width: 12px;
319 height: 12px;
320 border-radius: 50%;
321 margin-right: props => props.theme.spacing.sm;
322 background-color: props => props.status === 'online'
323 ? props.theme.colors.success
324 : props.status === 'warning'
325 ? props.theme.colors.warning
326 : props.status === 'critical'
327 ? props.theme.colors.danger
328 : props.status === 'offline'
329 ? '#95a5a6'
330 : props.theme.colors.info;
331
332 ${props => props.status === 'online' && `
333 box-shadow: 0 0 10px ${props.theme.colors.success};
334 `}
335
336 ${props => props.status === 'critical' && `
337 animation: ${pulse} 1s infinite;
338 `}
339`;
340
341// ========== Table Components ==========
342const Table = styled.table`
343 width: 100%;
344 border-collapse: collapse;
345`;
346
347const TableHeader = styled.th`
348 text-align: left;
349 padding: ${props => props.theme.spacing.sm};
350 border-bottom: 2px solid ${props => props.theme.colors.border};
351 color: ${props => props.theme.colors.textMuted};
352 font-weight: 500;
353`;
354
355const TableCell = styled.td`
356 padding: ${props => props.theme.spacing.sm};
357 border-bottom: 1px solid rgba(255, 255, 255, 0.1);
358`;
359
360const TableRow = styled.tr`
361 transition: background-color 0.2s ease;
362
363 &:hover {
364 background-color: rgba(255, 255, 255, 0.05);
365 }
366`;
367
368// ========== Loading Spinner ==========
369const Spinner = styled.div`
370 display: inline-block;
371 width: 24px;
372 height: 24px;
373 border: 3px solid rgba(255, 255, 255, 0.3);
374 border-radius: 50%;
375 border-top-color: ${props => props.theme.colors.primary};
376 animation: ${rotate} 1s linear infinite;
377 margin-right: ${props => props.theme.spacing.sm};
378`;
379
380// ========== Application Component ==========
381function SpaceDashboard() {
382 const [systemStatus, setSystemStatus] = useState('online');
383 const [fuelLevel, setFuelLevel] = useState(72);
384 const [oxygenLevel, setOxygenLevel] = useState(93);
385 const [alerts, setAlerts] = useState([
386 { id: 1, message: "Zmiana ciśnienia w module C", severity: "warning", time: "10:23" },
387 { id: 2, message: "Aktualizacja nawigacji zainstalowana", severity: "info", time: "09:15" },
388 { id: 3, message: "Cząsteczki mikrometeoroidów wykryte", severity: "warning", time: "07:42" },
389 ]);
390
391 const crewMembers = [
392 { id: 1, name: "Alex Chen", role: "Kapitan", status: "Mostek", avatar: "👨🚀" },
393 { id: 2, name: "Maria Rodriguez", role: "Inżynier", status: "Maszynownia", avatar: "👩🚀" },
394 { id: 3, name: "Jackson Lee", role: "Nawigator", status: "Mostek", avatar: "👨🚀" },
395 { id: 4, name: "Sophia Kim", role: "Lekarz", status: "Laboratorium", avatar: "👩🚀" },
396 ];
397
398 const toggleSystemStatus = () => {
399 setSystemStatus(systemStatus === 'online' ? 'warning' : 'online');
400 };
401
402 return (
403 <ThemeProvider theme={theme}>
404 <GlobalStyle />
405 <AppContainer>
406 <Header>
407 <MainTitle>Kosmiczne Centrum Dowodzenia</MainTitle>
408 <div>
409 <Button primary icon="🚀" onClick={toggleSystemStatus}>
410 System {systemStatus === 'online' ? 'Online' : 'Uwaga'}
411 </Button>
412 </div>
413 </Header>
414
415 <Grid>
416 {/* System Status Card */}
417 <Card primary>
418 <CardTitle icon="🛰️">Status Systemów</CardTitle>
419 <CardContent>
420 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
421 <div>
422 <StatusIndicator status={systemStatus} />
423 Status główny: {
424 systemStatus === 'online' ? 'Wszystkie systemy sprawne' :
425 systemStatus === 'warning' ? 'Uwaga - sprawdź alerty' :
426 'Usterka'
427 }
428 </div>
429 </div>
430
431 <div>
432 <div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'space-between' }}>
433 <span>Poziom paliwa</span>
434 <span>{fuelLevel}%</span>
435 </div>
436 <ProgressBar
437 value={fuelLevel}
438 warning={fuelLevel < 50}
439 danger={fuelLevel < 20}
440 success={fuelLevel >= 50}
441 />
442
443 <div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'space-between' }}>
444 <span>Poziom tlenu</span>
445 <span>{oxygenLevel}%</span>
446 </div>
447 <ProgressBar
448 value={oxygenLevel}
449 warning={oxygenLevel < 70}
450 danger={oxygenLevel < 40}
451 success={oxygenLevel >= 70}
452 />
453 </div>
454 </CardContent>
455 <Button ghost primary block>Szczegółowe statystyki</Button>
456 </Card>
457
458 {/* Crew Status Card */}
459 <Card success>
460 <CardTitle icon="👩🚀">Status Załogi</CardTitle>
461 <CardContent>
462 <Table>
463 <thead>
464 <tr>
465 <TableHeader></TableHeader>
466 <TableHeader>Imię</TableHeader>
467 <TableHeader>Lokalizacja</TableHeader>
468 </tr>
469 </thead>
470 <tbody>
471 {crewMembers.map(member => (
472 <TableRow key={member.id}>
473 <TableCell>{member.avatar}</TableCell>
474 <TableCell>
475 {member.name}
476 <div style={{ fontSize: '12px', color: theme.colors.textMuted }}>
477 {member.role}
478 </div>
479 </TableCell>
480 <TableCell>{member.status}</TableCell>
481 </TableRow>
482 ))}
483 </tbody>
484 </Table>
485 </CardContent>
486 <Button ghost success block>Zarządzaj załogą</Button>
487 </Card>
488
489 {/* Statistics Card */}
490 <Card>
491 <CardTitle icon="📊">Statystyki</CardTitle>
492 <CardContent>
493 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
494 <Statistic>
495 <StatValue primary>26</StatValue>
496 <StatLabel>Dni w kosmosie</StatLabel>
497 </Statistic>
498
499 <Statistic>
500 <StatValue success>97%</StatValue>
501 <StatLabel>Wydajność</StatLabel>
502 </Statistic>
503
504 <Statistic>
505 <StatValue warning pulse={fuelLevel < 30}>{fuelLevel}%</StatValue>
506 <StatLabel>Poziom paliwa</StatLabel>
507 </Statistic>
508 </div>
509 </CardContent>
510 <Button ghost block>Zobacz więcej</Button>
511 </Card>
512
513 {/* Alerts Card */}
514 <Card warning={alerts.length > 0}>
515 <CardTitle icon="⚠️">Alerty</CardTitle>
516 <CardContent>
517 {alerts.length === 0 ? (
518 <div style={{ textAlign: 'center', padding: '20px 0', color: theme.colors.textMuted }}>
519 Brak aktywnych alertów
520 </div>
521 ) : (
522 <div>
523 {alerts.map(alert => (
524 <div key={alert.id} style={{
525 marginBottom: '8px',
526 padding: '8px',
527 borderRadius: '4px',
528 backgroundColor: 'rgba(0, 0, 0, 0.2)'
529 }}>
530 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
531 <div>
532 <StatusIndicator status={
533 alert.severity === 'critical' ? 'critical' :
534 alert.severity === 'warning' ? 'warning' :
535 'online'
536 } />
537 {alert.message}
538 </div>
539 <div style={{ fontSize: '12px', color: theme.colors.textMuted }}>
540 {alert.time}
541 </div>
542 </div>
543 </div>
544 ))}
545 </div>
546 )}
547 </CardContent>
548 <Button ghost warning block>Sprawdź wszystkie alerty</Button>
549 </Card>
550 </Grid>
551
552 <WideSection>
553 <Card>
554 <CardTitle>Panel Kontrolny Misji</CardTitle>
555 <CardContent>
556 <div style={{ display: 'flex', gap: '16px', marginBottom: '20px' }}>
557 <Button primary icon="🚀">Rozpocznij Operację</Button>
558 <Button icon="📊">Analizuj Dane</Button>
559 <Button warning icon="⚠️">System Diagnostyczny</Button>
560 <Button danger icon="🚫">Procedura Awaryjna</Button>
561 </div>
562
563 <div style={{
564 backgroundColor: 'rgba(0, 0, 0, 0.3)',
565 padding: '16px',
566 borderRadius: '8px',
567 fontFamily: theme.fonts.mono,
568 fontSize: '14px'
569 }}>
570 <div style={{ marginBottom: '8px' }}>
571 <span style={{ color: theme.colors.success }}>system$</span> Inicjalizacja systemów głównych...
572 </div>
573 <div style={{ marginBottom: '8px' }}>
574 <span style={{ color: theme.colors.success }}>system$</span> Sprawdzanie statusu komunikacji...
575 </div>
576 <div style={{ marginBottom: '8px' }}>
577 <span style={{ color: theme.colors.success }}>system$</span> Łączenie z satelitami...
578 </div>
579 <div>
580 <Spinner /> Oczekiwanie na dalsze instrukcje...
581 </div>
582 </div>
583 </CardContent>
584 </Card>
585 </WideSection>
586 </AppContainer>
587 </ThemeProvider>
588 );
589}
590
591export default SpaceDashboard;Jednym z najlepszych podejść do zarządzania stylami w większych projektach React jest stworzenie własnej biblioteki komponentów:
1// Plik: src/components/Button/Button.styles.js
2import styled, { css } from 'styled-components';
3
4// Reużywalne style
5const buttonSizes = {
6 small: css`
7 padding: 6px 12px;
8 font-size: 12px;
9 `,
10 medium: css`
11 padding: 8px 16px;
12 font-size: 14px;
13 `,
14 large: css`
15 padding: 12px 20px;
16 font-size: 16px;
17 `
18};
19
20const buttonVariants = {
21 primary: css`
22 background-color: ${props => props.theme.colors.primary};
23 color: white;
24
25 &:hover {
26 background-color: ${props => props.theme.colors.primaryDark};
27 }
28 `,
29 secondary: css`
30 background-color: ${props => props.theme.colors.secondary};
31 color: white;
32
33 &:hover {
34 background-color: ${props => props.theme.colors.secondaryDark};
35 }
36 `,
37 success: css`
38 background-color: ${props => props.theme.colors.success};
39 color: white;
40
41 &:hover {
42 background-color: ${props => props.theme.colors.successDark};
43 }
44 `,
45 danger: css`
46 background-color: ${props => props.theme.colors.danger};
47 color: white;
48
49 &:hover {
50 background-color: ${props => props.theme.colors.dangerDark};
51 }
52 `
53};
54
55// Główny komponent przycisk
56export const StyledButton = styled.button`
57 display: inline-flex;
58 align-items: center;
59 justify-content: center;
60 border: none;
61 border-radius: ${props => props.theme.borderRadius.sm};
62 font-family: ${props => props.theme.fonts.body};
63 font-weight: 600;
64 cursor: pointer;
65 transition: all 0.2s ease;
66
67 // Domyślny rozmiar
68 ${buttonSizes.medium}
69
70 // Zastosuj wybrany rozmiar
71 ${props => props.size && buttonSizes[props.size]}
72
73 // Domyślny wariant
74 background-color: #e0e0e0;
75 color: #333;
76
77 &:hover {
78 background-color: #d5d5d5;
79 }
80
81 // Zastosuj wybrany wariant
82 ${props => props.variant && buttonVariants[props.variant]}
83
84 // Ghost button style
85 ${props => props.ghost && css`
86 background-color: transparent;
87 border: 1px solid currentColor;
88
89 &:hover {
90 background-color: rgba(0, 0, 0, 0.05);
91 }
92 `}
93
94 // Disabled state
95 ${props => props.disabled && css`
96 opacity: 0.6;
97 cursor: not-allowed;
98 pointer-events: none;
99 `}
100
101 // Full width button
102 ${props => props.fullWidth && css`
103 width: 100%;
104 `}
105
106 // Icon support
107 ${props => props.iconPosition === 'left' && props.icon && css`
108 svg, img {
109 margin-right: 8px;
110 }
111 `}
112
113 ${props => props.iconPosition === 'right' && props.icon && css`
114 svg, img {
115 margin-left: 8px;
116 }
117 `}
118
119 // Loading state
120 ${props => props.loading && css`
121 position: relative;
122 color: transparent;
123
124 &::after {
125 content: "";
126 position: absolute;
127 top: 50%;
128 left: 50%;
129 width: 18px;
130 height: 18px;
131 margin: -9px 0 0 -9px;
132 border: 2px solid rgba(255, 255, 255, 0.3);
133 border-radius: 50%;
134 border-top-color: white;
135 animation: buttonLoader 0.8s linear infinite;
136 }
137
138 @keyframes buttonLoader {
139 to { transform: rotate(360deg); }
140 }
141 `}
142`;
143
144// Plik: src/components/Button/Button.jsx
145import React from 'react';
146import PropTypes from 'prop-types';
147import { StyledButton } from './Button.styles';
148
149const Button = ({
150 children,
151 variant,
152 size,
153 disabled,
154 fullWidth,
155 ghost,
156 loading,
157 icon,
158 iconPosition = 'left',
159 as,
160 ...props
161}) => {
162 return (
163 <StyledButton
164 variant={variant}
165 size={size}
166 disabled={disabled || loading}
167 fullWidth={fullWidth}
168 ghost={ghost}
169 loading={loading}
170 icon={icon}
171 iconPosition={iconPosition}
172 as={as}
173 {...props}
174 >
175 {iconPosition === 'left' && icon}
176 {children}
177 {iconPosition === 'right' && icon}
178 </StyledButton>
179 );
180};
181
182Button.propTypes = {
183 children: PropTypes.node.isRequired,
184 variant: PropTypes.oneOf(['primary', 'secondary', 'success', 'danger']),
185 size: PropTypes.oneOf(['small', 'medium', 'large']),
186 disabled: PropTypes.bool,
187 fullWidth: PropTypes.bool,
188 ghost: PropTypes.bool,
189 loading: PropTypes.bool,
190 icon: PropTypes.node,
191 iconPosition: PropTypes.oneOf(['left', 'right']),
192 as: PropTypes.elementType
193};
194
195export default Button;
196
197// Plik: src/components/index.js
198export { default as Button } from './Button/Button';
199export { default as Card } from './Card/Card';
200// Inne eksporty...
201
202// Użycie biblioteki komponentów
203import { Button, Card } from './components';
204
205function App() {
206 return (
207 <div>
208 <Card title="Panel sterowania">
209 <Button variant="primary" size="large">
210 Rozpocznij misję
211 </Button>
212 <Button variant="danger" ghost>
213 Anuluj
214 </Button>
215 </Card>
216 </div>
217 );
218}| Podejście | Zalety | Wady | |-----------|--------|------| | Styled Components | - Prawdziwy CSS w JS<br>- Izolacja stylów<br>- Dynamiczne style oparte na propsach<br>- Gotowe wsparcie dla tematyzacji | - Zwiększa rozmiar paczki JS<br>- Krzywa uczenia dla nowych deweloperów<br>- Style są renderowane w czasie rzeczywistym | | CSS Modules | - Lokalne zakresy CSS<br>- Brak runtime CSS-in-JS<br>- Znajoma składnia CSS | - Ograniczone dynamiczne style<br>- Trudniejsze warunkowe style<br>- Wymaga odpowiedniej konfiguracji narzędzi | | SCSS/SASS | - Potężne funkcje (zmienne, mixiny, itd.)<br>- Kompilowane podczas budowania<br>- Znajoma składnia | - Nie ma izolacji selektorów<br>- Trudniejsza integracja z propsami<br>- Wymaga dodatkowych narzędzi | | Tailwind CSS | - Szybki development<br>- Nie trzeba nazywać klas<br>- Spójny design system | - Rozdęte klasy HTML<br>- Utrudniona kustomizacja<br>- Stroma krzywa początkowej nauki |
Styled Components to potężne narzędzie, które zmienia podejście do stylowania w React, umożliwiając bardziej komponentowe, modułowe i reaktywne interfejsy użytkownika. Dzięki możliwości pisania prawdziwego CSS bezpośrednio w komponentach, dynamicznego reagowania na propsy oraz łatwej tematyzacji, można tworzyć elastyczne i łatwe w utrzymaniu systemy komponentów UI.
W świecie kosmicznej eksploracji Reacta, Styled Components jest jak zaawansowany skafander kosmiczny - łączy w sobie wszystkie niezbędne narzędzia do przetrwania, jednocześnie oferując swobodę ruchu i komfort. Uczy nas, że logika i wygląd nie muszą być oddzielnymi bytami, a mogą ewoluować razem, tworząc nową jakość interfejsów użytkownika.