Introduction
This is part four of our series on building a design system using Solidjs. In the previous tutorial we created our Box
& Flex
components. In this tutorial we will create our first theme able component Badge 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: Badge styles & theming
We want to achieve the following for the Badge
component -
<Badge colorScheme="success" isFlat isSquared size="md">
Sample Badge
</Badge>
We have the colorScheme
prop, taking values - success, error, warning, etc. We can pass 3 variant props to the component - isFlat, isSquared, bordered. Basically, we have the colorScheme
& variant props
combination. We also have 3 size variants - sm, md, lg.
In the previous iteration of our design-system for handling the light and dark modes we will created 2 different classes using the data attribute selector [data-theme="dark"]
. In this tutorial series we will use css variables for theming, all the CSS variables are under the scss/themes
folder.
The consumer of our library will add the root
class and the light-theme
/ dark-theme
class to the root of the project, we won't be creating any ThemeProvider
because we don't need it.
Under components/atoms
create a new folder badge
and under badge
folder create a new file badge.scss
-
.badge {
--badge-font-size: none;
--badge-shadow-color: none;
box-sizing: border-box;
line-height: 1;
white-space: nowrap;
font-weight: 700;
font-size: var(--badge-font-size);
border-radius: $radii-pill;
&.xs {
padding: $space-2 $space-2;
--badge-font-size: 0.65rem;
}
&.sm {
padding: $space-2 $space-3;
--badge-font-size: 0.73rem;
}
&.md {
padding: $space-3 $space-4;
--badge-font-size: #{$font-size-xs};
}
&.lg {
padding: $space-4 $space-5;
--badge-font-size: #{$font-size-base};
}
&.xl {
padding: $space-5 $space-6;
--badge-font-size: #{$font-size-xl};
}
&.neutral {
background-color: var(--color-neutral);
color: var(--color-neutral-solid-contrast);
--badge-shadow-color: var(--color-neutral-shadow);
}
&.primary {
background-color: var(--color-primary);
color: var(--color-primary-solid-contrast);
--badge-shadow-color: var(--color-primary-shadow);
}
&.secondary {
background-color: var(--color-secondary);
color: var(--color-secondary-solid-contrast);
--badge-shadow-color: var(--color-secondary-shadow);
}
&.success {
background-color: var(--color-success);
color: var(--color-success-solid-contrast);
--badge-shadow-color: var(--color-success-shadow);
}
&.warning {
background-color: var(--color-warning);
color: var(--color-warning-solid-contrast);
--badge-shadow-color: var(--color-warning-shadow);
}
&.error {
background-color: var(--color-error);
color: var(--color-error-solid-contrast);
--badge-shadow-color: var(--color-error-shadow);
}
&.enable-shadow {
box-shadow: 0 2px 10px 0 var(--badge-shadow-color);
}
&.is-squared {
border-radius: calc(var(--badge-font-size) * 0.45);
}
&.is-flat {
@each $scheme in $color-schemes {
$bg: --color-#{$scheme}-light; // --color-primary-light
$color: #{$bg}-contrast; // --color-primary-light-contrast
&.#{$scheme} {
background-color: #{var($bg)};
color: #{var($color)};
}
}
}
&.is-bordered {
background-color: var(--background);
border-width: 2px;
border-style: solid;
@each $scheme in $color-schemes {
&.#{$scheme} {
color: var(--color-#{$scheme}); // --color-primary
}
}
}
}
For the css classes we have to -
- First we created the base
.badge
class. - Then we created the size variants classes
sm, md, lg
scoping all of them under the badge class. - We then create colorScheme classes using the rightful colors for each colorScheme. We then combine colorScheme with variants like
is-flat
,is-bordered
-.is-flat .warning, .is-bordered .success
, we used scss map function to create all these combinations. - Take a note we are using the local css variables like the
--badge-shadow-color
setting appropriate values for each colorScheme. You need to be smart about using local CSS variables, Scss functions for such use cases.
I would encourage you to check out my previous tutorial series to check the amount of code we have reduced, because we are using CSS variables for our theming.
Notice one thing, we have not imported $color-schemes from our variables folder, instead of importing our variables in every .scss file we will add the following configuration to vite's defineConfig
function -
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "./src/scss/variables/_borders.scss" as *;
@use "./src/scss/variables/_fonts.scss" as *;
@use "./src/scss/variables/_radii.scss" as *;
@use "./src/scss/variables/_spacings.scss" as *;
@use "./src/scss/variables/_colors.scss" as *;
`
}
}
},
It will add these variables to all scss
files that are imported in .tsx
files, like we imported our badge.scss
file in the badge/index.tsx
file.
Step Two: Badge component
Under atoms/badge
create a new file index.tsx
-
import { cva, VariantProps } from 'class-variance-authority'
import { Component, ComponentProps, mergeProps, splitProps } from 'solid-js'
import { ColorScheme } from '../../../cva-utils'
import './badge.scss'
const badge = cva(['badge'], {
variants: {
size: {
xs: 'xs',
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl'
},
enableShadow: {
true: 'enable-shadow'
},
bordered: {
true: 'is-bordered'
},
isFlat: {
true: 'is-flat'
},
isSquared: {
true: 'is-squared'
}
},
defaultVariants: {
size: 'md'
}
})
export type BadgeProps = VariantProps<typeof badge> &
ComponentProps<'span'> & {
colorScheme?: ColorScheme
}
export const Badge: Component<BadgeProps> = (props) => {
const mergedProps = mergeProps({ colorScheme: 'neutral' }, props)
const [variants, colorScheme, delegated] = splitProps(
mergedProps,
['size', 'enableShadow', 'bordered', 'isFlat', 'isSquared'],
['colorScheme']
)
return (
<span
class={badge({
size: variants.size,
enableShadow: variants.enableShadow,
bordered: variants.bordered,
isFlat: variants.isFlat,
isSquared: variants.isSquared,
className: colorScheme.colorScheme
})}
{...delegated}
/>
)
}
The above code is pretty straightforward.
Step Three: Badge story
Under atoms/badge
create a new file badge.stories.tsx
-
/** @jsxImportSource solid-js */
import { colorSchemes } from '../../../cva-utils'
import { Flex } from '../layouts'
import { Badge, BadgeProps } from '.'
import { StoryObj } from 'storybook-solidjs'
export default {
title: 'Atoms/Badge'
}
export const Default: StoryObj<BadgeProps> = {
args: {
size: 'md'
},
argTypes: {
size: {
name: 'size (s)',
type: { name: 'string', required: false },
options: ['xs', 'sm', 'md', 'lg', 'xl'],
description: 'Tag height width and horizontal padding',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'md' }
},
control: {
type: 'select'
}
}
},
render: (args) => (
<Flex direction='col' gap='lg'>
<Flex gap='xl'>
<Badge {...args}>Neutral</Badge>
<Badge {...args} colorScheme='primary'>
Primary
</Badge>
<Badge {...args} colorScheme='secondary'>
Secondary
</Badge>
<Badge {...args} colorScheme='success'>
Success
</Badge>
<Badge {...args} colorScheme='warning'>
Warning
</Badge>
<Badge {...args} colorScheme='error'>
Error
</Badge>
</Flex>
<Flex gap='xl'>
<Badge {...args} enableShadow>
Neutral
</Badge>
<Badge {...args} enableShadow colorScheme='primary'>
Primary
</Badge>
<Badge {...args} enableShadow colorScheme='secondary'>
Secondary
</Badge>
<Badge {...args} enableShadow colorScheme='success'>
Success
</Badge>
<Badge {...args} enableShadow colorScheme='warning'>
Warning
</Badge>
<Badge {...args} enableShadow colorScheme='error'>
Error
</Badge>
</Flex>
<Flex gap='xl'>
<Badge {...args} bordered>
Neutral
</Badge>
<Badge {...args} bordered colorScheme='primary'>
Primary
</Badge>
<Badge {...args} bordered colorScheme='secondary'>
Secondary
</Badge>
<Badge {...args} bordered colorScheme='success'>
Success
</Badge>
<Badge {...args} bordered colorScheme='warning'>
Warning
</Badge>
<Badge {...args} bordered colorScheme='error'>
Error
</Badge>
</Flex>
<Flex gap='xl'>
<Badge {...args} isFlat>
Neutral
</Badge>
<Badge {...args} isFlat colorScheme='primary'>
Primary
</Badge>
<Badge {...args} isFlat colorScheme='secondary'>
Secondary
</Badge>
<Badge {...args} isFlat colorScheme='success'>
Success
</Badge>
<Badge {...args} isFlat colorScheme='warning'>
Warning
</Badge>
<Badge {...args} isFlat colorScheme='error'>
Error
</Badge>
</Flex>
<Flex gap='xl'>
<Badge {...args} isSquared>
Neutral
</Badge>
<Badge {...args} isSquared colorScheme='primary'>
Primary
</Badge>
<Badge {...args} isSquared colorScheme='secondary'>
Secondary
</Badge>
<Badge {...args} isSquared colorScheme='success'>
Success
</Badge>
<Badge {...args} isSquared colorScheme='warning'>
Warning
</Badge>
<Badge {...args} isSquared colorScheme='error'>
Error
</Badge>
</Flex>
<Flex align='start' gap='xl'>
<Badge {...args} isSquared>
Neutral
</Badge>
<Badge {...args} isSquared bordered colorScheme='primary'>
Primary
</Badge>
<Badge {...args} isSquared isFlat colorScheme='secondary'>
Secondary
</Badge>
<Badge {...args} isSquared colorScheme='success'>
Success
</Badge>
<Badge {...args} isSquared bordered colorScheme='warning'>
Warning
</Badge>
<Badge {...args} isSquared isFlat colorScheme='error'>
Error
</Badge>
</Flex>
</Flex>
)
}
export const Playground: StoryObj<BadgeProps> = {
parameters: {
theme: 'split'
},
args: {
colorScheme: 'success',
size: 'md',
bordered: false,
isFlat: false,
isSquared: false,
enableShadow: 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: 'Tag height width and horizontal padding',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'md' }
},
control: {
type: 'select'
}
},
bordered: {
name: 'bordered',
type: { name: 'boolean', required: false },
description: 'Is Bordered',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' }
},
control: {
type: 'boolean'
}
},
isFlat: {
name: 'isFlat',
type: { name: 'boolean', required: false },
description: 'Is Flat',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' }
},
control: {
type: 'boolean'
}
},
isSquared: {
name: 'isSquared',
type: { name: 'boolean', required: false },
description: 'Is Bordered',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' }
},
control: {
type: 'boolean'
}
},
enableShadow: {
name: 'enableShadow',
type: { name: 'boolean', required: false },
description: 'enableShadow',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' }
},
control: {
type: 'boolean'
}
}
},
render: (args) => <Badge {...args}>Sample Badge</Badge>
}
From the terminal run yarn storybook
and check the badge stories.
Step Four: Create a theme addon in Storybook
Badge
component works both for light and dark mode, but we don't have a way in storybook to switch themes. Well we have to build one. I would recommend, you read this awesome article on how to create a theme switcher in storybook.
Under .storybook/preview.tsx
file paste the following -
/** @jsxImportSource solid-js */
import { Component } from 'solid-js'
import { Flex, FlexProps } from '../src/components/atoms'
import '../src/scss/main.scss'
const Container: Component<FlexProps> = (props) => {
return (
<Flex
align='start'
p='md'
style='min-height: 100vh; flex-basis: 50%;'
{...props}
/>
)
}
const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/
}
}
}
}
export const decorators = [
(StoryFun, context) => {
const theme = context.parameters.theme || context.globals.theme
if (theme === 'split') {
return (
<Flex>
<Container class='root light-theme' bg='white'>
<StoryFun />
</Container>
<Container class='root dark-theme' bg='black'>
<StoryFun />
</Container>
</Flex>
)
}
return (
<Container
class={theme === 'dark' ? 'root dark-theme' : 'root light-theme'}
bg={theme === 'dark' ? 'black' : 'white'}
>
<StoryFun />
</Container>
)
}
]
export const globalTypes = {
theme: {
name: 'Change Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
// The icon for the toolbar item
icon: 'circlehollow',
// Array of options
items: [
{ value: 'light', icon: 'circlehollow', title: 'light-view' },
{ value: 'dark', icon: 'circle', title: 'dark-view' },
{ value: 'split', icon: 'graphline', title: 'split-view' }
],
// Property that specifies if the name of the item will be displayed
showName: true
}
}
}
export default preview
Now from the terminal run yarn storybook
and play with the Theme switcher the above code will be understandable. We first add the root
class to the main root element. To change the theme as stated earlier we will use the light-theme
and dark-theme
classes on our root element, we would change the theme in a similar way if are using our library in a Solid project.
Conclusion
In this tutorial we created the first theme able component Badge
. All the code for this tutorial can be found here. In the next tutorial we will create a theme able Button component. Until next time PEACE.
Top comments (0)