Approach to creating variants with styled components - css

What is the best way to create variants using styled components? Heres what i am currently doing.
const ButtonStyle = styled.button`
padding:8px 20px;
border:none;
outline:none;
font-weight:${props => props.theme.font.headerFontWeight};
font-size:${props => props.theme.font.headerFontSize};
display:block;
&:hover{
cursor:pointer;
}
${({ variant }) =>
variant == 'header' && css`
background-color:${props => props.theme.colors.lightblue};
color:${({ theme }) => theme.colors.white};
&:active{
background-color:${props => props.theme.colors.blue}
}
`
}
${({ variant }) =>
variant == 'white' && css`
background-color:white;
color:${({ theme }) => theme.colors.lightblue};
&:active{
color:${props => props.theme.colors.blue}
}
`
}
`;
I cannot tell if this is the standard way of doing things.
I have also been using other components as bases to create other components from while changing a few things
eg
const InnerDiv = styled(otherComponent)`
position: unset;
background-color: red;
overflow-x: hidden;
display: flex;
`;
Which is the better approach? Are there any better alternatives?

Inspired by previous solutions, I want to share what I came up with:
import styled, { css, DefaultTheme } from 'styled-components';
const variantStyles = (theme: DefaultTheme, variant = 'primary') =>
({
primary: css`
color: ${theme.colors.light};
background: ${theme.colors.primary};
border: 1px solid ${theme.colors.primary};
`,
}[variant]);
const Button = styled.button<{ variant: string }>`
padding: 1rem;
font-size: 0.875rem;
transition: all 0.3s;
cursor: pointer;
${({ theme, variant }) => variantStyles(theme, variant)}
&:active {
transform: translateY(1.5px);
}
`;
export default Button;
For now it contains only primary and its the default one, by you can add more variants by adding new object to variantStyles object
Then you can use it by passing the variant as a prop or keep the default by not passing any variant.
import { Button } from './HeroSection.styles';
<Button variant="primary">Start Learning</Button>

This is just my opinion:
I don't think we can do anything very different from what you did.
A different way that I thought, would be to create an options object to map the possibilities of the variant, like this:
const variantOptions = {
header: {
backgroundColor: theme.colors.lightblue,
color: theme.colors.white,
active: theme.colors.blue,
},
white: {
backgroundColor: "white",
color: theme.colors.lightblue,
active: theme.colors.blue,
},
};
And use it in your style component like this:
const ButtonStyle = styled.button`
padding: 8px 20px;
border: none;
outline: none;
font-weight: ${(props) => props.theme.font.headerFontWeight};
font-size: ${(props) => props.theme.font.headerFontSize};
display: block;
&:hover {
cursor: pointer;
}
${({ variant }) =>
variant &&
variantOptions[variant] &&
css`
background-color: ${variantOptions[variant].backgroundColor};
color: ${variantOptions[variant].color};
&:active {
color: ${variantOptions[variant].active};
}
`}
`;
And all of this buttons will work:
<ButtonStyle variant="*wrong*">Button</ButtonStyle>
<ButtonStyle variant="header">Button</ButtonStyle>
<ButtonStyle variant="white">Button</ButtonStyle>
<ButtonStyle>Button</ButtonStyle>

When dealing with Styled Component variants here is what I like to do to keep things organised and scalable.
If the variants are stored within the same file I am using the inheritance properties:
const DefaultButton = styled.button`
color: ${(props) => props.theme.primary};
`;
const ButtonFlashy = styled(DefaultButton)`
color: fuchsia;
`;
const ButtonDisabled = styled(DefaultButton)`
color: ${(props) => props.theme.grey};
`;
If if we are talking about a reusable components I would use this technique:
import styled from 'styled-components';
// Note that having a default class is important
const StyledCTA = ({ className = 'default', children }) => {
return <Wrapper className={className}>{children}</Wrapper>;
};
/*
* Default Button styles
*/
const Wrapper = styled.button`
color: #000;
`;
/*
* Custom Button Variant 1
*/
export const StyledCTAFushia = styled(StyledCTA)`
&& {
color: fuchsia;
}
`;
/*
* Custom Button Variant 2
*/
export const StyledCTADisabled = styled(StyledCTA)`
&& {
color: ${(props) => props.theme.colors.grey.light};
}
`;
export default StyledCTA;
Usage:
import StyledCTA, { StyledCTADisabled, StyledCTAFushia } from 'components/StyledCTA';
const Page = () => {
return (
<>
<StyledCTA>Default CTA</StyledCTA>
<StyledCTADisabled>Disable CTA</StyledCTADisabled>
<StyledCTAFushia>Fuchsia CTA</StyledCTAFushia>
</>
)
};
Read more about this in the blog posts I created on the subject here and there.

