I have a bunch of content-similar React apps to style, and I'm trying to shape a system, based on Styled-Components/Styled-Theming, to rule and modify their global styles just by changing the <ThemeProvider /> theme prop.
I've been trying different options, and this is what seems to work best :
an external themes.js file with all styles wrapped in a global object
import theme from "styled-theming";
const backG = theme.variants("observatory", "backG", {
positiveDark: {
_OBS1: 'hsl(73, 65%, 50%)',
_OBS2: 'hsl(210, 100%, 60%)',
},
negativeDark: {
_OBS1: 'hsl(8, 70%, 63%)',
_OBS2: 'hsl(0, 100%, 90%)',
},
actionDark: {
_OBS1: 'hsl(53, 82%, 62%)',
_OBS2: 'hsl(48, 100%, 50%)',
},
});
const cursor = theme.variants("observatory", "cursor", {
normal: {
_OBS1: 'initial',
_OBS2: 'initial',
},
pointer: {
_OBS1: 'pointer',
_OBS2: 'pointer',
},
});
const fontS = theme.variants("observatory", "fontS", {
h1: {
_OBS1: "2.4rem",
_OBS2: "2.25rem",
},
h2: {
_OBS1: "2.2rem",
_OBS2: "2rem",
},
h3: {
_OBS1: "2rem",
_OBS2: "1.75rem",
},
});
const flexL = theme.variants("observatory", "flexL", {
layout1: {
_OBS1: "; display: flex; justify-content: center;",
_OBS2: "; display: flex; justify-content: center;",
},
layout2: {
_OBS1: "; display: flex; justify-content: space-around; align-items: flex-start;",
_OBS2: "; display: flex; flex-direction: column; align-items: flex-start;",
},
layout3: {
_OBS1: "; display: flex; flex-direction: column; align-items: flex-start;",
_OBS2: "; display: flex; justify-content: space-around; align-items: flex-start;",
}
});
const themes = {
backG: backG,
cursor: cursor,
flexL: flexL,
fontS: fontS,
}
export {themes};
a CRA representing my app, with
an index.js file with 2 <ThemeProvider />, the first one referencing the external file, the second one dealing with themes inside of that external file
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// styles
import {themes} from "./themes";
import {ThemeProvider} from "styled-components";
ReactDOM.render(
<ThemeProvider theme={themes}>
<ThemeProvider theme={{ observatory: "_OBS2" }}>
<App />
</ThemeProvider>
</ThemeProvider>,
document.getElementById('root')
);
an App.js file and other children components
import React from 'react';
// styles
import styled, { injectGlobal } from "styled-components";
// components
import ThreeButtons from "./ThreeButtons";
injectGlobal`
#import url('https://fonts.googleapis.com/css?family=Quicksand:400,500');
html {
font: 400 10px Roboto, sans-serif;
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
font: 500 1.8rem Quicksand;
}
`
const Wrapper1 = styled.div`
display: ${props => props.theme.flexL};
align-items: ${props => props.theme.flexL};
`
const App = () => (
<Wrapper1 flexL="layout1">
<ThreeButtons alignSelf1="center" flexL="layout2" />
<ThreeButtons alignSelf2="flex-end" flexL="layout3" />
</Wrapper1>
);
export default App;
import React from "react";
// styles
import styled from "styled-components";
// components
import Button from "./Button";
const Wrapper1 = styled.div`
flex: 1;
display: ${props => props.theme.flexL};
flex-direction: ${props => props.theme.flexL};
align-items: ${props => props.theme.flexL};
`
const ThreeButtons = (props) => (
<React.Fragment>
<Wrapper1
flexL={props.flexL}
>
<Button
backG="positiveDark"
cursor="pointer"
fontS="h1"
label="lg-pos-button-pointer"
/>
<Button
alignSelf={props.alignSelf1}
backG="negativeDark"
cursor="pointer"
fontS="h2"
label="md-neg-button-pointer"
/>
<Button
alignSelf={props.alignSelf2}
backG="actionDark"
cursor="normal"
fontS="h3"
label="sm-act-button-nopointer"
/>
</Wrapper1>
</React.Fragment>
)
export default ThreeButtons;
import React from "react";
// styles
import styled from "styled-components";
const StyledButton = styled.button`
align-self: ${props => props.alignSelf};
background: ${props => props.theme.backG};
font-size: ${props => props.theme.fontS};
cursor: ${props => props.theme.cursor};
padding: 1.2rem 1.8rem;
border: none;
border-radius: .3rem;
margin: 1rem;
filter: saturate(50%);
transition: filter ease-in-out .15s;
&:hover {
filter: saturate(100%);
}
`
const Button = (props) => (
<StyledButton
backG={props.backG}
cursor={props.cursor}
alignSelf={props.alignSelf}
fontS={props.fontS}
>
{props.label}
</StyledButton>
)
export default Button;
Like I said before, it looks like it works, but still there're 2 things I'm not really comfortable with because I don't get it :
in the themes.js external file, I've been trying to create some kind of Flexbox mixins : to make it work, I must add an initial semicolon in the string value, or the first property is not considered and its value is not applied. Can someone explain me why ?
in the index.js file, I must nest a <ThemeProvider /> inside another one to make it work correctly : again, can someone explain me how Styled-Components and Styled-Theming are working together ?
generally speaking, have you got any tips or advice to make my solution a better solution or to solve my initial problem, which was to globally style a bunch of content-similar React apps ?
If you need, you can find the entire code on GitHub : https://github.com/paillenapple/styles_sandbox
Thanks for your help.
Silvère
Related
I'm creating a light/dark mode switch in React with this tutorial - Tutorial Link
After adding GlobalStyles from styled-components even that I was doing all according to tutorial I receive this error:
./src/Components/Global.js
Line 13:40: Parsing error: Unterminated template
11 | align-items: center;
12 | background: ${({ theme }) => theme.body};
> 13 | color: ${({ theme }) => theme.text};
| ^
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
Here is code from Global.js
import { createGlobalStyle } from 'styled-components';
export const GlobalStyles = createGlobalStyle`
*,
*::after,
*::before {
box-sizing: border-box;
}
body {
align-items: center;
background: ${({ theme }) => theme.body};
color: ${({ theme }) => theme.text};
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
margin: 0;
padding: 0;
font-family: BlinkMacSystemFont, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
transition: all 0.25s linear;
}
And here is my App.js
import React from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import { GlobalStyles } from './Components/Global';
import { ThemeProvider} from 'styled-components';
import { lightTheme, darkTheme} from './Components/Theme'
function App() {
return (
<ThemeProvider theme={lightTheme}>
<>
<GlobalStyles />
<button>Toggle theme</button>
<h1>It's a light theme!</h1>
<footer>
</footer>
</>
</ThemeProvider>
);
}
export default App;
Question: How I can fix that error and did I miss something in syntax which leads to this error?
I cannot extend my imported component. I was looking into styled components docs and find that in v4+ should works prop "as", but it doesnt.
COMPONENT:
type Props = {
padding: string,
justify: string
}
const FlexContainer = styled.div<Props>`
padding: ${props => props.padding};
display: flex;
flex-wrap: wrap;
justify-content: ${props => props.justify};
`
export const Flexcontainer: React.FC<Props> = props =>{
return (
<FlexContainer padding={props.padding} justify={props.justify}>
{props.children}
</FlexContainer>
)
}
EXTENDED STYLE:
import { Flexcontainer } from '../../reusable/FlexContainer';
const FlexContainerExtended = styled.div`
color: red;
`
USE:
<FlexContainerExtended
padding={null}
justify={"flex-start"}
as={Flexcontainer}>
You just have to add a prop className to your Flexcontainer component like this:
export const Flexcontainer: React.FC<Props> = props =>{
return (
<FlexContainer className={props.className} padding={props.padding} justify={props.justify} >
{props.children}
</FlexContainer>
)}
To override styles, styled-components passes a className as props to the overrided component, it's why
You can just pass the base component to the styled function to override it.
type Props = {
padding: string,
justify: string
}
const FlexContainer = styled.div<Props>`
padding: ${props => props.padding};
display: flex;
flex-wrap: wrap;
justify-content: ${props => props.justify};
`
const FlexContainerExtended = styled(FlexContainer)`
color: red;
`
export const Flexcontainer: React.FC<Props> = props =>{
return (
<FlexContainer padding={props.padding} justify={props.justify}>
{props.children}
</FlexContainer>
)
}
// And use it like this
<FlexContainerExtended
padding={null}
justify={"flex-start"}/>
I know this question was asked a long time ago, but I leave here the solution I found for future visitors.
Base component definition
import React from 'react'
import styled from 'styled-components'
const ContainerWrapper = styled.div`
width: 100%;
max-width: 1200px;
padding-left: 5%;
padding-right: 5%;
margin-left: auto;
margin-right: auto;
`
export default function Container({ children, ...props }) {
return (
<ContainerWrapper {...props}>
{children}
</ContainerWrapper>
)
}
Extended component definition
Obs.: note that the extended component is an article, while the base component is a div, and so far they have no relationship.
Also note that the base component (Container) has been imported but not yet used.
import React from 'react'
import styled from 'styled-components'
import Container from '../../common/Container'
const HeroWrapper = styled.article`
height: 100vh;
padding-top: 74px;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
background-attachment: fixed;
`
Invoking componentes
Now, in the same file where I declared the extended component, I just call the base component, informing the name of the extended component in the as attribute.
export default function PostsWrapper() {
return (
<Container as={HeroWrapper}>
{/* ... */}
</Container>
)
}
I have the following React Header component in which I'm trying to get a theme value from the ThemeContext.Consumer and pass it into my NavListItem. It's currently working (sort of) but I'd like to only pass it once and have it applied globally. Instead of having to write theme={theme.typography.navigation} each time I use the styled component?
import React from "react"
import { Link } from "gatsby"
import PropTypes from "prop-types"
import styled from "styled-components"
import ThemeContext from "../context/ThemeContext"
const Main = styled.header`
height: 70px;
background-color: white;
position: fixed;
width: 100%;
left: 0;
`
const NavWrapper = styled.div`
position: relative;
`
const NavListLeft = styled.ul`
color: black;
position: absolute;
top: 25px;
left: 40px;
list-style: none;
margin: 0;
padding: 0;
`
const NavListItem = styled.li`
display: inline-block;
margin: 0 10px;
font-family: ${props => props.theme};
font-size: 16px;
`
const Header = ({ siteTitle }) => (
<ThemeContext.Consumer>
{theme => (
<Main>
<NavWrapper>
<NavListLeft>
<NavListItem theme={theme.typography.navigation}>
<Link style={{color: 'black'}}>Women</Link>
</NavListItem>
<NavListItem theme={theme.typography.navigation}>Men</NavListItem>
<NavListItem theme={theme.typography.navigation}>Designers</NavListItem>
<NavListItem theme={theme.typography.navigation}>Collection</NavListItem>
<NavListItem theme={theme.typography.navigation}>Sale</NavListItem>
</NavListLeft>
</NavWrapper>
</Main>
)}
</ThemeContext.Consumer>
)
Header.propTypes = {
siteTitle: PropTypes.string,
}
Header.defaultProps = {
siteTitle: ``,
}
export default Header
You could create a NavListItem that consumes your theme.
const ThemedNavListItem = styled.li`
display: inline-block;
margin: 0 10px;
font-family: ${props => props.theme.typography.navigation};
font-size: 16px;
`
const NavListItem = props => (
<ThemeContext.Consumer>
{theme => <ThemedNavListItem {...props} theme={theme} />}
</ThemeContext.Consumer>
);
const Header = ({ siteTitle }) => (
<Main>
<NavWrapper>
<NavListLeft>
<NavListItem>
<Link style={{color: 'black'}}>Women</Link>
</NavListItem>
<NavListItem>Men</NavListItem>
<NavListItem>Designers</NavListItem>
<NavListItem>Collection</NavListItem>
<NavListItem>Sale</NavListItem>
</NavListLeft>
</NavWrapper>
</Main>
);
A simpler solution using styled-components ThemeProvider.
A helper component for theming. Injects the theme into all styled
components anywhere beneath it in the component tree, via the context
API.
const NavListItem = styled.li`
display: inline-block;
margin: 0 10px;
font-family: ${props => props.theme.typography.navigation};
font-size: 16px;
`
const Header = ({ siteTitle }) => (
<ThemeProvider theme={theme}>
<Main>
<NavWrapper>
<NavListLeft>
<NavListItem>
<Link style={{color: 'black'}}>Women</Link>
</NavListItem>
<NavListItem>Men</NavListItem>
<NavListItem>Designers</NavListItem>
<NavListItem>Collection</NavListItem>
<NavListItem>Sale</NavListItem>
</NavListLeft>
</NavWrapper>
</Main>
</ThemeProvider>
);
I'm trying to customize a MUI TextField Select component with styled components.
The ideia is styled-compoents provide diferent classes to Select field and Menu, so i can have their styled separated.
const StyledSelect = styled(({ className, ...props }) => {
return (
<TextField {...props}
classes={{ root: className }}
SelectProps={{
MenuProps: {
classes: { paper: className, list: className },
anchorOrigin: {
vertical: "bottom",
horizontal: "left"
},
transformOrigin: {
vertical: "top",
horizontal: "left"
},
getContentAnchorEl: null
},
}}
/>
)
})`
& {
background-color: #454D5D;
border-radius: 10px;
margin-top: 5px;
}
& li {
color: #FFF;
}
&.MuiFormControl-root {
background-color: transparent;
}
& .MuiListItem-root {
font-size: 18px;
}
& .MuiListItem-root.Mui-selected {
background-color: #1A2233;
}
& .MuiFormLabel-root {
font-family: 'Roboto';
font-weight: 300;
}
& .MuiInputLabel-shrink {
color: ${props => props.color};
font-weight: normal;
}
& .MuiInput-underline:after {
border-bottom: 2px solid ${props => props.errors[props.field.name] && props.touched[props.field.name]
? CASABLANCA : props.color};
transition: none;
transform: none;
}
& .MuiInput-underline:before {
border-bottom: 1px solid ${props => props.color}
}
& .MuiSelect-roo {
color: black;
font-family: 'Roboto';
font-weight: 300;
}
& .MuiSelect-select:focus {
background: transparent;
}
`;
I wish my TextField class would be different from MenuProps class
One way to solve this is to have one wrapper component per class name you need to generate. In my example below, StyledTextField takes care of the root class name for TextField (the className property is equivalent to classes.root) and then MenuPaperClass provides an additional class name.
import React from "react";
import ReactDOM from "react-dom";
import TextField from "#material-ui/core/TextField";
import MenuItem from "#material-ui/core/MenuItem";
import styled from "styled-components";
const StyledTextField = styled(TextField)`
/* && to add specificity */
&& {
border: 1px solid green;
}
`;
const MenuPaperClass = styled(({ className, ...props }) => {
return (
<StyledTextField
SelectProps={{ MenuProps: { classes: { paper: className } } }}
value="1"
select
{...props}
>
<MenuItem value="1">One</MenuItem>
<MenuItem value="2">Two</MenuItem>
<MenuItem value="3">Three</MenuItem>
</StyledTextField>
);
})`
&& {
background-color: lightblue;
}
`;
function App() {
return (
<div className="App">
<MenuPaperClass />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
This isn't a particularly elegant solution and gets pretty tedious if you have 3 or more separate classes you want to use, so I may come back to this later to consider alternative approaches/syntax, but this does work.
I'm trying to pass props to my styled component. I am using the styled-component npm dependency.
import React, { Component } from 'react';
import styled from 'styled-components';
class WidgetContainer extends Component {
render() {
return (
<Wrapper name='widget-container'>
</Wrapper>
);
}
}
const mapStateToProps = state => {
return {
user: state.user,
bannerStatus: true
};
};
export default withRouter(connect(mapStateToProps)(WidgetContainer));
const Wrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
min-height: 100vh;
margin: auto;
align-items: center;
justify-content: flex-start;
background: -webkit-linear-gradient(top, rgba(162, 209, 234, 1) 0%, rgba(56, 83, 132, 1) 100%);
padding-top: ${(props) => {
console.log(props.bannerStatus)
console.log('DATA')
return props.user ? '160px' : '126px'}};
overflow-y: auto;
padding-bottom: 40px;
#media(max-width: 768px) {
padding-top: 126px;
padding-bottom: 5px;
}
`;
The console.log you can see returns undefined. Even though the value is hard-coded. The ternary operator doesn't work as a result.
Here are the docs: https://www.styled-components.com/docs/basics#adapting-based-on-props.
You are mapping state to props in only the WidgetContainer. Your Wrapper component is not being passed any props.
Try
<Wrapper bannerStatus={props.bannerStatus} name='widget-container' />
OR
<Wrapper user={props.user} bannerStatus={props.bannerStatus} name='widget-container' />