Introduction
This is part five of our series on building a design system using Solidjs. In the previous tutorial we created our first theme able Badge
component. In this tutorial we will create our theme able component Button component with light and dark modes. I would encourage you to play around with the deployed storybook. All the code for this series is available on GitHub.
Step One: Button styles & theming
We want to achieve the following for the Button
component -
<Button isRounded variant='bordered' isAuto colorScheme='success'>
Success
</Button>
We have the colorScheme
prop, taking values - success, error, warning, etc. We also pass the variant prop we can pass it values solid, ghost, flat, etc.
. Basically, we have the colorScheme
& variant props
combination. We also have 3 size variants - sm, md, lg.
Under components/atoms
create a new folder forms
, under forms
create an index.ts
file and a new folder button
. Finally under components/atoms/forms/button
create a new file button.scss
-
.button {
appearance: none;
box-sizing: border-box;
font-weight: $font-size-sm;
display: flex;
align-items: center;
justify-content: center;
line-height: 1.25;
user-select: none;
text-align: center;
white-space: nowrap;
border: none;
cursor: pointer;
padding: 0;
transition: background 0.25s ease 0s,
color 0.25s ease 0s,
border-color 0.25s ease 0s,
box-shadow 0.25s ease 0s,
transform 0.25s ease 0s,
opacity 0.25s ease 0s;
border-width: $border-weight-normal;
&:active {
transform: scale(0.97);
}
&.xs {
border-radius: $radii-xs;
height: $space-10;
padding-left: $space-3;
padding-right: $space-3;
line-height: $space-10;
min-width: $space-20;
font-size: $font-size-xs;
}
&.sm {
border-radius: $radii-sm;
height: $space-12;
padding-left: $space-5;
padding-right: $space-5;
line-height: $space-14;
min-width: $space-36;
font-size: $font-size-sm;
}
&.md {
border-radius: $radii-md;
height: $space-14;
padding-left: $space-7;
padding-right: $space-7;
line-height: $space-14;
min-width: $space-48;
font-size: $font-size-sm;
}
&.lg {
border-radius: $radii-base;
height: $space-16;
padding-left: $space-9;
padding-right: $space-9;
line-height: $space-15;
min-width: $space-60;
font-size: $font-size-md;
}
&.xl {
border-radius: $radii-xl;
height: $space-18;
padding-left: $space-10;
padding-right: $space-10;
line-height: $space-17;
min-width: $space-72;
font-size: $font-size-lg;
}
&.is-rounded {
border-radius: $radii-pill;
}
&.is-auto {
width: auto;
min-width: min-content;
}
&.solid {
@each $scheme in $color-schemes {
$base: --color-#{$scheme}; // --color-primary
$contrast: #{$base}-solid-contrast; // --color-primary-solid-contrast
&.#{$scheme} {
background: var($base);
color: var($contrast);
}
}
}
&.is-shadow {
@each $scheme in $color-schemes {
&.#{$scheme} {
box-shadow: 0 4px 14px 0 var(--color-#{$scheme}-shadow);
}
}
}
&.bordered {
background-color: transparent;
border-style: solid;
@each $scheme in $color-schemes {
&.#{$scheme} {
color: var(--color-#{$scheme});
border-color: var(--color-#{$scheme});
}
}
}
&.ghost {
background: transparent;
border-style: solid;
@each $scheme in $color-schemes {
$base: --color-#{$scheme}; // --color-primary
$contrast: #{$base}-solid-contrast; // --color-primary-solid-contrast
&.#{$scheme} {
color: var($base);
border-color: var($base);
&:hover {
color: var($contrast);
background-color: var($base);
}
}
}
}
&.light {
background-color: transparent;
@each $scheme in $color-schemes {
$base: --color-#{$scheme}; // --color-primary
$active: #{$base}-light-active; // --color-primary-light-active
&.#{$scheme} {
color: var($base);
&:active {
background-color: var($active);
}
}
}
}
&.flat {
&.neutral {
background-color: var(--color-neutral-light);
color: var(--color-neutral-light-contrast);
&:hover {
background-color: var(--color-neutral-light-hover);
}
&:active {
background-color: var(--color-neutral-light-active);
}
}
&.primary {
background-color: var(--color-primary-light);
color: var(--color-primary-light-contrast);
&:hover {
background-color: var(--color-primary-light-hover);
}
&:active {
background-color: var(--color-primary-light-active);
}
}
&.secondary {
background-color: var(--color-secondary-light);
color: var(--color-secondary-light-contrast);
&:hover {
background-color: var(--color-secondary-light-hover);
}
&:active {
background-color: var(--color-secondary-light-active);
}
}
&.warning {
background-color: var(--color-warning-light);
color: var(--color-warning-light-contrast);
&:hover {
background-color: var(--color-warning-light-hover);
}
&:active {
background-color: var(--color-warning-light-active);
}
}
&.success {
background-color: var(--color-success-light);
color: var(--color-success-light-contrast);
&:hover {
background-color: var(--color-success-light-hover);
}
&:active {
background-color: var(--color-success-light-active);
}
}
&.error {
background-color: var(--color-error-light);
color: var(--color-error-light-contrast);
&:hover {
background-color: var(--color-error-light-hover);
}
&:active {
background-color: var(--color-error-light-active);
}
}
}
}
For the css classes we have to -
- First we created the base
.button
class. - Then we created the size variants classes
sm, md, lg
scoping all of them under the button class. - We then create colorScheme classes using the rightful colors for each colorScheme. We then combine colorScheme with variants like
flat
,bordered
-.flat .warning, .bordered .success
, we used scss loops to create all these combinations.
Step Two: Button component
Under forms/button
create a new file index.tsx
-
import { Component, ComponentProps, splitProps } from 'solid-js'
import { cva, VariantProps } from 'class-variance-authority'
import { ColorScheme } from '../../../../cva-utils'
import './button.scss'
const button = cva(['button'], {
variants: {
size: {
xs: 'xs',
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl'
},
isRounded: {
true: 'is-rounded'
},
isAuto: {
true: 'is-auto'
},
isShadow: {
true: 'is-shadow'
},
variant: {
solid: 'solid',
ghost: 'ghost',
flat: 'flat',
light: 'light',
bordered: 'bordered'
}
},
defaultVariants: {
variant: 'solid',
size: 'md'
}
})
export type ButtonProps = VariantProps<typeof button> &
ComponentProps<'button'> & {
colorScheme?: ColorScheme
}
export const Button: Component<ButtonProps> = (props) => {
const [variants, colorScheme, children, delegated] = splitProps(
props,
['size', 'isRounded', 'isAuto', 'isShadow', 'variant'],
['colorScheme'],
['children']
)
return (
<button
class={button({
isAuto: variants.isAuto,
size: variants.size,
isRounded: variants.isRounded,
isShadow: variants.isShadow,
variant: variants.variant,
className: colorScheme.colorScheme || 'primary'
})}
{...delegated}
>
{children.children}
</button>
)
}
The above code is pretty straightforward.
Step Three: Button story
Under atoms/button
create a new file button.stories.tsx
-
/** @jsxImportSource solid-js */
import { StoryObj } from 'storybook-solidjs'
import { colorSchemes } from '../../../../cva-utils'
import { Flex } from '../../layouts'
import { Button, ButtonProps } from '.'
export default {
title: 'Atoms/Forms/Button'
}
export const Playground: StoryObj<ButtonProps> = {
parameters: {
theme: 'split'
},
args: {
colorScheme: 'primary',
variant: 'solid',
size: 'md',
isRounded: false,
isAuto: false,
isShadow: false
},
argTypes: {
colorScheme: {
name: 'colorScheme',
type: { name: 'string', required: false },
options: colorSchemes,
description: 'The Color Scheme for the button',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'green' }
},
control: {
type: 'select'
}
},
size: {
name: 'size (s)',
type: { name: 'string', required: false },
options: ['xs', 'sm', 'md', 'lg', 'xl'],
description: 'Button height width and horizontal padding',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'md' }
},
control: {
type: 'select'
}
},
variant: {
name: 'variant',
type: { name: 'string', required: false },
options: ['solid', 'bordered', 'ghost', 'flat', 'light'],
description: 'Button Variant',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'md' }
},
control: {
type: 'select'
}
},
isRounded: {
name: 'isRounded',
type: { name: 'boolean', required: false },
description: 'Is Rounded prop',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' }
},
control: {
type: 'boolean'
}
},
isAuto: {
name: 'isAuto',
type: { name: 'boolean', required: false },
description: 'Is Auto prop',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' }
},
control: {
type: 'boolean'
}
},
isShadow: {
name: 'isShadow',
type: { name: 'boolean', required: false },
description: 'Is shadow prop',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' }
},
control: {
type: 'boolean'
}
}
},
render: (args) => <Button {...args}>Button Component</Button>
}
export const Default = {
render: () => (
<Flex direction='col' gap='xl'>
<Flex gap='md' wrap='wrap'>
<Button>Primary</Button>
<Button colorScheme='secondary'>Secondary</Button>
<Button colorScheme='success'>Success</Button>
<Button colorScheme='warning'>Warning</Button>
<Button colorScheme='error'>Error</Button>
</Flex>
<Flex gap='md'>
<Button isAuto>Primary</Button>
<Button isAuto colorScheme='secondary'>
Secondary
</Button>
<Button isAuto colorScheme='success'>
Success
</Button>
<Button isAuto colorScheme='warning'>
Warning
</Button>
<Button isAuto colorScheme='error'>
Error
</Button>
</Flex>
<Flex gap='md'>
<Button isShadow isAuto>
Primary
</Button>
<Button isShadow isAuto colorScheme='secondary'>
Secondary
</Button>
<Button isShadow isAuto colorScheme='success'>
Success
</Button>
<Button isShadow isAuto colorScheme='warning'>
Warning
</Button>
<Button isShadow isAuto colorScheme='error'>
Error
</Button>
</Flex>
<Flex gap='md'>
<Button variant='bordered' isAuto>
Primary
</Button>
<Button variant='bordered' isAuto colorScheme='secondary'>
Secondary
</Button>
<Button variant='bordered' isAuto colorScheme='success'>
Success
</Button>
<Button variant='bordered' isAuto colorScheme='warning'>
Warning
</Button>
<Button variant='bordered' isAuto colorScheme='error'>
Error
</Button>
</Flex>
<Flex gap='md'>
<Button isRounded isAuto>
Primary
</Button>
<Button isRounded isAuto colorScheme='secondary'>
Secondary
</Button>
<Button isRounded isAuto colorScheme='success'>
Success
</Button>
<Button isRounded isAuto colorScheme='warning'>
Warning
</Button>
<Button isRounded isAuto colorScheme='error'>
Error
</Button>
</Flex>
<Flex gap='md'>
<Button isRounded variant='bordered' isAuto>
Primary
</Button>
<Button isRounded variant='bordered' isAuto colorScheme='secondary'>
Secondary
</Button>
<Button isRounded variant='bordered' isAuto colorScheme='success'>
Success
</Button>
<Button isRounded variant='bordered' isAuto colorScheme='warning'>
Warning
</Button>
<Button isRounded variant='bordered' isAuto colorScheme='error'>
Error
</Button>
</Flex>
<Flex gap='md'>
<Button variant='ghost' isAuto>
Primary
</Button>
<Button variant='ghost' isAuto colorScheme='secondary'>
Secondary
</Button>
<Button variant='ghost' isAuto colorScheme='success'>
Success
</Button>
<Button variant='ghost' isAuto colorScheme='warning'>
Warning
</Button>
<Button variant='ghost' isAuto colorScheme='error'>
Error
</Button>
</Flex>
<Flex gap='md'>
<Button variant='flat' isAuto>
Primary
</Button>
<Button variant='flat' isAuto colorScheme='secondary'>
Secondary
</Button>
<Button variant='flat' isAuto colorScheme='success'>
Success
</Button>
<Button variant='flat' isAuto colorScheme='warning'>
Warning
</Button>
<Button variant='flat' isAuto colorScheme='error'>
Error
</Button>
</Flex>
<Flex gap='md'>
<Button variant='light' isAuto>
Primary
</Button>
<Button variant='light' isAuto colorScheme='secondary'>
Secondary
</Button>
<Button variant='light' isAuto colorScheme='success'>
Success
</Button>
<Button variant='light' isAuto colorScheme='warning'>
Warning
</Button>
<Button variant='light' isAuto colorScheme='error'>
Error
</Button>
</Flex>
</Flex>
)
}
From the terminal run yarn storybook
and check the button stories.
Conclusion
In this tutorial we created theme able Button
component. All the code for this tutorial can be found here. Until next time PEACE.
Top comments (0)