We're in the middle of a design revamp at Amondo, changing the looks of our internal curation dashboard. It is a medium-sized dashboard built with React, TypeScript and styled-components, and a custom design system built from scratch. During the revamp it was very easy to change stuff because of the way design system was typed. There was very little chance to accidentally miss or delete something. I thought I'd share some "tips" for using TypeScript to improve your design system.
We have a bunch of components in the design system that allow customisation of appearance through props, say, a Button
.
<Button bg="black" color="yellow">Click me</Button>
Allowing arbitrary colors (hex codes, rgba values) to be passed as color props will eventually lead to inconsistency as the codebase grows, and it will make changing the colors harder. The better thing to do is to limit the colors to a predefined set. The set of colors we use is roughly:
// ui/config.ts
const baseColors = {
yellow: '#FFEA27',
blue: '#2F77F6',
green: '#1BDF99',
red: '#E32418',
// monochrome
dark1: '#18191B',
dark2: '#1F2023',
// ...
// others
twitterBlue: '#1DA1F2',
// ...
}
Usually dashboards have accent colors such as primary, secondary etc. so defining color aliases will make changes in the future very easy.
// ui/config.ts
export const colors = {
...baseColors,
primary: baseColors.blue,
secondary: baseColors.green,
background: baseColors.light1,
}
Instead of typing color props as string
, we define and use a Color
type like so:
// ui/config.ts
export type Color = keyof typeof colors | 'transparent' | 'inherit'
// ui/Button.tsx
interface Props {
// ...
bg?: Color
color?: Color
}
This will enable typechecking and autocomplete in code editors. 😍
The last thing is to parse the color names and use the hex value in the styled component.
// ui/config.ts
export function parseColor(c: Color) {
return colors[c] || c
}
// ui/Button.tsx
export default styled.button<Props>`
/* ... */
color: ${p => parseColor(p.color || defaults.color)};
bg: ${p => parseColor(p.bg || defaults.bg)};
`
We have all our icons as React components rendering SVG. There are about 50 icons, but most of them are a single path
node so they don't contribute much to the output bundle size. Our designers are kind enough so every icon has the same viewBox
. The process of adding new icons is first minifying them in SVGOMG, then converting to JSX. The icons are defined in icons.tsx
file.
// ui/icons.tsx
interface IconProps {
width: number
title?: string
}
const defaultProps = {
viewBox: '0 0 24 24',
fill: 'none',
// https://github.com/PolymerElements/iron-icon/issues/71
focusable: 'false',
}
// Icons
export const backArrow = ({ title, ...props }: IconProps) => (
<svg {...defaultProps} {...props}>
{title && <title>{title}</title>}
<path d="..." stroke="currentColor" />
</svg>
)
// more icons...
The stroke
(or fill
) of the path
is currentColor
, I'll get to why that is useful in a second. And then there's an Icon.tsx
which the design system exports.
// ui/Icon.tsx
// ...
import * as icons from './icons'
export type IconName = keyof typeof icons
export interface Props {
name: IconName
size?: number
color?: Color
title?: string
// ...
}
export const IconWrapper = styled.span<Props>`
display: inline-flex;
align-items: center;
vertical-align: middle;
color: ${p => p.color ? parseColor(p.color) : 'currentColor'};
`
export default function Icon(props: Props) {
const { name, size = 24, title } = props
const Tag = icons[name]
// This shouldn never happen because TypeScript is
// making sure the `name` is defined in `icons.tsx`
if (!Tag) {
console.warn(`Icon ${name} doesn't exist! Maybe the name is incorrect?`)
return null
}
return (
<IconWrapper {...props} role="img">
<Tag title={title} width={size} />
</IconWrapper>
)
}
We again use the keyof typeof
tehnique to get the list of all available icon names, for typechecking and autocomplete. If an icon is removed or renamed TypeScript will complain. By adding a new Icon the type is "updated" automatically, pretty convenient.
currentColor
, a special CSS value, is useful when Icon
is embedded in labels, text and buttons — the icon will inherit the color
value. Example:
<Button bg="primary" color="white">
<Icon name="search" /> Search
</Button>