Nikola Ristić     ← back to index

Tips for making design systems in React and TypeScript

17/04/2020

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.

Color usage

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)};
`

Icons

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>

Nikola Ristić     ← back to index