Typography

Typography is arguably the essential part of a website. When we think about the content of a web page, we think about words. In this article, we’ll take a look at how to set a typography system in CSS.
Create your design system - 图1

Setting the Typography system using CSS Variables

Designing a typography system means making decisions about:

  1. The typeface (font-family) you want to use.

  2. Type (modular) scale.

  3. Responsiveness of the text (size unit and breakpoints).

  4. Spacing and vertical rhythm.

  5. Colors (theming).

Let’s start with point one and two. Picking the right typeface is probably one of the first steps when you create a design system. Let’s assume you’ve browsed hundreds of font families and found the one you love (for now!); in your global/_typography.scss file, you can set your font-families as variables.

  1. :root {
  2. --font-primary: sans-serif;
  3. --font-secondary: serif;
  4. /* set base values */
  5. --text-base-size: 1em;
  6. --text-scale-ratio: 1.2;
  7. }
  8. body {
  9. font-size: var(--text-base-size);
  10. font-family: var(--font-primary);
  11. color: var(--color-text);
  12. }

The primary font, in this case, is the “most used” font; or the body font. The secondary font can be applied, for example, to a heading element. This approach is arbitrary, of course.

Along with the typeface, we’ve defined two variables: the — text-base-size and the — text-scale-ratio. The value of the text base size is 1em, while the — text-scale-ratio is used to generate the type scale.

When applied to the (we’re assuming no font-size is applied to the element), 1em equals to 16px in most modern browsers. Since our framework is mobile-first, we’re saying: on small devices, I want the body text size to be ~16px (the font-size of the body is equal to the — text-base-size).

And yes, after a long debate, we’ve opted for the em unit. The reasons behind this choice are explained later, in the Spacing section of the article.

The modular scale is a set of values obtained from a base value (in our case, 1em) and a ratio, or multiplier ( — text-scale-ratio). You can apply the scale to any measurable element (margin, paddings, etc.). Here’s how you create a type scale taking advantage of the CSS calc() function*:

  1. :root {
  2. --font-primary: sans-serif;
  3. --font-secondary: serif;
  4. /* set base values */
  5. --text-base-size: 1em;
  6. --text-scale-ratio: 1.2;
  7. /* type scale */
  8. --text-xs: calc(1em / (var(--text-scale-ratio) * var(--text-scale-ratio)));
  9. --text-sm: calc(1em / var(--text-scale-ratio));
  10. --text-md: calc(1em * var(--text-scale-ratio));
  11. --text-lg: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio));
  12. --text-xl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  13. --text-xxl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  14. --text-xxxl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  15. }
  16. body {
  17. font-size: var(--text-base-size);
  18. font-family: var(--font-primary);
  19. color: var(--color-text);
  20. }
  21. h1, h2, h3, h4, form legend {
  22. color: var(--color-text-heading);
  23. margin-bottom: var(--space-xxs);
  24. line-height: var(--heading-line-height);
  25. }
  26. /* text size */
  27. .text--xxxl {
  28. font-size: var(--text-xxxl);
  29. }
  30. h1, .text--xxl {
  31. font-size: var(--text-xxl);
  32. }
  33. h2, .text--xl {
  34. font-size: var(--text-xl);
  35. }
  36. h3, .text--lg {
  37. font-size: var(--text-lg);
  38. }
  39. h4, .text--md {
  40. font-size: var(--text-md);
  41. }
  42. .text--sm, small {
  43. font-size: var(--text-sm);
  44. }
  45. .text--xs {
  46. font-size: var(--text-xs);
  47. }
  48. p {
  49. line-height: var(--body-line-height);
  50. }

We could have simplified the code by replacing the parts where we repeat — text-scale-ratio with variables (e.g., — text-lg: calc(1em var( — text-md));), but we had issues with how the postcss-css-variables gulp plugin compiles nested calc() function and CSS Variables.

The reason why a modular scale is useful when applied to anything in a design system is that it generates a harmonious set of values, as opposed to setting each value independently (maybe obtaining them from a .sketch file).

Note that in defining each text size variable we multiply 1em by the — text-scale-ratio. That 1em is not the —text-base-size value. You could set a — text-base-size different from 1em (while you shouldn’t change the 1m in the calc() function).

Since the em unit is a relative unit equal to the current font size, if we update the — text-base-size variable at a specific media query, we update the font-size of the body, and, as a result, all the text size variables. The whole typography is affected.

The paragraph element inherits the base font size, while we set a specific font size for each heading element. Besides, we create some utility classes in case, for example, we want to apply the — text-xxl font size to an element that is not an

.

Why including the type scale in your CSS? In one word: control.

Say we want to increase the body font size at a specific media query; for example, we increase the — text-base-size to 1.25em past 1024px. The heading elements are all affected by the change (the 1em in the calc() function is no longer ~16px, but ~20px), therefore they all become bigger. Let’s suppose we feel like increasing the size of the

element even more. How do we do that?

One option would be increasing the — text-base-size value, but it’s not ideal. The body text is big enough, I just want to target the

. Here’s the advantage of storing the — text-scale-ratio in a variable. We can edit it and affect everything but the body text:

  1. :root {
  2. /* set base values */
  3. --text-base-size: 1em;
  4. --text-scale-ratio: 1.2;
  5. /* type scale */
  6. --text-xs: calc(1em / (var(--text-scale-ratio) * var(--text-scale-ratio)));
  7. --text-sm: calc(1em / var(--text-scale-ratio));
  8. --text-md: calc(1em * var(--text-scale-ratio));
  9. --text-lg: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio));
  10. --text-xl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  11. --text-xxl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  12. --text-xxxl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  13. }
  14. @include breakpoint(md) {
  15. :root {
  16. // you can manage typography editing only these 2 variables
  17. --text-base-size: 1.25em;
  18. --text-scale-ratio: 1.25;
  19. }
  20. }

