Writing self-contained layout components in CSS

As developers, when using modern UI libraries and frameworks like React, Vue or Web Components, we often like to write our CSS specifically for a single component. That way we can keep our global styles rather manageable and get the additional benefit of a well-structured CSS architecture. At the same time, design systems these days also consist of individual, reusable components that can be used in multiple places. Sometimes these components are put together in UI / pattern libraries, which are a part of a full design design system. One example of this are the Tailwind UI components, which are customizable and ready to use.

In this article, we'll take a closer look at how we can build our own self-contained and composable components that can be used as part of a larger design system and what we need to pay attention to, in order to create truly context-independent layout components.

Preventing the worst: side effects

The biggest hurdle when writing components from scratch is that we have no knowledge of the context they will be used in. In fact we want to create components that assume nothing about the context they will be used in. That means we need to make sure not to produce any side effects, like additional margins, or we risk breaking the entire layout. Here's a tiny example of a bad component to illustrate those side effects:

.component-1 {
  margin: 2rem;
}

.component-2 * {
  margin: 2rem;
}

While it looks fairly simple, it also illustrates how those side effects could look. Using that component in another context, like a list, we'd suddenly push whatever comes above or below it in the layout away by 2rem. That's not ideal! Instead of applying margins to our component or all children, what we should do is have an unstyled wrapper that doesn't assume anything about where it's used and doesn't create side effects like margins to the surrounding elements. Let's see how we can do that.

.component > * + * {
  margin-top: 1.5rem;
}

Now with the .component being our outer container that we should not apply any styles to, we now apply them to its children. And of those, we're only giving them a margin if they're preceded by another element – that means out first child will not have a margin-top applied, resulting in no side effects for our component overall. We can stack as many children as we want, without affecting any surrounding elements.

Creating more complex components – two columns

Now that we have a first of idea how we can keep our components isolated, let's look at a more complex component that needs to adjust base on the context it's used in as well as the general screen size. While container queries aren't fully here yet, they will likely make this example easier in the future. For now, we'll rely on flexbox to determine the components layout, depending on the context. Here's what we'll try to achieve – a classic media object with two columns:

image

Because we'll need to break the component's layout into two rows on smaller screens, we'll start out by giving our container the flex-wrap: wrap property, which allows the content to flow both ways (horizontally on larger screens and vertically on smaller screens).

.media-element > .media-element-wrapper {
    display: flex;
    flex-wrap: wrap;
}

Notice that we're again not applying any styles to our outer element, in order to prevent any side effects. Instead, we wrap our child elements in a .media-element-wrapper container. Next, we need to decide which side of our components is supposed to shrink and which when to break. In this example, we'll let the image (the left side) stay the same until a certain breakpoint is reached, while the right side adjusts in its size all the time. We can do that by giving our left-side element a set flex-basis, our right side gets a flex-basis of zero. We could as well switch those, so the left side resizes and the right one stays a certain size.

/* our left side */
.media-element-media {
    flex-basis: 280px;
    flex-grow: 1;
}

/* our right side */
.media-element-content {
    flex-basis: 0;
    flex-grow: 999;
    min-width: calc(50% - 1rem);
}

With that setup, we're basically done with the component. the left side will stay the same size, until the viewport reaches a min-width of 280px, which will break the component's layout into vertical alignment. Here's the matching HTML for an input field left and a button on the right side.

<div class="media-element">
    <div class="media-element-wrapper">
        <div class="media-element-content">
            <input type="text" placeholder="Type something..." />
        </div>
        <div class="media-element-media">
            <div class="button">Click me</div>
        </div>
    </div>
</div>

As described above, we managed to build this component to be responsive, self-contained, and without side effects. So no matter where in a larger set of components it is used, it doesn't interfere with any elements outside our wrapper. Now if we wanted to style our component and the elements inside the wrapper, we can do so more freely. Of course, spacing should still only happen inside the element, but we can always rely on our adjacent sibling selector or a more recursive version of it to space things evenly:

.media-element > * > * {
    margin: 0.5rem;
}

Wrapping up

When building more components for a complex system, it is a good idea to re-use "atomic components" or patterns, like the adjacent sibling selector and to make sure to regularly look at how the components actually play together on a page. Even when avoiding side effects, everything also depends on the containers our components are used in.

It's also important to set some general rules, like applying a sane box model to all elements, to ensure all components will handle spacing consistently.

* {
  box-sizing: border-box;
}

This article is just a small example of a single component and there's still a lot more that makes a good design system. If you want to dig deeper, here are some further articles worth reading.

https://www.invisionapp.com/inside-design/guide-to-design-systems/

https://bradfrost.com/blog/post/css-architecture-for-design-systems/

https://designsystemsrepo.com/design-systems-recent/

Code

The full code from this example can also be found on GitHub and on CodePen