Design Systems, Pt. 8: Layout Rules
We can extend a design system beyond the definition of design tokens and define rules for layouts as well, including rules for responsive behavior. This touches the topic of algorithmic design. This article covers both overarching rules and layout rules for specific use cases.
The examples in this article list the properties that are relevant for the particular layout and that cannot necessarily be derived from a static design mock-up without annotations. The code examples set the same properties as CSS custom properties, which allows you to generalize these layouts.
The focus shall be less on the specific techniques, but more on the underlying properties of these kinds of layouts that designers and developers should be able to reflect on.
Base rules
These are basic rules and defaults to apply across the project.
Horizontal and vertical spacing
We already added layout constraints for vertical spacing in part 4 when we generated the utility classes. Instead of allowing all spacer values for horizontal and vertical margins and paddings, we restricted vertical spacing to multiples of the base line height and horizontal spacing to multiples of the spacer base.
That was just an example for the generation of utility classes, but it is a good idea to formulate the underlying spacing rules. Following our example, we can define the set of rules for our design system.
- Vertical spacing must be based on the defined line heights.
- Horizontal spacing must be based on multiples of
0.25rem
.
Different design systems will have different sets of rules. We might define variations on the rules above:
- Both horizontal and vertical spacing must be based on multiples of
6px
. - Vertical spacing must be chosen from this set:
(0.25rem, 0.5rem, 1rem, 2rem, 4rem, 8rem)
.
Default vertical margin
This defines the default margin to use for margins between paragraphs, between form inputs, between paragraphs and lists, and anything else that appears underneath each other on a page. Of course not all elements will have the same vertical margin in between, but defining a default in the design system allows us to define a default in our styles as well and only set margins when exceptions occur.
The rule might be phrased like this:
The default margin between stacked elements is
1 line
.
Based on this rule, you can add a stack layout like this:
@use "../functions" as f;
.stack > * + * {
margin-top: var(--space, f.lines(1));
}
Variations on this rule might look like this:
- The stacked elements must have a vertical margin of
1 line
or0.5 lines
. The default is1 line
. - The default margin between stacked elements is
1 line
. The default margin between stacked sections is1.5 lines
.
The last rule could be translated into a stack layout again:
@use "../functions" as f;
.stack {
& > * + * {
margin-top: var(--space, f.lines(1));
}
& > section + * {
--space: #{f.lines(1.5)};
}
}
Of course we don’t need to add every exception to the rules. We’re more concerned about the general rules and defaults when specifying the system. One-off exceptions can be taken from the design mock-ups (knowing that we have constraints for margins that we should follow in case we encounter values that don’t fit the specification for vertical spacing).
Layout rules for specific use cases
These are not global rules, but rules that describe the constraints under which a specific element should behave on different viewport sizes. This kind of information is often missing from design specifications and mock-ups. We can add annotations to the mock-ups to communicate the intend.
These rules are very close to the CSS properties that need to be set. While the designer defining such rules doesn’t need to know all the intricacies of CSS, they still need a good understanding of what can be achieved.
Max widths or scaling
We already defined the measure
as a design token, which we apply to all HTML
elements containing text. Often missing from design specifications is
information about how layouts should behave on extremely large viewports. You
may want to annotate:
- a max width for any kind of container
- the horizontal alignment if the max width is exceeded (left, center, right)
- font scaling depending on viewport width
Example
- max width: 32rem
- align: center
.container {
--max-width: 32rem;
max-width: var(--max-width);
margin-right: auto;
margin-left: auto;
}
Grouping items
Properties to consider when grouping any kind of elements in a row, e.g. buttons, tags or tabs:
- wrap in multiple lines or scroll horizontally
- horizontal and vertical gap
- fixed width of elements or fit to content
- stretch to full width or jagged edge
Example 1
- gap: 4 spacers horizontally, 0.5 lines vertically
- wrap elements
.some-group {
--gap: #{f.lines(0.5)} #{f.spacer(2)};
--wrap: wrap;
display: flex;
flex-wrap: var(--wrap);
gap: var(--gap);
}
Example 2
- gap: spacer 2
- no wrap, scroll horizontally
.some-group {
--gap: #{f.spacer(2)};
--wrap: no-wrap;
display: flex;
flex-wrap: var(--wrap);
gap: var(--gap);
overflow-x: auto;
}
Image placement
When images are used, it is helpful to annotate how they are supposed to be scaled, for example:
- auto size
- fixed size (in either one or two dimensions)
- scaled to available space
- fixed aspect ratio
Example
- width 100%, 16:9, cover area
figure {
--x: 16;
--y: 9;
--fit: cover;
position: relative;
padding-bottom: calc(100% * var(--y) / var(--x));
img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: var(--fit);
overflow: hidden;
}
}