With this technique, you can manage the size of all your text elements by editing only two variables. Not just that, you can take advantage of the em unit and modify all margins, paddings, and spacing in general by editing the — text-base-size at a root level.

This method is not just a time-saver IMO, it’s a powerful approach to making text responsive. Both designers and developers can benefit from it. All we need is setting a demo-typography.html file with some text elements plus some components. The designer can jump in and tweak the typography directly in coding.
Create your design system - 图2
So…what’s the catch? When you use the Developer Tools to inspect the font size, you won’t see numeric values, but variable names; which don’t provide much info. But does it matter? Is it critical to know that a font-size is 28px as opposed to 29.4560px? Your call! 😉

Spacing

Now that sizing is sorted out, we need to deal with spacing. What is the ideal margin-bottom of an

element? What about paragraphs? One option to deal with spacing is setting another scale to manage the vertical rhythm.

This is how it works:

  1. Set a base value, or baseline -> e.g., 24px

  2. Generate a set of values that are multiples of the baseline

  3. Use these values to generate consistent spacing between elements

  1. :root {
  2. --baseline: 24px;
  3. /* spacing values */
  4. --space-sm: calc(var(--baseline)/2);
  5. --space-md: var(--baseline);
  6. --space-lg: calc(var(--baseline)*2);
  7. --space-xl: calc(var(--baseline)*3);
  8. }
  9. h1 {
  10. margin-bottom: var(--space-lg);
  11. }
  12. h2 {
  13. margin-bottom: var(--space-md);
  14. }
  15. p {
  16. margin-bottom: var(--space-sm);
  17. }

The advantage of this method is that it creates a harmonious vertical spacing, that makes the user feels “safe”. Read more about why vertical rhythm is a good practice in this great article by Zell Liew.

That said, we opted for something different! Since we use the em unit for the font-size, we should set spacing in px or rem values if we want to preserve vertical rhythm (otherwise spacing would be affected by the em unit of the font size). As a result, if we change the font-size of the

, we may want to tweak the margin-bottom as well (e.g., set a media query and increase the margin-bottom to — space-xl); otherwise the margin could look too small now that the

is bigger than before.

Even though setting a media query to tweak the margin is not a big deal, we decided to rely entirely on the elasticity of em units. If you set your margins in em, chances are you won’t need to edit them.

Just my 2 cents: we often adopt in web design concepts originated in print design (e.g., vertical rhythm). We can break these rules, for the sake of simplicity, if doing so does not affect the user experience.

Here’s an example of how to set a spacing system using em units:

  1. :root {
  2. /* spacing values */
  3. --space-xxxs: 0.25em;
  4. --space-xxs: 0.375em;
  5. --space-xs: 0.5em;
  6. --space-sm: 0.75em;
  7. --space-md: 1.25em;
  8. --space-lg: 2em;
  9. --space-xl: 3.25em;
  10. --space-xxl: 5.25em;
  11. --space-xxxl: 8.5em;
  12. }
  13. h1, h2, h3, h4, form legend {
  14. margin-bottom: var(--space-xxs);
  15. line-height: var(--heading-line-height);
  16. }
  17. .text-container {
  18. h2, h3, h4 {
  19. margin-top: var(--space-sm);
  20. }
  21. ul, ol, p {
  22. margin-bottom: var(--space-md);
  23. }
  24. }

I generally separate spacing (_spacing.scss) from typography (_typography.scss), but this is up to you.

The reason why margins of lists and paragraphs descend from the .text-container class is to separate blocks of text from all the other places where a paragraph or an unordered list can be used. Optionally, you can target the

element.

Editing text size and spacing on a component level

The system we’re exploring makes sense as long as all text elements change accordingly with the two main text variables ( — text-base-size and — text-scale-ratio). How do we edit text size on a component level?

Option 1 would be targeting the component:

  1. .component {
  2. font-size: 1.2em;
  3. }

This change will affect the whole component, in all media queries. It’s like saying: “I want all text elements of this component to be 120% of what they would normally be”.

If you want to target a specific element inside the component, you have the following options:

  1. .component h1 {
  2. font-size: 3em; // the size is no longer affected by the --text-scale-ratio
  3. font-size: calc(var(--text-base-size) * 3); // use base size to create new value - off scale
  4. font-size: calc(var(--text-xxl) - 0.8em); // use original font size to create new value - off scale
  5. font-size: var(--text-xl); // update the value of the font size using a different variable
  6. }

The first option allows you to set the size of the

as multiple of the — text-base-size (in this example x3); probably not ideal, because the size of the

is no longer affected by the — text-scale-ratio variable. The second option sets a new size based on the — text-base-size. Not a fan of this option either, because it generates a value that is most probably off the scale, and it’s not affected by the — text-scale-ratio.

Of the other two options, the last one is my favorite (reassigning the variable), because the new value obtained still belongs to the type scale.

Colors

Theming is part of a more complicated topic worth exploring in a different article. To complete our typography exploration though, let’s see how to set the primary colors*:

  1. :root {
  2. /* typography */
  3. --color-text: var(--gray-10);
  4. --color-text-heading: var(--black);
  5. --color-text-subtle: var(--gray-6);
  6. --color-link: var(--color-primary);
  7. --color-link-visited: var(--color-primary-dark);
  8. }
  9. body {
  10. color: var(--color-text);
  11. }
  12. h1, h2, h3, h4, form legend {
  13. color: var(--color-text-heading);
  14. }
  15. a {
  16. color: var(--color-link);
  17. &:visited {
  18. color: var(--color-link-visited);
  19. }
  20. }

I generally prefer to store all the color variables in a separate file (_colors.scss).

