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.
VerveUI has a fully customizable design system. The design system is divided into two parts: design tokens and component styles.
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:
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.
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.
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.
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.
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.
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.
Name | Size | Pixels | Visual representation |
---|---|---|---|
0 | 0px | 0px | |
px | 1px | 1px | |
0.5 | 0.125rem | 2px | |
1 | 0.25rem | 4px | |
1.5 | 0.375rem | 6px | |
2 | 0.5rem | 8px | |
2.5 | 0.625rem | 10px | |
3 | 0.75rem | 12px | |
3.5 | 0.875rem | 14px | |
4 | 1rem | 16px | |
5 | 1.25rem | 20px | |
6 | 1.5rem | 24px | |
7 | 1.75rem | 28px | |
8 | 2rem | 32px | |
9 | 2.25rem | 36px | |
10 | 2.5rem | 40px | |
11 | 2.75rem | 44px | |
12 | 3rem | 48px | |
14 | 3.5rem | 56px | |
16 | 4rem | 64px | |
20 | 5rem | 80px | |
24 | 6rem | 96px | |
28 | 7rem | 112px | |
32 | 8rem | 128px | |
36 | 9rem | 144px | |
40 | 10rem | 160px | |
44 | 11rem | 176px | |
48 | 12rem | 192px | |
52 | 13rem | 208px | |
56 | 14rem | 224px | |
60 | 15rem | 240px | |
64 | 16rem | 256px | |
72 | 18rem | 288px | |
80 | 20rem | 320px | |
96 | 24rem | 384px |
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.
Name | Size | Pixels | Visual representation |
---|---|---|---|
0 | 0px | 0px | |
1 | 1px | 1px | |
2 | 0.125rem | 2px | |
3 | 0.25rem | 4px | |
4 | 0.375rem | 6px | |
5 | 0.5rem | 8px | |
6 | 0.625rem | 10px | |
7 | 0.75rem | 12px | |
8 | 0.875rem | 14px | |
9 | 1rem | 16px | |
10 | 1.25rem | 20px | |
11 | 1.5rem | 24px | |
12 | 1.75rem | 28px | |
13 | 2rem | 32px | |
14 | 2.25rem | 36px | |
15 | 2.5rem | 40px | |
16 | 2.75rem | 44px | |
17 | 3rem | 48px | |
18 | 3.5rem | 56px | |
19 | 4rem | 64px | |
20 | 5rem | 80px | |
21 | 6rem | 96px | |
22 | 7rem | 112px | |
23 | 8rem | 128px | |
24 | 9rem | 144px | |
25 | 10rem | 160px | |
26 | 11rem | 176px | |
27 | 12rem | 192px | |
28 | 13rem | 208px | |
29 | 14rem | 224px | |
30 | 15rem | 240px | |
31 | 16rem | 256px | |
32 | 18rem | 288px | |
33 | 20rem | 320px | |
34 | 24rem | 384px |
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.
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.
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.
@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.
Name | Pixels (min) | Pixels (max) | Visual representation |
---|---|---|---|
0 | 0px | 0px | |
1 | 1px | 1px | |
2 | 2px | 2px | |
3 | 4px | 4px | |
4 | 6px | 6px | |
5 | 8px | 8px | |
6 | 10px | 10px | |
7 | 12px | 12px | |
8 | 14px | 14px | |
9 | 16px | 16px | |
10 | 20px | 20px | |
11 | 24px | 24px | |
12 | 28px | 28px | |
13 | 32px | 32px | |
14 | 36px | 36px | |
15 | 40px | 48px | |
16 | 48px | 64px | |
17 | 64px | 96px | |
18 | 80px | 144px | |
19 | 128px | 256px | |
20 | 160px | 416px |
Resize your browser window to see how the bars change size starting from step 15 onwards.
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.
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
.
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.
.text-size-6xl { font-size: var(--text-size-6xl); line-height: var(--text-line-height-6xl); letter-spacing: var(--text-tracking-6xl);}
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:
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.