Reusable parametric css with styled components - javascript

I'm using styled-components library in my react app and I've come across an interesting problem I wasn't able to find an elegant solution to online. What I want to achieve is to have a reusable piece of code, maybe similar to sass mixins, that would allow me to extend all my buttons with code that animates background darken on hover.
const DarkenHover = css<{ active?: boolean; normalColor: string; activeColor: string }>`
background-color: ${p => (p.active ? p.normalColor : p.activeColor)};
&:hover {
background-color: ${p => darken(0.1)(p.active ? p.normalColor : p.activeColor)};
}
transition: background-color .1s ease-in;
`;
const FooButton = styled.div<{ active?: boolean }>`
${p => DarkenHover({
active: p.active,
normalColor: "red",
activeColor: "blue",
})}
`;
const FooButton = styled.div<{ active?: boolean }>`
${p => DarkenHover({
active: p.active,
normalColor: "white",
activeColor: "green",
})}
`;
This obviously is not valid syntax but it demonstrates my use case. How can I use this DarkenHover css object with attributes?

You can save the styles in a var and reuse later.
const animation = css`
background-color: ${p => p.active ? ThemeColors.backgroundDark : "white"};
&:hover {
background-color: ${p => darken(0.1)(p.active ? p.normalColor : p.activeColor)};
}
transition: background-color .1s ease-in;
}
`;
The when you use it in another component, it should be able to access its props:
const FooButton = styled.div`
${animation};
`
Also to be able to have separate props per each styled components, those can be passed via attrs method:
const FooButton = styled.div.attrs({normalColor: '#000000' })`
${animation}
`;

Related

React Styled Component doesn't show correct output

I've changed my style in StyledButton tag but it doesn't reflect on webpage. Can you help what is wrong in this code
import React, { Component } from 'react';
import './App.css';
//import Radium, {StyleRoot} from 'radium';
import Person from './Person/Person';
import { render } from 'react-dom';
import styled from 'styled-components'
const StyledButton = styled.button `
background-color: ${props => props.alt ? 'red' : 'green'}; //Here I have define these property which is not reflecting in output
color: white;
font: inherit;
border: 1px solid blue;
padding: 8px;
cursor:pointer;
&:hover:{
background-color:${props => props.alt ? 'salmon' : 'green'}; //Hover Function is also not working
color:black;
}`;
class App extends Component {
state ={
persons : [
{id:'12', name: 'Max', age: 28},
{id:'21', name: 'Manu', age:29},
{id:'33', name: 'Nikhil', age:23}
]};
nameChangeHandler = (event, id) => {
const personIndex = this.state.persons.findIndex(p=>{
return p.id===id;
});
const person = {
... this.state.persons[personIndex]
};
person.name = event.target.value;
const persons =[...this.state.persons];
persons[personIndex]=person;
this.setState({ persons: persons
});
}
deletePersonHandler = (personIndex) =>{
//const persons = this.state.persons.slice();
//const persons = this.state.persons
const persons = [... this.state.persons];
persons.splice(personIndex,1);
this.setState({persons: persons})
}
togglePersonsHandler = ()=> {
const doesShow = this.state.showPersons;
this.setState({showPersons: !doesShow});
}
render()
{
let persons = null;
if(this.state.showPersons)
{
persons = (
<div>
{ this.state.persons.map((person, index) =>{
return <Person
click = { () => this.deletePersonHandler(index) }
name={person.name}
age={person.age}
key= {person.id}
changed={(event)=> this.nameChangeHandler(event, person.id)}/>
})}
</div>
);
//StyledButton.backgroundColor= 'red';
}
let classes = []
if(this.state.persons.length<=2)
{
classes.push('red');
}
if(this.state.persons.length<=1)
{
classes.push('bold');
}
return (
<div className = "App">
<h1>Hi there this is my first react application</h1>
<p className= {classes.join(' ')}>Hi this is really working</p>
<StyledButton
alt ={ this.state.showPersons }
onClick = {this.togglePersonsHandler}>Toggle Persons</StyledButton>
{ persons }
</div>
);
}
}
export default App;
Code just toggle the names and ages when user click on button and delete the names when click on the paragraph and adding certain classes these are all works fine.
I'm using styled component package on toggle button and it is not working properly, I don't why Please let me know if you understand
You've defined alt to be a transient prop, i.e. $alt, but you don't pass that prop to the StyledButton.
You've also a typo in your hover selector, there's a trailing colon (:): &:hover: should be :hover (the leading parent node selector & is also unnecessary).
const StyledButton = styled.button `
background-color: ${props => props.$alt ? 'red' : 'green'};
color: white;
font: inherit;
border: 1px solid blue;
padding: 8px;
cursor: pointer;
:hover {
background-color:${props => props.$alt ? 'salmon' : 'green'};
color:black;
}
`;
...
<StyledButton
$alt={this.state.showPersons} // <-- pass transient prop
onClick={this.togglePersonsHandler}
>
Toggle Persons
</StyledButton>
This was introduced in v5.1. If you didn't intend to declare a transient prop or you aren't on v5.1 or newer, then simply remove the $ and use the alt prop.
const StyledButton = styled.button `
background-color: ${props => props.alt ? 'red' : 'green'};
color: white;
font: inherit;
border: 1px solid blue;
padding: 8px;
cursor: pointer;
:hover {
background-color:${props => props.alt ? 'salmon' : 'green'};
color:black;
}
}`;
...
<StyledButton
alt={this.state.showPersons} // <-- use alt prop
onClick={this.togglePersonsHandler}
>
Toggle Persons
</StyledButton>
Demo