The style above can be used to set some basic rules. Chances are we need to create variations of these colors. Soon, we’ll be able to do so by updating the variable values. Unfortunately, updating a CSS Variable using a class is not supported in all modern browser (and gulp plugins). For the time being, we need to change the variable entirely!

Putting all together

Let’s put all the pieces together! This is how we’ve set typography so far in our framework. It’s not necessarily the final version. We will review everything before publishing the library on Github. Oh and any feedback is welcome! Let us hear what you think and what could be done better.

  1. /* --------------------------------
  2. Typography
  3. -------------------------------- */
  4. :root {
  5. --font-primary: sans-serif;
  6. --font-secondary: serif;
  7. /* set base values */
  8. --text-base-size: 1em;
  9. --text-scale-ratio: 1.2;
  10. /* type scale */
  11. --text-xs: calc(1em / (var(--text-scale-ratio) * var(--text-scale-ratio)));
  12. --text-sm: calc(1em / var(--text-scale-ratio));
  13. --text-md: calc(1em * var(--text-scale-ratio));
  14. --text-lg: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio));
  15. --text-xl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  16. --text-xxl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  17. --text-xxxl: calc(1em * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio) * var(--text-scale-ratio));
  18. /* line-height */
  19. --heading-line-height: 1.2;
  20. --body-line-height: 1.4;
  21. }
  22. @include breakpoint(md) {
  23. :root {
  24. --text-base-size: 1.25em;
  25. --text-scale-ratio: 1.25;
  26. }
  27. }
  28. body {
  29. font-size: var(--text-base-size);
  30. font-family: var(--font-primary);
  31. color: var(--color-text);
  32. }
  33. h1, h2, h3, h4, form legend {
  34. color: var(--color-text-heading);
  35. margin-bottom: var(--space-xxs);
  36. line-height: var(--heading-line-height);
  37. }
  38. /* text size */
  39. .text--xxxl {
  40. font-size: var(--text-xxxl);
  41. }
  42. h1, .text--xxl {
  43. font-size: var(--text-xxl);
  44. }
  45. h2, .text--xl {
  46. font-size: var(--text-xl);
  47. }
  48. h3, .text--lg {
  49. font-size: var(--text-lg);
  50. }
  51. h4, .text--md {
  52. font-size: var(--text-md);
  53. }
  54. .text--sm, small {
  55. font-size: var(--text-sm);
  56. }
  57. .text--xs {
  58. font-size: var(--text-xs);
  59. }
  60. p {
  61. line-height: var(--body-line-height);
  62. }
  63. a {
  64. color: var(--color-link);
  65. &:visited {
  66. color: var(--color-link-visited);
  67. }
  68. }
  69. b, strong {
  70. font-weight: bold;
  71. }
  72. .text-container {
  73. h2, h3, h4 {
  74. margin-top: var(--space-sm);
  75. }
  76. ul, ol, p {
  77. margin-bottom: var(--space-md);
  78. }
  79. ul, ol {
  80. list-style-position: outside;
  81. padding-left: 24px;
  82. }
  83. ul {
  84. list-style-type: disc;
  85. }
  86. ol {
  87. list-style-type: decimal;
  88. }
  89. ul li, ol li {
  90. line-height: var(--body-line-height);
  91. }
  92. em {
  93. font-style: italic;
  94. }
  95. u {
  96. text-decoration: underline;
  97. }
  98. }
  99. /* utility classes */
  100. .truncate {
  101. // truncate text if it exceeds parent
  102. overflow: hidden;
  103. text-overflow: ellipsis;
  104. white-space: nowrap;
  105. }

I hope you enjoyed the article! For more web design nuggets

Grid & Layout

Choosing how to handle layouts and content positioning is probably one of the first decision you’re going to make when building a design system.
Create your design system - 图3
Defining the grid means creating the system to structure your content, whether it’s a single component or a page layout.

1.  Layout with an auto-generated number of columns

Let’s say you have a gallery of products you want to lay out in a grid: all your items need to have the same width (they are instances of the same ‘product’ component, so they have the same dimension) and have a minimum width so that the design does not break; assume you want, at different screen sizes, the maximum number of items per row; something like the CodyHouse library(resize the page to see the change in number of items).

It would be ideal to find a way to automatically determine this maximum number of items without having to add new CSS code at different media queries.

This can be done using CSS Grid.

Let’s start by creating a grid mixin (we’ll be reusing it also for the layouts 3 and 4).

  1. $gap-horizontal: 10px;
  2. $gap-vertical: $gap-horizontal;
  3. @mixin grid {
  4. @supports(grid-area: auto) {
  5. display: grid;
  6. grid-column-gap: $gap-horizontal;
  7. grid-row-gap: $gap-vertical;
  8. }
  9. }

This mixin is used to initialize the grid container (with display grid) and set the grid gap (which is the free space between two adjacent items).

We have included the CSS Grid code inside a @supports rule to target browsers that support the Grid (leaving out also the browsers that support the old CSS Grid specification, like IE 11).

Now we can define the mixin that will create our gallery; the min-width of the items will be the only argument of this mixin:

  1. @mixin gridAuto($min-width) {
  2. @supports(grid-area: auto) {
  3. grid-template-columns: repeat(auto-fit, minmax($min-width, 1fr));
  4. }
  5. }

The minmax function allows us to set a min-width for our elements, while the repeat() function takes care of actually creating the grid.

We can now use these mixins like that:

  1. .gallery-grid {
  2. @include grid;
  3. @include gridAuto(260px);
  4. }

And here’s an example of these mixins in action: link

The mixins we defined above won’t work in IE and older versions of Edge (<= 15). Your gallery would still be accessible, but your items would show up one per row (100% width).