There are many ways to do this. one simple way is to use the package called Styled-components-modifiers. documentation is simple and straightforward.
https://www.npmjs.com/package/styled-components-modifiers
Simple usage example:
import { applyStyleModifiers } from 'styled-components-modifiers';
export const TEXT_MODIFIERS = {
success: () => `
color: #118D4E;
`,
warning: () => `
color: #DBC72A;
`,
error: () => `
color: #DB2A30;
`,
};
export const Heading = styled.h2`
color: #28293d;
font-weight: 600;
${applyStyleModifiers(TEXT_MODIFIERS)};
`;
In the Component - import Heading and use modifier prop to select the variants.
<Heading modifiers='success'>
Hello Buddy!!
</Heading>

Styled components are usually used with Styled system that supports variants and other nice features that enhance Styled components. In the example below Button prop variant automatically is mapped to keys of variants object:
const buttonVariant = ({ theme }) =>
variant({
variants: {
header: {
backgroundColor: theme.colors.lightblue,
color: theme.colors.white,
active: theme.colors.blue,
},
white: {
backgroundColor: 'white',
color: theme.colors.lightblue,
active: theme.colors.blue,
},
},
})
const Button = styled.button`
${(props) => buttonVariant(props)}
`
Styled System Variants: https://styled-system.com/variants

Use the variant API to apply styles to a component based on a single prop. This can be a handy way to support slight stylistic variations in button or typography components.
Import the variant function and pass variant style objects in your component definition. When defining variants inline, you can use Styled System like syntax to pick up values from your theme.
// example Button with variants
import styled from 'styled-components'
import { variant } from 'styled-system'
const Button = styled('button')(
{
appearance: 'none',
fontFamily: 'inherit',
},
variant({
variants: {
primary: {
color: 'white',
bg: 'primary',
},
secondary: {
color: 'white',
bg: 'secondary',
},
}
})
)

Related

How to add a state and a list of links displayed in react js and storybook?