Is it possible to switch the value of a css property using an if statement in styled-components?

I'm trying to display a label that says "required" or "optional" at the end of a tag using the CSS after selector. My idea is to look like this.
I'm using React.js and the styled-components library, so I take props.required as an argument, "required" when {props.required == true}, "required" when {props.required == false} I tried to switch the display, such as "optional".
// form.js
<FormTitle required={true}>Name</FormTitle>
<FormTitle required={false}>Phone Number</FormTitle>
// styles.js
import styled from 'styled-components'
const FormTitle = styled.div`
...
&:after {
content: ${props => props.required ? "required": "optional"};
background-color: ${props => props.required ? "#FF0101": "#6FC173"};
...
}
`
But when I do this, the label does not appear.
Apparently, switching the content property of CSS with the if the statement of javascript causes a problem. Because if you don't use an if statement for the content property, everything else works.
...
&:after {
content: "required";
background-color: ${props => props.required ? "#FF0101": "#6FC173"};
...
}
`
Is there a way to solve this?
The content property expects the value to be wrapped by quotes. Wrap the expression with standard quotes:
const FormTitle = styled.div`
::after {
content: '${props => props.required ? "required": "optional"}';
background-color: ${props => props.required ? "#FF0101": "#6FC173"};
}
`
Another option is to use the styled component .attrs() to set a data attribute on the element, and then you use the attribute's value as the content of the pseudo element (::after):
const FormTitle = styled.div.attrs(props => ({
'data-required': props.required ? 'required': 'optional',
}))`
&::after {
content: attr(data-required);
background-color: ${props => props.required ? "#FF0101": "#6FC173"};
}
`
Examples - Sandbox

Creating Responsive Props for Styled Components

I am trying to create responsive props for styled components as follows. To start with, we have a component (let's say a button):
<Button primary large>Click Me</Button>
This button will get a background-color of primary and a large size (as determined by a theme file).
I now want to create a responsive version of this button. This is how I would like that to work:
<Button
primary
large
mobile={{size: 'small', style: 'secondary'}}
tablet={size: 'small'}}
widescreen={{style: 'accent'}}
>
Click Me
</Button>
I now have my same button, but with the styles and sizes varied for different screen sizes.
Now, I have gotten this to work -- but it involves a lot of duplicate code. This is an example of what it looks like:
const Button = styled('button')(
({
mobile,
tablet,
tabletOnly,
desktop,
widescreen
}) => css`
${mobile &&
css`
#media screen and (max-width: ${theme.breakpoints.mobile.max}) {
background-color: ${colors[mobile.style] || mobile.style};
border: ${colors[mobile.style] || mobile.style};
border-radius: ${radii[mobile.radius] || mobile.radius};
color: ${mobile.style && rc(colors[mobile.style] || mobile.style)};
}
`}
${tablet &&
css`
#media screen and (min-width: ${theme.breakpoints.tablet.min}), print {
background-color: ${colors[tablet.style] || tablet.style};
border: ${colors[tablet.style] || tablet.style};
border-radius: ${radii[tablet.radius] || tablet.radius};
color: ${tablet.style && rc(colors[tablet.style] || tablet.style)};
}
`}
${tabletOnly &&
css`
#media screen and (min-width: ${theme.breakpoints.mobile.min}) and (max-width: ${theme.breakpoints.tablet.max}) {
background-color: ${colors[tabletOnly.style] || tabletOnly.style};
border: ${colors[tabletOnly.style] || tabletOnly.style};
border-radius: ${radii[tabletOnly.radius] || tabletOnly.radius};
color: ${tabletOnly.style &&
rc(colors[tabletOnly.style] || tabletOnly.style)};
}
`}
`
What I am looking for is a way to simplify this code. Basically, I want to only write the CSS styles ONCE and then generate the different props and media queries based off of a query object that something like this:
const mediaQueries = {
mobile: {
min: '0px',
max: '768px'
},
tablet: {
print: true,
min: '769px',
max: '1023px'
},
desktop: {
min: '1024px',
max: '1215px'
},
widescreen: {
min: '1216px',
max: '1407px'
},
fullhd: {
min: '1408px',
max: null
}
}
I imagine I should be able to create a function that loops through through the mediaQueries object and inserts the appropriate css for each iteration. However, I can't seem to figure out how to do this.
Any ideas on how to do this?
Also, thanks in advance for any help you can offer.
Maybe something like this is what you are looking for:
import { css } from "styled-components";
//mobile first approach min-width
const screenSizes = {
  fullhd: 1408,
widescreen: 1215,
  desktop: 1023,
  tablet: 768,
  mobile: 0
}
const media = Object
    .keys(screenSizes)
    .reduce((acc, label) => {
        acc[label] = (...args) => css`
            #media (min-width: ${screenSizes[label] / 16}rem) {
                ${css(...args)}
            }
        `
        return acc
    }, {});
Then you just import and use like so:
import media from './media'
const button = styled.button`
${({large , small})=> media.mobile`
color: red;
font-size: ${large ? '2em' : '1em'};
`}
`
Here's some further reading including using with theming:
Media queries in styled-components
Utilizing Props:
Using the same media query object from above:
Create a helper function to format the styles object to a css string:
const formatCss = (styleObject) => {
return JSON.stringify(styleObject)
.replace(/[{}"']/g,'')
.replace(/,/g,';')
+ ';'
}
Create another helper function to map over the styles and generate queries by mapping over its keys and using bracket notation dynamically add queries:
const mapQueries = (myQueries) =>{
return Object.keys(myQueries).map(key=> media[key]`
${formatCss(myQueries[key])}
`)
}
In your styled-component:
export const Button = styled.button`
${({myQueries}) => !myQueries ? '' : mapQueries(myQueries)}
`
Finally add a myQueries prop to your component like so (notice the use of css-formatted keys instead of javascriptFormatted keys for simplicity):
<Button myQueries={{
mobile:{ color:'red' },
tablet:{ color:'blue', "background-color":'green'},
desktop:{ height:'10rem' , width:'100%'}
}}>Button</Button>
For iterating through all your media queries, you can create a function similar to:
import { css } from "styled-components";
const sizes = {
desktop: 992,
tablet: 768,
phone: 576
};
// Iterate through the sizes and create a media template
const media = Object.keys(sizes).map(screenLabel => {
return {
query: (...args) => css`
#media (max-width: ${sizes[screenLabel] / 16}em) {
${css(...args)}
}
`,
screenLabel
};
});
export default media;
For usage in the component:
import media from "./media";
// The labels for this has to be same as the ones in sizes object
const colors = {
phone: "red",
tablet: "yellow",
desktop: "green"
};
const Heading = styled.h2`
color: blue;
${media.map(
({ query, screenLabel }) => query`
color: ${colors[screenLabel]};
`
)}
`;

Conditional styled-components in object notation

Styled components documentation doesn't mention this case and I can't figure out the syntax.
How would I turn this styled component:
const StyledButton = styled.button`
color: red;
${props => props.disabled && css`
color: grey;
background-color: grey;
`}
`
into object notation:
const StyledButton = styled.button(props => ({
color: 'red',
------
}))
I know the following would solve this question, but for my use case I need to keep the logic from the first exemple. So this won't make it for me:
const StyledButton = styled.button(props => ({
color: props.disabled ? 'grey' : 'red',
backgroundColor: props.disabled ? 'grey' : transparent,
}))
Maybe this would be what you're after (or similar)
const StyledButton = styled.button((props) => {
const disabledStyles = {
color: 'grey',
'background-color': 'grey',
};
return {
color: 'red',
...(props.disabled && disabledStyles)
};
})
I definitely don't understand why you can't use the ternary approach you have above but I've had some weird reqs on projects too. Good luck

Styled Components: nesting and referring to other components at the same time

<FooterLight transparent={true}/>
Is it possible to have a nested rule in definition of FooterLight for which the props value transparent is evaluated. Then it assigns 'color: white' to its children ChangelogVersion and CopyRight?
Next two questions:
Since color: white !important is the same for ChangelogVersion and CopyRight. Can these be merged together in one statement?
Does &&& work to not use !important?
export const FooterLight = styled(Navbar).attrs({fixed: 'bottom'})`
background-color: ${props => props.transparent
? 'transparent'
: 'white'};
${props => props.transparent && ChangelogVersion} {
color: white !important;
}
${props => props.transparent && CopyRight} {
color: white !important;
}
`
export const ChangelogVersion = styled(NavLink)`
&&& {
font-size: 14px !important;
padding-right: .5rem;
}
`
export const CopyRight = styled.div `
padding: .5rem 0;
color: '#777777';
font-size: 14px;
}
Sure can... You could do something like this:
export const FooterLight = styled(Navbar).attrs({fixed: 'bottom'})`
background-color: ${props => props.transparent
? 'transparent'
: 'white'};
${ChangelogVersion}, ${CopyRight} {
${props => props.transparent && "color: white !important;"}
}
`
As for your second statement, &&& might work but you're better off structuring the CSS better so it doesn't need to be !important in the first place... In you're example there's no reason for any of the importants to be there, so it's hard to offer a suggestion.

Categories