If you need to provide a better fallback, then you can use the float property to recreate your gallery, but you won’t be able to change the number of items per row at different screen sizes: you’ll need to set a fixed number of items per row (this number will be passed as the second argument to the gridAuto() mixin).

Here’s what the grid mixin becomes with the fallback addition:

  1. $gap-horizontal: 10px;
  2. $gap-vertical: $gap-horizontal;
  3. @mixin grid {
  4. // fallback for IE
  5. &::before, &::after {
  6. content: '';
  7. display: table;
  8. }
  9. &::after {
  10. clear: both;
  11. }
  12. > * {
  13. float: left;
  14. margin-right: $gap-horizontal;
  15. margin-bottom: $gap-vertical;
  16. }
  17. @supports(grid-area: auto) {
  18. //CSS Grid style
  19. display: grid;
  20. grid-column-gap: $gap-horizontal;
  21. grid-row-gap: $gap-vertical;
  22. &::after, &::before {
  23. content: none;
  24. }
  25. > * {
  26. margin-right: 0;
  27. margin-bottom: 0;
  28. }
  29. }
  30. }

The CSS properties defined inside the @supports rule are the ones applied when the browser supports CSS Grid (the new specification); while the properties defined outside the @supports are applied in all browsers (and this is why, inside the @supports rule, we had to add some additional style to overwrite those properties).

The gridAuto() mixin becomes:

  1. @mixin gridAuto($min-width, $fallback: 3) {
  2. > * {
  3. float: left;
  4. width: calc(100%/#{$fallback} - #{$gap-horizontal});
  5. &:nth-child(#{$fallback}n + 1) {
  6. clear: both;
  7. }
  8. }
  9. @supports(grid-area: auto) {
  10. grid-template-columns: repeat(auto-fit, minmax($min-width, 1fr));
  11. > * {
  12. width: auto;
  13. margin: 0;
  14. }
  15. }
  16. }

The mixin now accepts two arguments: the first one is the minimum width of the items in the gallery (the same as before — it will be used only if the browsers support CSS Grid), the second (which is optional — default value is 3) is the number of items per row on browsers that do not support CSS Grid (where the fallback is applied).

2. One-dimensional layout

This is probably the easiest layout we can create: we have items we want to arrange in columns, with the option of customizing their width and still being able to distribute the space among them equally.

There are few techniques we can use to implement this kind of layout. We are going to use Flexbox in combo with utility classes to customize the items width.

This approach has been around for a long time. It’s handy, but if you don’t want to use utility classes in your HTML (something like col — 1, col — 5, …), then a different method is the one I describe in section 3, where we create the same layout using CSS Grid.

Before defining our grid classes, let’s set a variable that we will use as grid gap:

  1. :root {
  2. --grid-gap: 10px;
  3. }

Let’s define a class for our grid container:

  1. .flex-grid {
  2. display: flex;
  3. flex-wrap: wrap;
  4. margin: calc(var(--grid-gap) * -1) 0 0 calc(var(--grid-gap) * -1);
  5. }

We initialize our flex container (using display flex) and allow the children to wrap on multiple lines if needed (using the flex-wrap property).
The negative margins are added to balance the padding we use to create the grid gap (see .col class definition below) so that no empty space is left between the .flex-grid element and its container.

Our grid items will have a .col class:

  1. .col {
  2. padding: calc(var(--grid-gap)) 0 0 calc(var(--grid-gap));
  3. background-clip: content-box;
  4. flex-basis: 100%;
  5. }

We use the padding to create the gap between elements and the background-clip so that the background-color/image of the .col element is not applied to the padding (leaving the gap visible).
Create your design system - 图4
You don’t need to use the background-clip property if you are not planning on adding a background-color/image to your .col element (or if the .col element has a child you can apply this background style to).

By default, all .col items have a 100% width (flex-basis 100%). We can use classes to change that width value:

  1. $grid-columns: 12;
  2. @for $i from 1 through $grid-columns {
  3. .col--#{$i} {
  4. flex-basis: round-width($i);
  5. max-width: round-width($i);
  6. }
  7. }

The round-width function is used to round the columns width to a number with 2 decimal places (this prevents the layout from breaking in IE).

  1. @function round-width ($i) {
  2. $width : floor(100 * $i * 100/ $grid-columns) / 100;
  3. @return $width#{"%"};
  4. }

This creates the classes .col — 1 up to .col — 12 (you can change the value of the $grid-columns variable if you are not using a 12 unit-grid).

If you want to create a layout with two elements, the first taking up 9 of the 12 available columns and the latter the remaining 3, you can use something like:

  1. <ul class="flex-grid">
  2. <li class="col col--9">1</li>
  3. <li class="col col--3">2</li>
  4. </ul>

You can also define different classes for different media queries if you want to change the width of your elements at different screen sizes. Something like:

  1. @media only screen and (min-width: 600px) {
  2. @for $i from 1 through $grid-columns {
  3. .col--md-#{$i} {
  4. flex-basis: round-width($i);
  5. max-width: round-width($i);
  6. }
  7. }
  8. }

Here’s an example of the flex classes in action: link

We’ve decided to include this grid system in our library, because of its ease of use. It’s optional though, you can use the method described next (which does not rely on utility classes) if you prefer.

3.  Two-dimensional layout

In layout 2, we considered the case where we needed to control the width of the elements in our row. We didn’t consider the height of the elements at all.

If we want to create a two-dimensional layout where we can control the height of our elements as well, then CSS Grid is probably the best solution.

Here’s an example of a layout you can create using this technique:
Create your design system - 图5
We’ll be reusing the grid mixin (defined in section 1) and we’ll be adding a second gridLayout() mixin.
This new mixin will accept, as argument, a list of pairs of numbers:

  1. .grid {
  2. @include gridLayout(
  3. (7, 2), //item 1
  4. (5, 1), //item 2
  5. (5, 1), //item 3
  6. (12, 1) //item 4
  7. );
  8. }

