I'm using styled-components in my React app and wanting to use a dynamic theme. Some areas it will use my dark theme, some will use the light. Because the styled components have to be declared outside of the component they are used in, how do we pass through a theme dynamically?
3 Answers
That's exactly what the ThemeProvider
component is for!
Your styled components have access to a special theme
prop when they interpolate a function:
const Button = styled.button`
background: ${props => props.theme.primary};
`
This <Button />
component will now respond dynamically to a theme defined by a ThemeProvider
. How do you define a theme? Pass any object to the theme
prop of the ThemeProvider
:
const theme = {
primary: 'palevioletred',
};
<ThemeProvider theme={theme}>
<Button>I'm now palevioletred!</Button>
</ThemeProvider>
We provide the theme to your styled components via context
, meaning no matter how many components or DOM nodes are in between the component and the ThemeProvider it'll still work exactly the same:
const theme = {
primary: 'palevioletred',
};
<ThemeProvider theme={theme}>
<div>
<SidebarContainer>
<Sidebar>
<Button>I'm still palevioletred!</Button>
</Sidebar>
</SidebarContainer>
</div>
</ThemeProvider>
This means you can wrap your entire app in a single ThemeProvider
, and all of your styled components will get that theme. You can swap that one property out dynamically to change between a light and a dark theme!
You can have as few or as many ThemeProvider
s in your app as you want. Most apps will only need one to wrap the entire app, but to have a part of your app be light themed and some other part dark themed you would just wrap them in two ThemeProvider
s that have different themes:
const darkTheme = {
primary: 'black',
};
const lightTheme = {
primary: 'white',
};
<div>
<ThemeProvider theme={lightTheme}>
<Main />
</ThemeProvider>
<ThemeProvider theme={darkTheme}>
<Sidebar />
</ThemeProvider>
</div>
Any styled component anywhere inside Main
will now be light themed, and any styled component anywhere inside Sidebar
will be dark themed. They adapt depending on which area of the application they are rendered in, and you don't have to do anything to make it happen! 🎉
I encourage you to check out our docs about theming, as styled-components was very much built with that in mind.
One of the big pain points of styles in JS before styled-components existed was that the previous libraries did encapsulation and colocation of styles very well, but none of them had proper theming support. If you want to learn more about other pain points we had with existing libraries I'd encourage you to watch my talk at ReactNL where I released styled-components. (note: styled-components' first appearance is at ~25 minutes in, don't be surprised!)
While this question was originally for having multiple themes running at the same time, I personally wanted to dynamically switch in runtime one single theme for the whole app.
Here's how I achieved it: (I'll be using TypeScript and hooks in here. For plain JavaScript just remove the type
s, as
, and interface
):
I have also included all the imports at the top of each block code just in case.
We define our theme.ts
file
//theme.ts
import baseStyled, { ThemedStyledInterface } from 'styled-components';
export const lightTheme = {
all: {
borderRadius: '0.5rem',
},
main: {
color: '#FAFAFA',
textColor: '#212121',
bodyColor: '#FFF',
},
secondary: {
color: '#757575',
},
};
// Force both themes to be consistent!
export const darkTheme: Theme = {
// Make properties the same on both!
all: { ...lightTheme.all },
main: {
color: '#212121',
textColor: '#FAFAFA',
bodyColor: '#424242',
},
secondary: {
color: '#616161',
},
};
export type Theme = typeof lightTheme;
export const styled = baseStyled as ThemedStyledInterface<Theme>;
Then in our main entry, in this case App.tsx
we define the <ThemeProvider>
before every component that's going to use the theme.
// app.tsx
import React, { memo, Suspense, lazy, useState } from 'react';
import { Router } from '@reach/router';
// The header component that switches the styles.
import Header from './components/header';
// Personal component
import { Loading } from './components';
import { ThemeProvider } from 'styled-components';
// Bring either the lightTheme, or darkTheme, whichever you want to make the default
import { lightTheme } from './components/styles/theme';
// Own code.
const Home = lazy(() => import('./views/home'));
const BestSeller = lazy(() => import('./views/best-seller'));
/**
* Where the React APP main layout resides:
*/
function App() {
// Here we set the default theme of the app. In this case,
// we are setting the lightTheme. If you want the dark, import the `darkTheme` object.
const [theme, setTheme] = useState(lightTheme);
return (
<Suspense fallback={<Loading />}>
<ThemeProvider theme={theme}>
<React.Fragment>
{/* We pass the setTheme function (lift state up) to the Header */}
<Header setTheme={setTheme} />
<Router>
<Home path="/" />
<BestSeller path="/:listNameEncoded" />
</Router>
</React.Fragment>
</ThemeProvider>
</Suspense>
);
}
export default memo(App);
And in header.tsx we pass the setTheme to the component (Lifting the state up):
// header.tsx
import React, { memo, useState } from 'react';
import styled, { ThemedStyledInterface } from 'styled-components';
import { Theme, lightTheme, darkTheme } from '../styles/theme';
// We have nice autocomplete functionality
const Nav = styled.nav`
background-color: ${props => props.theme.colors.primary};
`;
// We define the props that will receive the setTheme
type HeaderProps = {
setTheme: React.Dispatch<React.SetStateAction<Theme>>;
};
function Header(props:
function setLightTheme() {
props.setTheme(lightTheme);
}
function setDarkTheme() {
props.setTheme(darkTheme);
}
// We then set the light or dark theme according to what we want.
return (
<Nav>
<h1>Book App</h1>
<button onClick={setLightTheme}>Light </button>
<button onClick={setDarkTheme}> Dark </button>
</Nav>
);
}
export default memo(Header);
Here's something that did the job for me:
import * as React from 'react';
import { connect } from 'react-redux';
import { getStateField } from 'app/redux/reducers/recordings';
import { lightTheme, darkTheme, ThemeProvider as SCThemeProvider } from 'app/utils/theme';
import { GlobalStyle } from 'app/utils/globalStyles';
interface ThemeProviderProps {
children: JSX.Element;
isLightMode?: boolean;
}
const ThemeProvider = ({ children, isLightMode }: ThemeProviderProps) => {
return (
<SCThemeProvider theme={isLightMode ? lightTheme : darkTheme}>
<React.Fragment>
{children}
<GlobalStyle />
</React.Fragment>
</SCThemeProvider>
);
};
export const ConnectedThemeProvider = connect((state) => ({
isLightMode: getStateField('isLightMode', state)
}))(ThemeProvider);