I need to have the accordion component that will change over a state when user clicks on the icon.
When user clicks on the icon, the icon and color of the title should get changed and list of links should display.
So far I managed to add onClick option to the component but what will be the best approach to achieve the result of a list displaying over a click?
How to make the icon change over click and display a list with storybook?
Could you please kindly give some advise?
Any help would be appreciated.
I already added two options for the icon, so I think I can add somehow a state to it and make it display on storybook, but not really sure how :(
Here is the component:
import { string, oneOf, func, bool } from "prop-types"
import Icon, { icons } from "design-system/components/icon"
import * as Styled from "./Button.styled"
const Button = ({
href,
text,
iconStart,
iconEnd,
variant,
color,
size,
active,
onClick,
}) => (
<Styled.Component
as={href ? `a` : `button`}
variant={variant}
color={color}
size={size}
href={href}
onClick={onClick}
>
{iconStart && (
<Styled.Icon>
<Icon name={iconStart} size={size} />
</Styled.Icon>
)}
<Styled.Text variant={variant} color={color} active={active}>
{text}
</Styled.Text>
{iconEnd && (
<Styled.Icon>
<Icon name={iconEnd} size={size} />
</Styled.Icon>
)}
</Styled.Component>
)
Button.propTypes = {
text: string.isRequired,
href: string,
iconStart: oneOf(Object.keys(icons)),
iconEnd: oneOf(Object.keys(icons)),
variant: oneOf(["fill", "border", "text", "textLine"]),
color: oneOf(["primary", "black", "white"]),
size: oneOf(["small", "medium", "large"]),
active: bool,
onClick: func,
}
Button.defaultProps = {
href: null,
iconStart: null,
iconEnd: null,
variant: "fill",
color: "primary",
size: "medium",
active: null,
onClick: null,
}
export default Button
Here are the styles:
import styled from "#emotion/styled"
import { css } from "#emotion/react"
import theme from "design-system/theme"
const sizes = {
small: {
typography: theme.typography.desktop.bodySmall,
padding: "8px 32px",
},
medium: {
typography: theme.typography.desktop.h5,
padding: "10px 40px",
},
large: {
typography: theme.typography.desktop.h4,
padding: "12px 48px",
},
}
const colors = {
primary: {
mainColor: theme.colors.primary[500],
filledText: theme.colors.neutrals[100],
},
black: {
mainColor: theme.colors.grey[600],
filledText: theme.colors.neutrals[100],
},
white: {
mainColor: theme.colors.neutrals[100],
filledText: theme.colors.primary[300],
},
}
export const Component = styled.a`
text-align: center;
border-radius: 10px;
padding: 19px;
position: relative;
color: ${({ theme }) => theme.colors.grey[600]};
background-color: ${({ theme }) => theme.colors.complementary[100]};
`
export const Text = styled.span`
${({ variant }) =>
variant === "textLine" &&
css`
text-decoration: underline;
`}
${({ active, variant, color }) =>
active &&
variant === "text" &&
css`
border-bottom: 2px solid ${colors[color].mainColor};
`}
`
export const Icon = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
`
and storybook:
import { Meta, Canvas, Story, ArgsTable } from "#storybook/addon-docs"
import Button from "design-system/components/button"
import Icon from "design-system/components/icon"
<Meta title="Components/Button" component={Button} />
# Button
<Canvas>
<Story
name="Overview - button icon end"
args={{
text: "O nas",
iconEnd: "arrowDown",
variant: "fill",
size: "medium",
}}
>
{Template.bind()}
</Story>
</Canvas>
<ArgsTable />
export const Template = (args) => <Button {...args} />

react styled-component nested item's props

My problem is about props. I want to use nested components with props in styled-components. For example:
const MenuItem = ({ item }) => {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const isActive = router?.asPath === item?.path;
return (
<MenuItemWrapper key={item?.slug} onClick={() => setIsOpen(!isOpen)}>
<Link href={item?.path}>
<InnerMenuItem isActive={isActive}>
{item?.prefix && <Prefix>{item?.prefix}</Prefix>}
{item?.label}
{item?.children && <RightArrow isOpen={isOpen} />}
</InnerMenuItem>
</Link>
<Children>
{
item?.children?.map((child) => <MenuItem item={child} />)
}
</Children>
</MenuItemWrapper>
);
};
export default MenuItem;
this is MenuItem component. I use MenuItem component as a recursion component.
in styled-component i tried this but it doesnt work. I want to apply different style in Children > InnerMenuItem but it not working
export const Children = styled.div`
display: flex;
flex-direction: column;
margin-left: 65px;
${MenuItemWrapper} {
font-size: 16px;
padding: 9px 0;
&:not(:first-child) {
border-top:none;
}
}
${InnerMenuItem} {
${({ isActive }) => // HOW CAN I USE THIS PROPS HERE
isActive &&
css`
color: orange
`};
}
`;
from styled components official documentations:
"If the styled target is a simple element (e.g. styled.div), styled-components passes through any known HTML attribute to the DOM. If it is a custom React component (e.g. styled(MyComponent)), styled-components passes through all props."
example :
const Input = styled.input`
color: ${props => props.inputColor || "palevioletred"};
`;
return(
<Input inputColor="rebeccapurple" />
)
another way is by Extending Styles, example :
const Button = styled.button`
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;
return(
<div>
<Button>Normal Button</Button>
<TomatoButton>Tomato Button</TomatoButton>
</div>
);
more about styled-components read here
have you tried
${InnerMenuItem} {
color: ${({isActive})=> isActive ? 'orange' : undefined}
};

Understanding css helper function in styled components

Styled components has a helper css function. But I don't understand when should I used it.
For example this is their example where they use it:
import styled, { css } from 'styled-components'
const complexMixin = css`
color: ${props => (props.whiteColor ? 'white' : 'black')};
`
const StyledComp = styled.div`
/* This is an example of a nested interpolation */
${props => (props.complex ? complexMixin : 'color: blue;')};
`
But if we take similar example from docs here they don't use it:
const Button = styled.button`
/* Adapt the colors based on primary prop */
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
Their description is also not clear and is confusing me:
A helper function to generate CSS from a template literal with
interpolations. You need to use this if you return a template literal
with functions inside an interpolation due to how tagged template
literals work in JavaScript.
Can someone help explain why we need it?
PS this answer also doesn't use it
I use the css function when I create variants for a component:
that is variant switcher:
const sizeVariant: Record<NonNullable<StyledLogoProps['size']>, ReturnType<typeof css>> = {
small: css`
width: 44px;
height: 44px;
`,
regular: css`
width: 93px;
height: 93px;
`,
};
that is component was created by 'styled-components':
interface StyledLogoProps {
size: 'small' | 'regular';
}
export const StyledLogo = styled.div<StyledLogoProps>`
${({ size }) => sizeVariant[size]}
`;
and this is the use in the react:
<>
<StyledLogo size="regular" />
<StyledLogo size="small" />
</>
quite a useful thing

How to conditionally render using object properties in styled-components?

I am trying to conditonally render in styled-components. This code seems to work in this case.
background-color: ${props => (props.active ? 'Black' : 'Green')};
I want to rather use object properties from a JSON file and provide 2 colours to the above condition. Something similar to these below instead of Black and Green.
${colors['Brand/PrimaryBlack']}
${colors['Brand/PrimaryGreen']}
colored.json
{
"colors": {
"Brand/PrimaryBlack": "#424449",
"Brand/PrimaryGreen": "#8ED6C9",
}
}
styles.js
import styled from 'styled-components'
import { colors } from './colored.json'
const Tabs = styled.button`
background-color: ${props => (props.active ? 'Black' : 'Green')};
`
How can I achieve this?
The ternary works exactly the same as your previous code, but just references the keys in your colors JSON, i.e. background-color: ${props => colors[props.active ? "Brand/PrimaryBlack" : "Brand/PrimaryGreen"]};.
{
"colors": {
"Brand/PrimaryBlack": "#424449",
"Brand/PrimaryGreen": "#8ED6C9",
}
}
import styled from 'styled-components'
import { colors } from './colored.json'
const Tabs = styled.button`
background-color: ${props => colors[props.active ? "Brand/PrimaryBlack" : "Brand/PrimaryGreen"]};
`;
You can do what you desire using styled components in the following way:
background-color: ${(props) =>
props.active ? colors["Brand/PrimaryGreen"] : colors["Brand/PrimaryBlack"]};
Find the working CodeSandBox here
Inside of Template literal you pass any valid JavaScript code inside of ${} expression even call to function so if you have an object which you want to access some keys you can just access those keys as you would do in a normal JavaScript code. so if you have an object colors with some properties you can access it inside of you Styled Component like this
const colors = {
"Brand/PrimaryGreen": "green",
"Brand/PrimaryBlack": "black"
};
const Comp = styled.div`
background: ${props => props.active? colors["Brand/PrimaryBlack"] : colors["Brand/PrimaryGreen"]};
color: #fff;
`;
You can simply do this
<TagName active={this.state.active}>Test</TagName>
And in your styles something like this:
const TagName = styled.button`
width: 100%;
outline: 0;
border: 0;
height: 100%;
justify-content: center;
align-items: center;
line-height: 0.2;
${({ active }) => active && `
background: blue;
`}
`;

React: Styled-components loading button

I am using React- typescript for my app. For Styling I am using Styled-components. I have created one global button component. Inside the button component i used loader. My target is when user will click the button it will display loading. From the parent component I have created one fake api call and in there I added settime-out 5s. But when I click the button it does not display the loader which was in Button component. From the parent component set-time out works and it display in my console. I don't know why it does not the display the loading ..... Also i added the disabled option when fake api will call button should be disabled. That logic also does not work.
Here is the Button component
import * as React from "react";
import styled, { css } from "styled-components";
interface IButtonProps {
children?: React.ReactChild;
className?: string;
size?: "small" | "medium" | "big";
themes?: "primary" | "secondary" | "dark" | "light";
disabled?: boolean;
loading?: boolean;
style?: React.CSSProperties;
onClick?: () => void;
onSubmit?: () => void;
}
const Button = ({
children,
className,
size,
themes,
disabled,
loading,
style,
onClick,
onSubmit
}: IButtonProps) => (
<button
className={className}
onClick={onClick}
onSubmit={onSubmit}
style={
disabled && disabled
? { opacity: 0.5, pointerEvents: `none` }
: loading ? { ...style, pointerEvents: `none` } : //This is my disabled condition.
style
}
>
{loading ? <p>loading...</p> : children} //This is my loading condition.
</button>
);
const sizes = {
small: css`
padding: 5px 20px;
font-size: 12px;
`,
medium: css`
padding: 10px 30px;
font-size: 14px;
`,
big: css`
padding: 15px 40px;
font-size: 18px;
`
};
const ButtonThemes = {
primary: css`
border: 1px solid tomato;
background: tomato;
color: white;
`,
secondary: css`
border: 1px solid palevioletred;
background: palevioletred;
color: white;
`,
dark: css`
border: 1px solid #273444;
background: #273444;
color: white;
`,
light: css`
border: 1px solid #eff2f7;
background: #f9fafc;
color: #273444;
`
};
const StyledButton = styled(Button)`
${({ size = "small" }) => sizes[size]};
${({ themes = "primary" }) => ButtonThemes[themes]};
outline: none;
border-radius: 5px;
cursor: pointer;
`;
export default StyledButton;
This is my parent component. Where I used settimeout and import Button component.
const handleClick = () => {
setTimeout(() => {
console.log("check it out") //It does display when the button click
}, 5000);
}
//This is the Button
<Button size="medium" themes="primary" onClick={handleClick}>click</Button>
You should create a loading state by using useState hook if your parent component is function component, if using class component you can define a state in constructor like this this.state = { loading: false } in your parent component, and set it to true in handleClick and pass the state as prop to your Button component:
// In case function component
const [loading, setLoading] = useState(false);
const handleClick = () => {
setLoading(true);
setTimeout(() => {
setLoading(false); // When finish, set it to false
console.log("check it out") //It does display when the button click
}, 5000);
}
//This is the Button
<Button
size="medium"
themes="primary"
onClick={handleClick}
loading={loading}
>
click
</Button>

Resources