For each element in your layout, you’ll have to pass a pair of numbers (in the example above, I’m passing 4 pairs which means our layout is composed of 4 elements). For each couple of numbers, the first will be the number of columns the element has to occupy, the second the number of rows.

In the code above, we are saying: the first element in the layout has to take up 7 columns and 2 rows; the second element 5 columns and 1 row; the same for the third element; the last one 12 columns (100% width) and 1 row.

Let’s see what the mixin looks like:

  1. @mixin gridLayout($cols...) {
  2. @supports(grid-area: auto) {
  3. grid-template-columns: repeat($grid-columns, 1fr);
  4. $i: 1;
  5. @each $col in $cols {
  6. > :nth-of-type(#{$i}) {
  7. grid-column-end: span nth($col,1);
  8. @if length($col) > 1 {
  9. grid-row-end: span nth($col,2);
  10. }
  11. }
  12. $i: $i + 1;
  13. }
  14. }
  15. }

First, we use grid-template-columns to define our grid of columns; this will create a template of 12 columns, all having the same width.

Notice we haven’t defined the grid-template-rows (or grid-auto-rows). This is mostly because the row height depends on the type of content you want to show: you can decide to have rows of a fixed height, rows that are a percentage of the viewport or just leave your content determine the height. You can specify this in your class when calling the mixin.

The ‘each’ loop is where the items are allocated: for each one of the pairs passed to the mixin, we take the corresponding element (using the :nth-of-type selector) and place it in our grid (using the span keyword).

Here’s an example of the mixin in action: Link

Note: the second number in each pair is not required (in the mixin, we check if the second value is passed before setting the grid-row-end property).

Passing just one number per item will allow you to create a one-dimensional layout; this is an alternative to the method described in section 2 (Flexbox + utility classes).

