JavaScript and React course Β· Module 14: Advanced Styling

Theming in Styled-components - Galactic Design System

6 min read
In this lesson7

Imagine a fleet in which every ship has a different shade of purple on its alarm buttons, because every engineer typed the color from memory. When command decides to change the fleet colors, someone will have to search hundreds of files and fix every #7c4dff value by hand. That's what a project without a theme looks like. ThemeProvider in styled-components lets you create a consistent system of colors, sizes, and typography - like a unified interface for an entire space fleet.

Before we start, one honest note. In March 2025 the library's maintainer announced that styled-components is entering maintenance mode: it gets bug fixes but no new features, and the author himself does not recommend it for new projects. The library (currently version 6) still runs in a huge number of existing applications, so it's worth knowing, and the idea of a theme you'll learn here carries over one to one to CSS Variables and other tools.

ThemeProvider

ThemeProvider is a component that provides a theme to all components inside it. Under the hood it uses React context, so the theme reaches any depth without passing props. The theme itself is a plain JavaScript object, in which we group values by category: colors, spacing, border radii and font sizes:

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 renders nothing in the DOM and doesn't change how Dashboard looks on its own. It only makes the object available, and components have to use it. You invent key names such as colors or spacing yourself, the library doesn't impose any structure.

Using the Theme in Components

Every styled-component automatically has access to the theme via props.theme. Inside the style template you insert a function in ${...}, and the library calls it with the component's props:

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`;

Note the destructuring - ({ theme }) is shorthand for (props) => props.theme. Both forms work identically, but I recommend the second one, because with many values it's shorter and more readable. Neither of these components contains a single hardcoded color any more.

Switching Themes (Dark/Light)

One of the most popular uses of ThemeProvider is switching between dark and light themes. We define two objects with the same keys, and the isDark state picks which one goes to the provider:

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}

After the click, only the object passed to theme changes, and all styled-components recompute their styles. The component code stays untouched. This only works when both themes have identical keys, because a missing key means undefined in the CSS.

Global Styles

createGlobalStyle lets you define global CSS styles that also have access to the theme. It returns a component that you render once, inside 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 must sit inside the provider, otherwise theme would be empty. Thanks to that, the background of the whole page changes together with the theme, not just the background of individual panels.

The useTheme Hook

If you need access to the theme in component logic (not just in styles), use the useTheme hook. It returns the same object you passed to the provider:

1import { useTheme } from 'styled-components';
2
3function StatusChart({ data }) {
4  const theme = useTheme();
5
6  // Use theme colors in logic, e.g. for Canvas, SVG, or conditions
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 is useful when you need theme values outside of styled-components - for example, for drawing on Canvas, configuring chart libraries, or conditional logic based on theme colors.

Nested Themes

ThemeProvider can be nested so that different sections of the application have different themes. The nearest provider wins:

1function App() {
2  return (
3    <ThemeProvider theme={darkTheme}>
4      <Header />
5      <ThemeProvider theme={lightTheme}>
6        <ContentArea />  {/* lightTheme applies here */}
7      </ThemeProvider>
8      <Footer />  {/* darkTheme applies again here */}
9    </ThemeProvider>
10  );
11}

It's like different sections of a spaceship with different lighting modes - the command bridge can be dark while the laboratory is bright. The inner provider can also receive a function theme={outer => ({ ...outer, ... })}, which extends the outer theme instead of replacing it.

Theming Best Practices

  1. Centralize colors and sizes - never hardcode values in components, always reference the theme
  2. Use TypeScript - define a type for your theme to get autocompletion (in styled-components you extend the DefaultTheme interface)
  3. Maintain consistency - all themes should have the same keys (colors, spacing, fontSize)
  4. Test both themes - make sure every component looks good in both dark and light modes

In the next lesson you'll see responsive patterns, and in the main project you'll combine this theme with CSS Modules and Tailwind in a single mission dashboard.

Remember: a theme is the fleet's shared palette, you change it in one place and every ship immediately shines in the new colors.

Code for this lesson: App.jsx
1import React, { useState } from 'react';
2import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
3
4// Two themes -- dark (cosmic) and light
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>Ion engines</CardTitle>
127          <p>Status: <Badge variant="success">Active</Badge></p>
128          <p style={{ marginTop: '8px', fontSize: '14px' }}>
129            Full power -- ready for the hyperspace jump.
130          </p>
131        </Card>
132
133        <Card>
134          <CardTitle>Energy shields</CardTitle>
135          <p>Status: <Badge variant="warning">Warning</Badge></p>
136          <p style={{ marginTop: '8px', fontSize: '14px' }}>
137            Energy level: 42%. Recharge required.
138          </p>
139        </Card>
140
141        <Card>
142          <CardTitle>Life support system</CardTitle>
143          <p>Status: <Badge variant="danger">Critical</Badge></p>
144          <p style={{ marginTop: '8px', fontSize: '14px' }}>
145            Oxygen reserves for 72h. Immediate intervention required!
146          </p>
147        </Card>
148      </Container>
149    </ThemeProvider>
150  );
151}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the role of the ThemeProvider component in styled-components?

  2. 2. What is createGlobalStyle used for in styled-components?

Hands-on tasks in the game

  • Vertical ordering

    Arrange the elements in the correct ThemeProvider hierarchy:

  • Code editor

    Implement theme switching with ThemeProvider

  • Click in order

    Arrange the syntax for accessing the primary color from the theme in a styled-component:

  • Code editor

    Implement navigation with dynamic active link styling

Useful articles