DevelopmentLayout-isolated componentsWritten by Emil Sjölander on Wed Apr 01 2020

The move to component-based development has enabled a large number of really incredible improvements to tools and the front-end ecosystem as a whole. Remember when we were building our apps as a set of screens and pages instead of thinking in components? The component model has changed this entirely - now, we think of pages as a composition of reusable components. As a result, we've also seen a huge increase in the number of open source UI components being built, published, and reused between applications. This modular approach to UI is vital to us at Visly since it's what enables our product to work with any app right away. In the pre-component era, we would have probably required you to build your whole app inside of Visly from scratch.

However, a lot of CSS layout patterns come from this pre-component era, and I want to discuss with you here how some of those patterns should be deprecated, as they break the modularity and composition assumptions of components.

Layout isolation

Before we jump into the properties and patterns we should be avoiding, I think it's worth exploring conceptually what we're trying to avoid. Essentially, we want to avoid any properties on the root element of a component that affect, or are affected by, elements outside of the bounds of that component.

I would discourage properties like margin, because they act on elements outside of the component's scope; the same goes for properties like align-self, as it will stretch the width or height of the component depending on the flex-direction of its parent. In contrast, properties like padding are fine, as they are confined to the scope of the component. Basically, if a property depends on, or impacts, other components outside of its scope, I would discourage using it.

Layout-isolated component - A component that is unaffected by the parent it is placed within, and does not itself affect the size and position of its siblings.

Something I think worth reiterating is that this only applies to the root element of a reusable component. So for example, as you can see in the code below, using align-self does not make a component break layout isolation automatically, just if it is placed on the root element.

// Does NOT comform to layout isolation principals
function MyComponent() {
  return (
    <div style={{alignSelf: 'center'}}>
      <div />
    </div>
  )
}

// This component is layout isolated
function MyComponent() {
  return (
    <div>
      <div style={{alignSelf: 'center'}}/>
    </div>
  )
}

What properties make a component break layout isolation?

Any property which affects, or is affected by, elements outside a component scope are not layout isolated properties and should be avoided on the root layer of your component. I'll cover a couple of the most common properties (and why they pose a problem) below.

Align self

The reason align-self is not layout-isolated is that it will apply vertical or horizontal alignment to the component depending on the direction of the container in which it's placed. It's especially bad when using stretch, as setting that as the value means your component will end up stretching either vertically or horizontally depending on its container. Some components are built to be stretched in either direction, but this is not the case for the vast majority of components.

So how do you solve this? Should you stop using align-self? Not at all! All you need to do to make your component reusable while still using align-self is to allow the parent to pass in the alignment value.

// Don't do this. This component isn't very re-usable
function MyComponent() {
  return <div style={{alignSelf: 'stretch'}}/>
}

// Do this. This component can be used from anywhere
function MyComponent(props) {
  return <div style={{alignSelf: props.alignSelf}}/>
}

Flex properties

Flex (flex-grow & flex-shrink) is a fun - though that may be my Stockholm syndrome speaking - and incredibly powerful property. However, as they say, with great power comes a great amount of bugs. Flex actually breaks layout isolation principles in two ways, which leads to countless bugs. Trust me - I was the go-to "flexbox guy" at Facebook for close to three years, and at least 25% of all problems people had were because they were re-using components with flex properties in contexts they weren't initially designed for.

So what does flex do? Flex describes how a component flexes in the main axis. What defines the main axis? The container. Which by definition makes flex break layout isolation. But even if that wasn't the case, there's a second aspect of flex properties that also make them affect components outside their scope. The amount a component flexes depends on its flex value in relation to the sum of all its siblings' flex values. So if you are expecting a component to flex into all the remaining space, that won't always be true, depending on the other siblings. Because of this, flex properties are doubly bad when it comes to making components reusable.

There is one important thing to note here. Flex shrink defaults to 1, which by this definition makes every single component with display: flex break layout isolation. In my opinion, you should change this default because it creates a whole lot more problems than it solves. For this reason, components built in Visly all default to having flex-shrink and flex-grow set to 0.

