Introduction

Copy and paste-able components built with React Aria and Tailwind CSS, written in TypeScript.

VerveUI has a short learning curve, but you do need to understand the following concepts first.

Design System

VerveUI has a fully customizable design system. The design system is divided into two parts: design tokens and component styles.

Design tokens

Design tokens are design decisions written in CSS variables in the design-system.css file. They provide a source of truth for the design and ensure it is consistent across the entire UI. There are the following design tokens:

  • Space tokens
  • Color tokens
  • Font family tokens
  • Font size tokens
  • Font line-height tokens
  • Font tracking tokens
  • Roundness tokens
  • Shadow tokens
  • Border tokens

On its own, this file does nothing except declare CSS variables. These CSS variables will be used by the new utility classes we extend Tailwind with to inform the component styles.

Extended Tailwind Utility Classes

The variables in design-system.css will be used by the extended-theme.ts file to extend the default Tailwind utility classes. We don't change the default Tailwind classes to make sure developers have no surprises. Instead we create a few new utility classes described after the component styles.

Component styles

The styles for the components are defined in the component-styles.css file. This file holds the component classes (and their variants) and defines the styles for each component.

The design-system.css, extended-theme.ts, and component-styles.css files are the only files you need to edit to customize the design and can be thought of as owned by the designer. Unless you're adding/removing components or variants, a design change = a commit to these files.

Fluid space scale

Tailwind already has a spacing scale. This spacing scale has a few problems, described below. The *-space-{size} utility is the solution to these problems with 21 steps, from 0 to 20.

Spacing is the cement that holds together the bricks of components on a UI. The importance of good spacing is hard to overstate: it's the most important aspect of design. And the difference between an exquisitely elegant design and a graceless one. So it's no wonder designers are rightly taught to pay great attention to whitespace.

However, it remains one of the most unnecessarily unsystematized aspects of web design (and by consequence development). Most designers still space things out by hand: Shift + Left, Shift + Right, Shift + Up, and Shift + Down to move things around by increments of 10px and counting 10, 20, 30 in their heads is the bread and butter of spacing.

As for developers, spacing is handled with hard-coded utility classes, infinitely growing BEM classes, or, God forbid, CSS in style tags. Out of all of these, utility classes is the least worse: if there's a clear spacing scale agreed upon between developer and designer beforehand, it's relatively easy to implement a given design. But if the designer decides to change a spacing rule (from w-4 to w-5, for example) after the implementation is done, you're going to have a hard time figuring out which instances of that w-4 pertain to the spacing the designer wants to change and which should remain the same.

We can do better.

Let's give spacing the well-thought-out treatment it deserves and enable clear communication between designer and developer, preserve the designer's complete control over spacing, even after the implementation is complete, and greatly speed up development in the process.

Modular scale

One popular method for establishing a scale is to use a modular scale, based on a specific ratio. Ratios like 4:5, 2:3, or the "golden ratio" of 1:1.618 are commonly used. Typically, you start with a base size, often 16px (a common default for browsers), and then apply your chosen ratio to calculate the subsequent sizes in your scale.

While the mathematical elegance of this approach is appealing, it has a deal-breaking limitation: limited size options. You could try different ratios and equations, but at that point you’re just trying to pick a scale that happens to match the sizes you already know you want.

Hand-picked scale

It's more pragmatic to hand-pick the values because this approach grants the designer complete control over the number and variety of space sizes.

Let's take a look at TailwindCSS's spacing scale.

NameSizePixelsVisual representation
00px0px
px1px1px
0.50.125rem2px
10.25rem4px
1.50.375rem6px
20.5rem8px
2.50.625rem10px
30.75rem12px
3.50.875rem14px
41rem16px
51.25rem20px
61.5rem24px
71.75rem28px
82rem32px
92.25rem36px
102.5rem40px
112.75rem44px
123rem48px
143.5rem56px
164rem64px
205rem80px
246rem96px
287rem112px
328rem128px
369rem144px
4010rem160px
4411rem176px
4812rem192px
5213rem208px
5614rem224px
6015rem240px
6416rem256px
7218rem288px
8020rem320px
9624rem384px