Placing two items side by side
We can formulate rules for placing two elements next to each other, so that we know how they should behave on different screen sizes.
We want to place the items next to each other when there is enough room, but wrap them when the viewport is too narrow.
Examples are page titles aligned with a primary action button, labels aligned with inputs, navigation bar logos aligned with navigation bar items, or content areas aligned with a sidebar or an aside.
Properties:
- The base width of each element. The base width can be
auto
if the width of the element depends on its content, or it can be a concrete value like15rem
or25%
. One of the elements can be set to take up all remaining space. - If one of the elements is supposed to take up all remaining space, we need to define a minimum width for that element.
Example 1
- Element A: width 10rem
- Element B: fill up space, min. 60%
- gap: spacer-4
Instead of creating multiple design mock-ups for different screen sizes, we can annotate the screens with rules like these.
The rules above can be translated into this SCSS:
@use "../functions" as f;
.some-wrapper {
--width-left: 10rem;
--min-width-right: 60%;
--gap: #{f.spacer(4)};
display: flex;
flex-wrap: wrap;
gap: var(--gap);
> .element-a {
flex-basis: var(--width-left);
flex-grow: 1;
}
> .element-b {
flex-basis: 0;
flex-grow: 666;
min-width: var(--min-width-right);
}
}
Example 2
- Element A: min width 20rem
- Element B: min width 10rem
- gap: spacer-2
@use "../functions" as f;
.some-wrapper {
--width-left: 20rem;
--width-right: 10rem;
--gap: #{f.spacer(2)};
display: flex;
flex-wrap: wrap;
gap: var(--gap);
> .element-a {
flex-basis: var(--width-left);
flex-grow: 1;
}
> .element-b {
flex-basis: var(--width-right);
flex-grow: 1;
}
}
Responsive grid
We want a grid that adjusts the number of columns depending the viewport width. These are the relevant properties:
- minimum column width: prevent columns from getting smaller than this value. If there isn’t enough room, the column count is reduced.
- gap
- maximum column count (optional)
- minimum or maximum height for rows
- overflow behavior if cell content exceeds dimensions
Example 1
- minimum column width: 10rem
- gap: spacer-4
Which translates to:
@use "../functions" as f;
.grid-example-1 {
--min-width: 10rem;
--gap: #{f.spacer(4)};
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--min-width), 100%), 1fr)
);
gap: var(--gap);
}
Example 2
- minimum column width: 10rem
- maximum column count: 3
- gap: spacer-4
@use "../functions" as f;
.grid-example-2 {
--min-width: 10rem;
--max-columns: 3;
--gap: #{f.spacer(4)};
/* calculated */
--min-column-percent: calc(100% / var(--max-columns));
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(
clamp(var(--min-column-percent) - var(--gap), var(--min-width), 100%),
1fr
)
);
gap: var(--gap);
}
Overlapping items
What are the positioning rules if there are overlapping items on the page? Think about an article header overlapping the cover image. Is there an underlying grid we can adhere to? Is the header positioned in terms of percentages?
Overlapping layouts can be easily achieved with CSS grids. We can define columns
and rows that are sized depending on the content and configure the remaining
columns and rows to take up fractions of the remaining space by utilizing the
fr
unit. If there is not enough room, these latter columns and rows will
collapse to make room for the content.
Getting the responsive behavior right requires some tweaking in the browser. Understanding the underlying grid and ratios is essential, as well as any minimum/maximum widths and heights, and whether any columns and rows should be collapsible.
Example
In this example, an image is displayed with a fixed ratio of 4:5. An uneven 5x5
grid is imposed on the area. The column for the figcaption
is sized to fit the
content. The font size depends on the viewport width. The box with additional
text is set to be sized 3fr
, but at least 21em
. The remaining columns use
fr
units and collapse to make room for the content columns on smaller viewport
sizes.
.overlay-example-1 {
--x: 4;
--y: 5;
position: relative;
padding-bottom: calc(100% * var(--y) / var(--x));
figure {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: grid;
grid-template-rows: 1fr 12fr 4fr 2fr 1fr;
grid-template-columns: 1fr max-content 1fr minmax(21em, 3fr) 1fr;
& > img {
grid-row: 1 / span 5;
grid-column: 1 / span 5;
width: 100%;
height: 100%;
object-fit: cover;
overflow: hidden;
}
& > figcaption {
grid-row: 2 / span 3;
grid-column: 2 / span 1;
margin: 0 f.spacer(4);
color: f.color(text-invert);
font-weight: f.weight(bold);
font-size: min(#{f.size(xxxxl) + " + 6vw"}, 6rem);
writing-mode: vertical-rl;
}
& > div {
display: flex;
flex-direction: column;
grid-row: 3 / span 1;
grid-column: 4 / span 1;
align-items: flex-start;
width: 100%;
height: 100%;
padding: f.lines(0.25) f.spacer(2);
color: f.color(text-invert);
background: f.color(selection);
}
h3 {
margin-bottom: f.lines(0.5);
font-weight: f.weight(bold);
font-size: f.size(xl);
line-height: f.lines(1);
}
ul {
padding-left: 0;
font-weight: f.weight(bold);
font-size: f.size(s);
list-style-type: none;
}
}
}
Breakpoints
As you have seen above, a lot of responsive behavior can be accomplished without media queries. However, we cannot completely get around them, in particular whenever an element is transformed to something different depending on the viewport size, for example when switching between an expanded menu and a hamburger menu.
For these cases, we already defined a set of breakpoints. The best way to illustrate the intended behavior is still to add separate screens for the layout variants. Annotate the breakpoints at which the changes should be applied.
Some layout changes may not even require mock-ups for the different viewport sizes. For example, if you want to adjust the header sizes while still adhering to the font scale and without affecting the body text, you can just annotate this (e.g. h1 up to breakpoint tablet: size xl, from breakpoint tablet: size xxl).
Summary
The more detailed you define the layout rules, the closer you are to writing the actual CSS. Whether it is necessary to annotate every layout in this detail level is debatable. The question comes down to how detailed a designer wants to define a layout, and how much freedom is given to the developer to choose a solution that they think works best.
The more complex a layout is, the more fine-tuning it will require in the browser. It may be best to define the basic idea behind a layout and figure out the details during the implementation.
Rules can be written as sentences, key/value pairs, or with lines, arrows and annotations in the graphics software. How the rules are defined is up to you, as long as everybody knows what is meant. The only important thing is clarity of intent.
References
Next part: Design Systems, Pt. 9: Conclusion