Just like with align-self, the solution is to move flex out of the component and into a prop that can be controlled by the wrapping component.

// Don't do this. This component isn't very re-usable
function MyComponent() {
  return <div style={{flex: 1}}/>
}

// Do this. This component can be used from anywhere
function MyComponent(props) {
  return <div style={{flex: props.flex}}/>
}

Percentages

Percentage dimensions are like flex, but not quite as dangerous because the dimension they apply to does not depend on the parent component. However the size is by definition based on the percentage of the parent size. This means it can be quite easy to get into situations like the one below, where a button looks great in one context but terrible in another.

Percentage sizes being set on the button, by the button, are the problem here. And like all the other examples, it can be fixed by allowing the parent to set a percentage size on the button rather than having the button component itself control that.

// Don't do this. This component isn't very re-usable
function MyComponent() {
  return <div style={{height: '100%'}}/>
}

// Do this. This component can be used from anywhere
function MyComponent(props) {
  return <div style={{height: props.height}}/>
}

If you're building components in Visly, you'll notice that we allow setting the dimensions of a component to 100%. While I don't personally believe that this something we should be using, because it's so common, we chose to support it in Visly. However, all components built in Visly have the ability to have their dimensions overridden by their parent, from either Visly or code.

Margins

Ever fixed a layout using negative margin? Ever wrapped a component in a div to add some extra margin? Both are quite common and serve a good lesson as to why components themselves should never include margin. The reason margin has this issue and padding doesn't is that margin applies to the space outside of the component, while padding applies to the space within. The amount of margin you want on a component depends on which siblings are next to it and the container where your component appears - highly context-specific knowledge that, if built to be reusable, your component should not know about.

While this can be solved, just like all the rest of these examples, by passing margin in as a prop from the parent, we have found a <Spacer/> component to be far superior. Aside from the parent-dependency problems of margin, they have a discoverability problem. Is the spacing due to left margin on this component, or right margin on this other component? It's impossible to know without inspecting the DOM! <Spacer/> components solve this and are also a lot easier to manipulate. Want that same spacing between two other elements? Just copy paste the spacer - much easier than fiddling with margins.

While not exactly the same as code, you can see a lot of these benefits in Visly. We don't support margins at all, instead relying on a spacer component. In an update coming soon, we will also be adding support for stack spacing, a concept where a container can configure the spacing between its children.

Tab index

Tab index doesn't change the visual layout of an element but it does impact the layout of an element for screen readers, or people who prefer navigating interfaces using their keyboard. Because it impacts the order of siblings, by definition it breaks layout isolation. Like the previous example, you'll want to move tab index out of the components' control and into its props so that it can be set by the parent.

Absolute positioning

This one is not as bad as the others, as it typically doesn't break anything; it may just make your component unusable in certain situations. For example, if you make a Notification component, you may be inclined to add a position: absolute rule on it, since notifications pop up on top of other content. However, later on, you may want to show the notifications within the context of a sidebar. In this case, you no longer want to position the notification with absolute coordinates.

Like all the previous examples, you can fix this by passing these styles in from above! You never know where your components may be used in the future. In this example we go further by declaring all the styles that can be set on the component as a subset of CSSProperties.

import { CSSProperties } from 'react'

type Style = Pick<
    CSSProperties,
    'position' | 'left' | 'top' | 'right' | 'bottom'
>

interface Props {
	style: Style
}

// This component can be used from anywhere
function MyComponent(props: Props) {
  return <div style={props.style}/>
}

In conclusion

  • Build your components to be reusable; you never know where you'll end up needing them.
  • Add a style prop to your components so that layout responsibility is shifted to the parent.
  • Update your global CSS and disable default flex shrink * { flex-shrink: 0; }.
  • Use <Spacer/> components or stack spacing instead of margin to make code easier to move around.

The idea for this post came out of a twitter discussion with Max Stoiber who shares a lot of my thoughts on this - you should check out his thoughts on margin.

If you have any questions about Visly or comments on the article, please reach out over email or twitter. If you want to give Visly a try, please request access.