We can modify the gridLayout() mixin to add a fallback for browsers that do not support CSS Grid. Just keep in mind that, with the fallback, you won’t be able to control the height of your items.

  1. @mixin gridLayout($cols...) {
  2. $i: 1;
  3. @each $col in $cols {
  4. > :nth-of-type(#{$i}) {
  5. width: calc( #{round-width(nth($col,1))} - #{$gap-horizontal});
  6. }
  7. $i: $i + 1;
  8. }
  9. @supports(grid-area: auto) {
  10. grid-template-columns: repeat($grid-columns, 1fr);
  11. $i: 1;
  12. @each $col in $cols {
  13. > :nth-of-type(#{$i}) {
  14. grid-column-end: span nth($col,1);
  15. @if length($col) > 1 {
  16. grid-row-end: span nth($col,2);
  17. }
  18. }
  19. $i: $i + 1;
  20. }
  21. > :nth-child(n) {
  22. width: auto;
  23. margin: 0;
  24. }
  25. }
  26. }

4.  Two-dimensional layout with overlapping elements

This is quite a specific layout case: let’s say you want to create a two-dimensional layout (as we did with case 3 where you can set both width and height of your items) but you want to control the start/end position of your elements as well (so that they can overlap).

With the gridLayout() mixin, the items are automatically placed in the grid with no overlapping. You won’t be able to create something like this:
Create your design system - 图6
We can create a new gridAdvanced() mixin which will bring our layout a step further; here’s how we’re going to use it:

  1. .grid {
  2. @include gridAdvanced(
  3. (1, 8, 1, 3), //item 1
  4. (1, 8, 3, 5), //item 2
  5. (5, -1, 2, 4) //item 3
  6. );
  7. }

This time, we’ll need to pass, for each element in our layout, four numbers: the first two are the item’s start and end position within the grid columns, while the last two the row start and end position.

In the example above, we have 3 elements: the first one starts at column 1 and ends at column 8 (which means, it takes up 7 columns in our layout -> remember column 8 marks the end of the element, so it is not included) and starts at row 1 and ends at row 3 (2 rows); the second one takes the same columns but it starts at row 3 and ends at row 5 (2 rows); the third one starts at column 5 and takes up all the remaining columns (-1 means go till column 12 but include it as well) and starts at row 2 and ends at row 4 (2 rows).

Here’s our mixin:

  1. @mixin gridAdvanced($cols...) {
  2. @supports(grid-area: auto) {
  3. grid-template-columns: repeat($grid-columns, 1fr);
  4. $i: 1;
  5. @each $col in $cols {
  6. > :nth-of-type(#{$i}) {
  7. grid-column-start: nth($col,1);
  8. grid-column-end: nth($col,2);
  9. grid-row-start: nth($col,3);
  10. grid-row-end: nth($col,4);
  11. }
  12. $i: $i + 1;
  13. }
  14. }
  15. }

This is quite similar to the gridLayout() one; this time, though, we are using the grid-column(/row)-start/end to specify the position of our elements.

Here’s an example of the mixin in action: Link

We can provide a fallback for this mixin as well; keep in mind that you won’t be able to create the overlapping effect, as well as customize the items’ height.

  1. @mixin gridAdvanced($cols...) {
  2. $i: 1;
  3. $span: 0;
  4. @each $col in $cols {
  5. > :nth-of-type(#{$i}) {
  6. @if nth($col,2) == -1 {
  7. $span: $grid-columns - nth($col,2) - nth($col,1);
  8. } @else {
  9. $span: nth($col,2) - nth($col,1);
  10. }
  11. width: calc( #{$span}*((100% - #{$gap-horizontal}*#{$grid-columns})/#{$grid-columns}) + (#{nth($col,1)} - 1)*#{$gap-horizontal} - 1px);
  12. min-width: 0px;
  13. }
  14. $i: $i + 1;
  15. }
  16. @supports(grid-area: auto) {
  17. grid-template-columns: repeat($grid-columns, 1fr);
  18. $i: 1;
  19. @each $col in $cols {
  20. > :nth-of-type(#{$i}) {
  21. grid-column-start: nth($col,1);
  22. grid-column-end: nth($col,2);
  23. grid-row-start: nth($col,3);
  24. grid-row-end: nth($col,4);
  25. }
  26. $i: $i + 1;
  27. }
  28. > :nth-child(n) {
  29. width: auto;
  30. margin: 0;
  31. }
  32. }
  33. }

That is all I have to share on how we’re setting the grid system for the CodyHouse library. As usual, we’re open to suggestions! Any feedback is welcome. 🙂

Colors

In this article, we’ll take a look at how to set a color system in CSS, and what are the best practices to make sure the system is easy to use and maintain.
Create your design system - 图7

Color Variables 101

Unlike other CSS globals, creating a color system is 10% about coding and 90% about semantics. While working on your _colors.scss file, you want to keep in mind the following goals:

  1. The color variables have to be easy to remember → You don’t want to check the global file anytime you have to pick a color.

  2. The system has to be easy to update → You will add, remove, and rename colors. Make sure doing so is not complicated.

  3. The system should include only the essential colors → we’ve heard this one so many times…yet we always end up with more colors than we need! The real success key of a design system is removing all that is not necessary (colors included).

Semantic vs Declarative colors

When it comes to setting the color variables, there are two main approaches: semantic and declarative colors.

The semantic approach looks like:

  1. :root {
  2. --color-primary: #4D84F4;
  3. --color-secondary: #f5414f;
  4. --color-text: #2e2e31;
  5. --color-border: #d1d0d2;
  6. --color-success: #88c459;
  7. --color-error: #f5414f;
  8. --color-warning: #ffd137;
  9. }

While here’s an example of a declarative approach:

  1. :root {
  2. --blue: #4D84F4;
  3. --red-brand: #f5414f;
  4. --black: #2e2e31;
  5. --gray: #d1d0d2;
  6. --green: #88c459;
  7. --red: #f94747;
  8. --yellow: #ffd137;
  9. }

Neither of them is wrong. Picking the one that meets your needs depends on so many factors (e.g., project size, branding colors relevance, etc.).

While working on the _colors.scss file of our framework, I had to take into account users were going to edit it (100%). That means that even if the declarative approach was the easiest to use, I had to mix it with the semantic approach to get a system that was also easy to maintain.

The essential color palette

Step number one was declaring the minimum number of colors needed to create the web components. In general, the essential color palette is composed of:

  1. The main/primary color → used for the links, the button background color, etc. In general, it’s the main call-to-action color.

  2. The accent color → used to highlight something important on the page. It shouldn’t be a variation of the primary color, but a complementary color.

  3. A scale of neutral colors → It’s generally a scale of grayscale tones, to be used for text elements, subtle elements, borders, etc.

  4. Feedback colors → success, error, warning.

Some of these colors need a variation (darker/lighter version), often used to highlight interactivity (e.g., :hover/:active states).

In CSS, this translates to:

  1. :root {
  2. /* main colors */
  3. --color-primary: #4D84F4;
  4. --color-primary-light: color-mod(var(--color-primary) tint(15%));
  5. --color-primary-dark: color-mod(var(--color-primary) shade(15%));
  6. --color-primary-bg: color-mod(var(--color-primary) alpha(20%));
  7. --color-accent: #f5414f;
  8. --color-accent-light: color-mod(var(--color-accent) tint(15%));
  9. --color-accent-dark: color-mod(var(--color-accent) shade(10%));
  10. --color-accent-bg: color-mod(var(--color-accent) alpha(20%));
  11. // shades - generated using chroma.js - 12 steps
  12. --black: #1d1d21;
  13. --gray-10: #2e2e31;
  14. --gray-6: #7b7a7d;
  15. --gray-4: #a5a5a6;
  16. --gray-3: #bbbbbc;
  17. --gray-2: #d1d0d2;
  18. --gray-1: #e8e7e8;
  19. --white: white;
  20. /* feedback */
  21. --color-success: #88c459;
  22. --color-error: #f5414f;
  23. --color-warning: #ffd137;
  24. }

*note: we’re using the postcss-color-mod-function plugin to translate the color functions into RGBA code compatible with all browsers.

The snippet above represents the color palette: all the colors used across the project.

The variations of the primary and accent colors are generated using color functions. This approach comes in handy if you have a demo.html file (and we do in our framework) so that you can tweak the values of the functions until you’re satisfied with the colors obtained. The shades (or neutral) colors are generated using chroma.js. In this case, using the functions was not ideal, because you generally have two opposite colors (black and white), and you need to generate a scale of values based on these two colors.

Adding semantic colors to the mix

Once the color palette is ready, we can add semantic colors. Creating semantic colors does not mean incrementing the number of colors, but distributing the colors using semantic references.

  1. :root {
  2. /* main colors */
  3. --color-primary: #4D84F4;
  4. --color-primary-light: color-mod(var(--color-primary) tint(15%));
  5. --color-primary-dark: color-mod(var(--color-primary) shade(15%));
  6. --color-primary-bg: color-mod(var(--color-primary) alpha(20%));
  7. --color-accent: #f5414f;
  8. --color-accent-light: color-mod(var(--color-accent) tint(15%));
  9. --color-accent-dark: color-mod(var(--color-accent) shade(10%));
  10. --color-accent-bg: color-mod(var(--color-accent) alpha(20%));
  11. // shades - generated using chroma.js - 12 steps
  12. --black: #1d1d21;
  13. --gray-10: #2e2e31;
  14. --gray-6: #7b7a7d;
  15. --gray-4: #a5a5a6;
  16. --gray-3: #bbbbbc;
  17. --gray-2: #d1d0d2;
  18. --gray-1: #e8e7e8;
  19. --white: white;
  20. /* feedback */
  21. --color-success: #88c459;
  22. --color-error: #f5414f;
  23. --color-warning: #ffd137;
  24. /* typography */
  25. --color-text: var(--gray-10);
  26. --color-text-heading: var(--black);
  27. --color-text-subtle: var(--gray-6);
  28. --color-link: var(--color-primary);
  29. --color-link-visited: var(--color-primary-dark);
  30. --color-mark: var(--color-accent-bg);
  31. --color-blockquote-border: var(--gray-2);
  32. /* border */
  33. --color-border: var(--gray-2);
  34. /* body */
  35. --color-body: var(--white);
  36. /* forms */
  37. --form-element-border: var(--color-border);
  38. --form-element-border-focus: var(--color-primary);
  39. --form-element-border-error: var(--color-error);
  40. --form-element-bg: var(--white);
  41. --form-text-placeholder: var(--gray-4);
  42. /* buttons */
  43. --btn-primary-bg: var(--color-primary);
  44. --btn-primary-hover: var(--color-primary-light);
  45. --btn-primary-active: var(--color-primary-dark);
  46. --btn-primary-label: var(--white);
  47. /* icons */
  48. --color-icon-primary: var(--gray-4);
  49. --color-icon-secondary: inherit;
  50. }

Why I think this is a good approach

First of all, this system relies on two essential colors: primary and accent colors. That means that when you need to use the color variables, it won’t be difficult for you to remember what those variables represent (even if you are not using declarative names such as “blue” and “red”).

Your system may need to include more colors (e.g., a secondary color). You’re still dealing with just three colors. Managing a system based on 10+ main colors would be difficult regardless of the approach you’re using, so you may want to consider simplifying it.

The grayscale colors use a different naming convention: the higher is the number at the end of the variable, the darker is the color.
This approach becomes handy when you’re not sure which neutral color you want to apply. If gray-2 looks too subtle, you can try gray-3. You may have noticed some shades are missing (e.g., gray-5). They were not essential in our case (we never used them while creating the web components), so we removed them from the color palette.
Create your design system - 图8

Semantic colors are added to the mix for three main reasons:

  1. The _colors.scss file becomes the source of truth anytime you need to modify a color. Do you feel the text heading elements should be darker? Open the _colors.scss file and edit the color-text-heading variable.

  2. If you define a color-border, for example, then you won’t need to look up which gray color you’ve been using in other components the next time you create a border element. The same concept applies to many elements, not just borders.

  3. It makes it a piece of cake to create and maintain different themes.

Theming

As soon as we can use CSS variables without having to rely on plugins or polyfill, creating color themes will be super simple*! Does that mean we can’t create themes today? No, we can. We have two options.

*in our framework, we use the postcss-css-variables plugin to compile CSS variables. It currently does not support updating variables in a CSS class.

Option 1 is updating CSS variables anyway. Browsers that don’t support variables will show the “default” color theme. This is not an issue, as long as the content is accessible.

For example, you have a default color theme → white background and black text color, and a .theme-dark → black background and white text color. Then you create two components, one with the default theme, the other with the .dark-theme. If having both components with the default theme does not affect the user experience, then you can consider the .dark-theme as an enhancement (optional). In this case, it makes sense to update the variables to create different themes even if they’re not supported everywhere.

This is how you create a new theme updating some key CSS variables:

  1. @supports(--css: variables) {
  2. .theme--dark {
  3. --component-background: var(--black);
  4. --color-border: var(--gray-6);
  5. --color-text: var(--gray-4);
  6. --color-text-heading: var(--white);
  7. }
  8. }

I love this solution because it abstracts the color correction, and it allows you to keep your color themes in a single file. By doing so, we can potentially change the state of each component (from theme-a to theme-b) simply by applying a CSS class.

Option 2 would be targeting all elements whose appearance is affected by the theme. The advantage of this method is that it’s supported by all browsers. However, it’s not as easy to maintain compared to the one based entirely on CSS variables.

Here’s an example of option 2 in action:

  1. .theme--dark {
  2. color: var(--gray-1);
  3. h1, h2, h3, h4 {
  4. color: var(--white);
  5. }
  6. .text--subtle {
  7. color: var(--gray-4);
  8. }
  9. a {
  10. color: var(--color-accent);
  11. }
  12. }

Now you know how we’re planning to handle colors in our framework!

Spacing

In this section we’ll take a look at how to set a spacing system in CSS, and how to take advantage of relative units to handle responsiveness.

Create your design system - 图9

Setting the Spacing system using CSS Variables

Step one of setting a spacing system is creating a scale of (spacing) values. To create a scale of non-linear values you need 1) a unit or base value and 2) a multiplier. We’ve already talked about creating a modular scale in our article on web typography.