This is a comprehensive enough scale, however it does have a few problems.

First, there's one major no-go: semantics are tied with implementation. The names of the steps in the scale (1, 1.5, 2, 2.5, etc) are tied to multiples of 4px, which is an implementation detail. In order for the designer to have complete control over the design after the implementation is done, semantics and implementation have to be decoupled. This is absolutely crucial for development speed.

The other awkward thing is that we have half steps (0.5, 1.5, 2.5, etc) on the lower end of the scale because we need multiples of 2px, and we skip steps (64, 72, 80, etc) on the higher end of the scale because we don't need those multiples of 4px. This inconsistency makes for a harder-to-communicate scale and an awkward developer experience where if the designer asks you to step a component's spacing up, you don't go from 64 to 65, but from 64 to 72.

Let's do better.

Instead of having a scale whose steps are tied to multiples of 4px, let's create a scale that just has "steps" going up. Tailwind has 35 steps, from 0 to 34, so let's replicate that.

NameSizePixelsVisual representation
00px0px
11px1px
20.125rem2px
30.25rem4px
40.375rem6px
50.5rem8px
60.625rem10px
70.75rem12px
80.875rem14px
91rem16px
101.25rem20px
111.5rem24px
121.75rem28px
132rem32px
142.25rem36px
152.5rem40px
162.75rem44px
173rem48px
183.5rem56px
194rem64px
205rem80px
216rem96px
227rem112px
238rem128px
249rem144px
2510rem160px
2611rem176px
2712rem192px
2813rem208px
2914rem224px
3015rem240px
3116rem256px
3218rem288px
3320rem320px
3424rem384px

This simple change is deceptively powerful.

First, the designer doesn't have to think about absolute values (4px, 10px, 16px, etc), but relative ones ("this should be more spaced out than that by 2 steps", "that should be more spaced out than this by 4 steps", etc). Eventually the designer has to converge on absolute values, of course, but as long as the designer starts with a somewhat decent scale, the flexibility to think in relative terms is a huge speed booster, specially in the early phases of design.

Also, if the designer gets the relative values right - which is much easier than to get the absolute values right - the implementation will not change: step 30 will still be step 30, whether the designer decides later that that means 240px or 244px. This frees the developer to develop at full speed and, given an initial mockup, in parallel, without having to wait for the designer to get the values right.

Finally, by giving the designer complete control over the absolute values, we eliminate an entire category of back-and-forth work between designer and developer! The only possible mistakes or changes that require coordination between the two are those that involve creating or eliminating steps. But this is a much smaller set of changes.

Breakpoint-based sizing

We still need to design and develop for multiple screen sizes. This is usually done by having designers design mockups for multiple sizes to account for phones, tablets, and desktops. It's not uncommon for a designer to design mockups for 4 or 5 different screen sizes. Then, the developer has to go through the entire codebase and add rules for each breakpoint. Tailwind has 5 breakpoints, so at it's most responsive, there's 5 breakpoints per component. If you have 100 components, that's 500 breakpoints.

Doing all this is a tremendous waste of effort.

Let's do better.

Fluid sizing

The clamp() function , introduced in CSS, offers a fluid approach to sizing. It allows designers to define a minimum and maximum size range, ensuring a smooth scale between these values based on the viewport size.

For example, you can use the clamp(16px, 5vw, 24px) to set a size that is 5% of the viewport width, 16 pixels on small screens, and a maximum of 24 pixels on larger screens. This results in a seamless transition of sizes as the screen size changes, also providing a smoother experience for users.

However, the vw unit isn't ideal: the size can start to grow too soon, too late, too slow, or too fast. What we want is a linear growth rate from the minimum viewport size, 320px say, to the maximum width the website will have, 1536px say. Imagine the size on the y axis and the viewport width on the x axis. Here's how we can use CSS variables and the calc() function to calculate the exact growth rate we want between those two points, using the general equation of a straight line: y = mx + c.

This is how we implement this linear growth rate in VerveUI.

