This repo is intended as an example of a larger scale Design System monorepo that houses a collection of techniques I've accumulated over the years. A lot of the pieces have been influenced by hundreds of hours of work and research, but there are plenty of things I'm still not happy with. I decided to put this out there, regardless of it being far from perfect, because that's just how the development world is and I like it when people share "real" things.
- React as the JS library, utilizing hooks and functional components.
- TypeScript for static type checking throughout the entire repo.
- Next.js as the example framework for testing/dogfooding/etc. the Design System within the
monorepo.
- Any other framework accepting React would do, this is just the most popular one and the one that ships with Turborepo, currently.
- Stitches as the CSS-in-TS library.
- I've used a lot of CSS-in-JS libraries over the years and Stitches is one of my favorites, although it is now deprecated :(.
- Still, the examples are useful, and you can use Class Variant Authority with something like TailwindCSS for a very similar experience.
- Rollup as the TS/JS/CSS bundler.
- I've used many bundlers in the past, and tried many more while on the search for proper tree-shaking and code-splitting for JS and CSS. Rollup is the only one that gave me the full experience I was looking for.
- Storybook for component documentation and testing.
- I've used Storybook for years, and it's still the best option for rapidly developing and maintaining visual components.
- GitHub Actions for CI/CD. This includes building, linting, testing, and uses
Turborepo remote caching for less friction (downtime) on PRs.
- While I don't enjoy the difficulty of troubleshooting Github Actions locally, they are very simple when you know some of the common patterns.
- Changesets for managing versioning and publishing through CI.
- This is my favorite way of handling version updates for published packages on a team, and possibly even for solo development. It strikes a great balance between automation and control for me.
- Each time a PR with a changeset is merged, it will be published to GitHub Packages and a release will be created.
- Fontsource for self-hosted fonts (default and can be overridden).
- In my apps I often use
next/font
, particularly for minimizing layout shift, but I wanted an agnostic self-hosted option as the default.
- In my apps I often use
- ESLint for linting, with Prettier for formatting.
@jimmydalecleveland/stitches-ui-example
: The Design System and main purpose of the Repo. Published as a scoped package on GitHub.web
: a Next.js app for testing/dogfooding/etc. the UI components before publishing.eslint-config-custom
:eslint
configurations (includeseslint-config-next
andeslint-config-prettier
)tsconfig
:tsconfig.json
s used throughout the monorepo. UsesES2022
for the design system, as I only want to support updated evergreen browsers for this project.
Run the following before any commands listed below:
npm i
To build all apps and packages, run the following command:
npm run build
To spin up the Storybook docs for the Design System, run the following command:
npm run docs
To develop all apps and packages, run the following command:
npm run dev
Turborepo can use a technique known as Remote Caching to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. This repo uses my own Vercel account to cache remotely and has been set up already.
I was largely influenced by Braid, and much of Mark Dalgleish's work, for much of this system. I've also studied many grid and column layout systems from the usual suspects (Material, Foundation, Semantic, Chakra, etc.) and taken ideas about many other components from them.
This system makes heavy use of composition, even for spacing and layouts with components like Stack
which you may have
seen in various forms of other systems. I've come to enjoy the reliability of this design philosophy, but I have no
problem admitting it is not without flaws and can feel verbose at times. For me, personally, it is worth the tradeoffs
because I have been in some nightmare situations with previous systems that rely on spacing being handled either inside
components (big ew) or by overwriting child styles from the parent. Finding what is actually applying spacing an
alignment in these systems can eventually be a huge headache, especially without a component snapshot diffing tool like
Chromatic (Storybook team).
It's also really important to note that this Design System was much more complicated than many others I've built due to the challenge of supporting multiple "brands" or themes. Most Design Systems I encounter do not support this functionality well, even though they are made to be adopted however you wish, they don't allow for easy overriding of colors, fonts, spacing, etc. after you have set up the initial theme and components. It honestly might seem like they do if you haven't really tried to apply other popular Design Systems in this fashion, but when I put them to the test, they became impossible or extremely difficult to accomplish this ask.
A key theme across this design system is the idea of Vibes. Rather than using primary
, secondary
, tertiary
and so
on, I've opted to use the purpose of each component in a more actionable way. Here are the vibes
used in this system
as an intro:
neutral
neutralInverse
subdued
positive
warning
critical
info
attract
disabled
Each of these not only sets colors but weights and other styles as well and each component determines how it will
interpret a vibe. For an example, a Button
would choose the most eye-catching background color with a vibe
of attract
, but it would also need to pick an appropriately readable text color. These colors are all typed variables
in the stitches.config.ts
theme file.
This of course includes states like hover, focus, active, etc. as well.
Text
would obviously not have a background but can still have a attract
vibe which while change the css
color to a
standout one described in the theme variables.
The neutral
and neutralInverse
are there to support light and dark themes without assuming which is your default. So
if a neutral
background is white, then neutralInverse
might be black or a very dark blue, as just one example. This
method helps keep the background and foreground colors readable on each other.
Vibes like positive
, warning
, critical
, and info
do not have the typical 10 color range the others do, but
instead only need 5. These are used for callouts, form validation, and such. I hope the names are self-explanatory,
which is why I chose them to be in this format.
Stitches encourages the use of immutable style rules, which can be unintuitive for situations where you'd normally use media queries in the actual styling. Where this becomes powerful, though, is the ability to apply properties responsively when a component is used. Take this simple example:
<Inline alignX={{ "@initial": "center", "@bp2": "left" }}>
<Text>Some text</Text>
<Text>Some more text</Text>
</Inline>
Here, the Inline
component is using the alignX
prop to align the children horizontally. The alignX
prop is typed
to accept a ResponsiveValue
which is an object with keys that match the breakpoints in the stitches.config.ts
file.
The values of the object are the values you want to apply at each breakpoint. In this case, the first breakpoint
is @initial
which is the default value, and the second breakpoint is @bp2
which is the second breakpoint in the
config file. This is a shorthand for @media (min-width: 768px)
(default). The final result is that the elements will
be centered on smaller devices and left aligned on larger.
Box
: The foundation of most components. ItText
: The majority of text/copy on a page.Heading
: A component forhx
tags, that usesclamp
for responsive resizing.- The size of the heading is determined by the
size
prop, which uses element tags as the values to adhere to a consistent style across heading types. - When passing a size, say
size="h2"
, the element will automatically be set as the appropriatehx
tag, so<h2>
in this example. This can be overridden by passing theas
prop with a different heading tag. - Using
clamp
for all sizes, the heading will have a minimum size it will ever be, a maximum size, and it will scale between those sizes based on the viewport width. This is a nice way to handle some other responsive sizing methods that awkwardly shrink too much on smaller devices.
- The size of the heading is determined by the
Button
: Uses thevibe
system, as well as having ahollow
variant for each vibe. Also supports left or right icons as props so it can balance and size them appropriately.- When using an Icon from the Design System with button, it is expected to pass the icon variable, not the instantiation of it. e.g.
<Button RightIcon={Rocket} vibe="attract">
. This is because Button handles calling the Icon and setting its colors to match the button vibe.
- When using an Icon from the Design System with button, it is expected to pass the icon variable, not the instantiation of it. e.g.
Icons
: Each icon is a named component, that uses thevibe
interface, as well as abackgroundVibe
for more flexibility.- Icons also have built in support within certain components, such as
Button
placing it on the left or right and making it match thevibe
of the button. - I wanted
Icons
to be its own export that used method calling (e.g.Icons.Rocket
), but ran into tree-shaking issues with my first attempts. - I have only added a few icons as examples, but they are all from Heroicons. I simply have overrides for this Design System.
- Icons also have built in support within certain components, such as
Inline
: The first "layout control" component. Used for laying out other components and elements in a row, and controlling alignment and spacing across the elements.Inline
is a common component for using "Responsive Properties" as described above.
Divider
: Ok, this one is pretty cool. You wouldn't think aDivider
component could be so challenging to get right, but hoo doggy it can be.- I put a lot of research time into getting the semantic correct on this one, combined with ease of use and just doing what you want automatically.
- The
Divider
can be used standalone, but it is commonly used as a prop for layout components likeStack
andColumns
(coming soon). When the layout orientation is changed, the dividers will swap orientation as well, and filled up the height or width correctly. This is actually very difficult to get correct.
Stack
: A heavy lifter that is the most used layout component, and it deals with spacing between elements. The intended use is to wrap whatever components/elements you want to have space between (including dividers) and set the space property, which will apply the same spacing between all children.- The Stack also handles alignment of children through the align property, as well as automatically making its children wrapped in
li
tags if the as property is set tool
orul
.
- The Stack also handles alignment of children through the align property, as well as automatically making its children wrapped in
Card
: This is a visual component to be used as a container with common spacing, a neutral background, and a shadow. A typical layout of the web.Columns
: This is another crucial layout component, typically used often but not as much asStack
. It is very handy but can be confused as aGrid
from other libraries.- The
Columns
(paired withColumn
) is used for common columnar layouts, such as a 1/3rd 2/3rd split that changes to stacked on smaller devices. - It is not meant for wrapping columned layouts, like css Grid is. I don't particularly find
Grid
components useful because if I need that much flexibility I prefer to just write a one-of grid style for that specific layout. Column
(singular) is a helper component that lets you set the size, if you wish, of a particular column. e.g.<Column width="1/5">
Columns
usesflex
under the hood, as I tried usinggrid
in various ways, but it didn't fit my use case of needing to set the size of children as easily, likeColumn
can.
- The