A basic approach to spacing would look like this:

  1. :root {
  2. --space-xxs: 4px;
  3. --space-xs: 8px;
  4. --space-sm: 12px;
  5. --space-md: 20px;
  6. --space-lg: 32px;
  7. --space-xl: 52px;
  8. --space-xxl: 84px;
  9. }

The snippet above describes a spacing system based on the Fibonacci sequence (each value is found by adding up the two numbers before it).

Note: The first time I read about applying the Fibonacci sequence to a scale of spacing values was in this excellent article by The Scenery.

The downside of a basic approach is that, well…it’s basic. If we want to change a single spacing value, we have to do the math and update all other values manually.

Let’s try to bring some control over the system. We can introduce a variable to define the unit number, and use the calc() function to obtain the spacing values.

  1. :root {
  2. --space-unit: 16px;
  3. --space-xxs: calc(0.25 * var(--space-unit)); // 4px
  4. --space-xs: calc(0.5 * var(--space-unit)); // 8px
  5. --space-sm: calc(0.75 * var(--space-unit)); // 12px
  6. --space-md: calc(1.25 * var(--space-unit)); // 20px
  7. --space-lg: calc(2 * var(--space-unit)); // 32px
  8. --space-xl: calc(3.25 * var(--space-unit)); // 52px
  9. --space-xxl: calc(5.25 * var(--space-unit)); // 84px
  10. }