design-tokens.css
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
@layer base {
:root {
--content-min-width: 20; /* in rems = 320px */
--content-max-width: 96; /* in rems = 1536px */
--scale-20-min-size: 10; /* in rems = 160px */
--scale-20-max-size: 26; /* in rems = 416px */
/* space-20 */
--size-deltaX: calc(var(--content-max-width) - var(--content-min-width));
--space-20-deltaY: calc(var(--scale-20-max-size) - var(--scale-20-min-size));
--space-20-gradient: calc(var(--space-20-deltaY) / var(--size-deltaX));
--space-20-intercept: calc(var(--scale-20-min-size) - (var(--space-20-gradient) * var(--content-min-width)));
--space-20-font-size: calc(var(--space-20-gradient) * 100vw + var(--space-20-intercept) * 1rem);
--space-20: clamp(calc(var(--scale-20-min-size) * 1rem), var(--space-20-font-size), calc(var(--scale-20-max-size) * 1rem));
}
}

The design process to implement fluid sizing is rather simple for both designers and developers. Designers only have to draw two mockups: one for small screens and one for large screens. While the developer only needs to tell the browser to interpolate between the two scales, based on the current viewport width.

Additionally, if we're going to use fluid sizing instead of breakpoint-based sizing, we don't need as many steps on the scale, because a space of 384px would always be too overwhelming (it would overflow, in fact), on a 320px wide viewport.

And we need no variation in the lower steps, some variation from step 15 onwards, and more variation in the higher steps because of the two opposing forces between the need to increase the spacing on larger screens to keep the design balanced, and the fact that users don't resize their browser windows to see the same content bigger, but to see more content - and therefore we don't want to just scale up the entire UI (that'd be a terrible UX).

So let's cap the scale at step 20 and add a min and a max space size.

NamePixels (min)Pixels (max)Visual representation
00px0px
11px1px
22px2px
34px4px
46px6px
58px8px
610px10px
712px12px
814px14px
916px16px
1020px20px
1124px24px
1228px28px
1332px32px
1436px36px
1540px48px
1648px64px
1764px96px
1880px144px
19128px256px
20160px416px

Resize your browser window to see how the bars change size starting from step 15 onwards.

Note on inversion of control

The technique we're using here is generally referred to, in software engineering, as "inversion of control". We create an abstraction (the scale) and implement our UI using it. This allows the designer to retain complete control over the specific values each step in the scale maps to.

The *-space-{size} utility

VerveUI comes with a *-space-{size} utility that allows you to use this fluid scale in your UI. For example, w-space-20 will give you a width of 160px on 320px screens, a width of 416px on 1536px screens, and a dinamically calculated width on all other screen sizes.

The *-space-{size} utility works with all the properties that accept a size value, such as padding, margin, width, minWidth, maxWidth, height, minHeight, maxHeight, gap, inset, space, translate, scrollMargin, and scrollPadding.

Fluid typography scale

Just like the fluid space scale and utility, we also have a fluid typography scale and utility. Because we don't need as many values, the scale is based on t-shirt sizes: text-size-6xl, text-size-5xl, text-size-4xl, text-size-3xl, text-size-2xl, text-size-xl, text-size-lg, text-size-md, text-size-sm, and text-size-xs.

Just like the fluid space scale, the text size dynamically adapts to the viewport width, just like the default text-{size} utility, it comes with line-height and, additionally it also comes with tracking.

fluid-typography.css
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
.text-size-6xl {
font-size: var(--text-size-6xl);
line-height: var(--text-line-height-6xl);
letter-spacing: var(--text-tracking-6xl);
}

Note on fluid design & development

Although fluid (as opposed to breakpoint-based) design & development is conceptually simple, it does come with powerful benefits. Once you've experienced not having to think about breakpoints or screen sizes, building components that automatically adapt to the space they have, with minimal and elegant code, and witness the visual harmony and consistency it gives you, you'll never go back.

VerveUI follows these ideals:

  • These components should be able to be used in any framework that supports React.
  • Copy and paste components give you full control over the component code, it is yours to use.
  • Simple and beautiful default design.
  • Design code and decisions are decoupled from development code and decisions: design change = tailwind.css commit.
  • tailwind.css contains a full design system with fluid space sizing and fluid typography sizing and modular scales.

That's it about the design system. I hope you enjoy using VerveUI.