We’re getting closer! In the example above, we’ve defined each spacing value by multiplying the unit number by a multiplier. Note that the Fibonacci sequence is still applied to the multipliers. Now if we want to update the whole system, we can edit the unit value.

Next, let’s get rid of the absolute unit and replace it with the em unit. To do so, we can replace 16px with 1em:

  1. :root {
  2. --space-unit: 1em;
  3. --space-xxs: calc(0.25 * var(--space-unit));
  4. --space-xs: calc(0.5 * var(--space-unit));
  5. --space-sm: calc(0.75 * var(--space-unit));
  6. --space-md: calc(1.25 * var(--space-unit));
  7. --space-lg: calc(2 * var(--space-unit));
  8. --space-xl: calc(3.25 * var(--space-unit));
  9. --space-xxl: calc(5.25 * var(--space-unit));
  10. }

What looks like a small tweak, actually changes a lot! The em unit is a relative unit equal to the current font size. In most browsers, the default font-size (before CSS styling is applied) is 16 pixels. Therefore we can assume 1em = 16px. However, if you edit the font-size of an element, 1em is no longer 16px (for that element), but it’s equal to the new font-size. What appears like a lack of control, is a powerful responsiveness shortcut. Let me show you why.

If you’ve been following this series on design systems, you know we‘ve created a typography system where all font sizes are intertwined and obtained by multiplying a text-base-size variable (equal to 1em) by a ratio. That means that the text-base-size variable is the controller of the whole type system. If you increase the text-base-size at a specific media query, all the text size variables change accordingly.

  1. @include breakpoint(md) {
  2. :root {
  3. --text-base-size: 1.25em;
  4. }
  5. }

By updating just one variable, you get this:
Create your design system - 图10
Not just that! Since the spacing unit is equal to 1em, and all other spacing values are multipliers of the unit value, when we update the text-base-size variable, we affect the spacing as well. 💪

Look how this method affects typography and spacing at the same time:
Create your design system - 图11
We’re still updating a single variable (text-base-size). No additional media queries needed so far! All you have to do to take advantage of this powerful approach to responsiveness is using the spacing variables to set paddings and margins on a component level:

  1. .header__top {
  2. background: var(--black);
  3. padding: var(--space-sm);
  4. text-align: center;
  5. a {
  6. color: var(--white);
  7. @include fontSmooth;
  8. }
  9. }
  10. .header__main {
  11. border-bottom: 1px solid var(--color-border);
  12. padding-top: var(--space-sm);
  13. padding-bottom: var(--space-sm);
  14. background: var(--white);
  15. }
  16. .header__nav {
  17. ul {
  18. display: flex;
  19. }
  20. li {
  21. margin-right: var(--space-md);
  22. &:last-child {
  23. margin-right: 0;
  24. }
  25. }
  26. }

What if you want to update all spacing values at once, without having to change the text-base-size variable? Just update the space-unit variable:

  1. :root {
  2. --space-unit: 1em;
  3. --space-xxs: calc(0.25 * var(--space-unit));
  4. --space-xs: calc(0.5 * var(--space-unit));
  5. --space-sm: calc(0.75 * var(--space-unit));
  6. --space-md: calc(1.25 * var(--space-unit));
  7. --space-lg: calc(2 * var(--space-unit));
  8. --space-xl: calc(3.25 * var(--space-unit));
  9. --space-xxl: calc(5.25 * var(--space-unit));
  10. }
  11. @include breakpoint(md) {
  12. :root {
  13. --space-unit: 1.25em;
  14. }
  15. }

It is true that by embracing a method like this one you lose some of your “visual” control, but it’s in favor of simplicity and maintainability. It is true that you won’t be able to set pixel-perfect values, but you will deal with responsiveness to a whole new level: most of the work will be setting a robust scale of values for spacing and typography. Then you can edit 2–3 variables at specific media queries to create a cascade effect, as opposed to tweaking all components and sub-elements.

How to set a default padding for all components

There will be cases where you need different components to have the same padding. Setting a variable for the default padding of the components is a trick I’ve learned after a lot of time spent looking for a padding value hidden somewhere in my CSS files. 😓

To be clear, I’m referring to those blocky, “container-like” components.

Here’s an example:
Create your design system - 图12
Getting back to our _spacing.scss file, let’s include the component padding variable:

  1. :root {
  2. --space-unit: 1em;
  3. --space-xxs: calc(0.25 * var(--space-unit));
  4. --space-xs: calc(0.5 * var(--space-unit));
  5. --space-sm: calc(0.75 * var(--space-unit));
  6. --space-md: calc(1.25 * var(--space-unit));
  7. --space-lg: calc(2 * var(--space-unit));
  8. --space-xl: calc(3.25 * var(--space-unit));
  9. --space-xxl: calc(5.25 * var(--space-unit));
  10. /* components padding */
  11. --component-padding: var(--space-sm);
  12. }
  13. .component {
  14. padding-top: var(--component-padding);
  15. }

By doing so, you create another “dev shortcut”: if you need to apply padding to a component, you don’t need to check which spacing variable has been used in other similar cases. Just use the component-padding variable.

Margin utility classes

While it’s safe to include padding directly in your component CSS, including margins can cause layout issues. Once again, we’re referring to the main components, those blocks that define your main layout (not a