by

Travis Waith-Mair

@travisWaithMair

non-traditional.dev

How to achieve layout composition in React

MySudo

The Age of Components

Components let you split the UI into independent, reusable pieces, and think about each piece in isolation

-React Docs

CSS was not designed for components

CSS was invented to solve problems from the top down.

Exception Based Styles

A typical CSS styled-sheet

/* General Styles */
body {
}

/* Sitewide */

.main-nav {
}

.footer {
}

/* More Specific */

.blog-feed {
}

.sign-up-form {
}

#first-name-input {
}

Components Start From The Bottom Up

  • Breakdown pages into more specific parts
  • Often in isolation
  • Composed together to build up the whole

Tooling is Not Enough

Tools like SASS, Less, CSS-Modules, and CSS-in-JS all solve the problem of maintaining CSS style sheets.

They don't show you how to make your CSS composable

How Do We Achieve Layout Composition?

Hero Layout

A Naive Solution

.hero-container {
  /* container styles */
}

.hero-top {
  /* top styles */
}

.hero-left {
  /* left styles */
}

.hero-right {
  /* right styles */
}

/* ect... */

BEM, OOCSS, Functional CSS or Atomic CSS help manage our CSS style sheets at scale.

 

When we approach our layout as something unique for each component, we miss a fantastic opportunity.

CSS methodologies only get you so far

What if I told you CSS layout could be componentized?

Layout Primitives

function Stack() {
  /** vertically stacks elements on top of each other */
}

function Inline() {
  /** horizontally stacks elements in a row */
}

function Split() {
  /** splits the parent's width between two elements */
}

function Cover() {
  /** covers an area and vertically centers its child */
}

function Frame() {
  /** Frames out a media into the correct aspect ratio */
}

Composable Solution

In JSX

export function Hero() {
  return (
    <Stack>
      <Inline>{/* */}</Inline>
      <Split>
        <Cover>
          <Stack>
            <h1>{/* */}</h1>
            <p>{/* */}</p>
            <Inline>
              <button>{/* */}</button>
              <button>{/* */}</button>
            </Inline>
          </Stack>
        </Cover>
        <Frame>
          <img />
        </Frame>
      </Split>
    </Stack>
  );
}

Layout Primitives can be used across our app.

  • A Signup form
  • A Blog Post Feed
  • A Feature Page
  • etc

If tooling and methodologies don't help, how do we create composable layouts?

Encapsulated CSS

The key to compositional layout

What is Encapsulated CSS?

 

Encapsulated CSS is a term I use to summarize the rules of how to apply CSS in a composable way.

The focus isn't on naming schemes, but on CSS properties themselves

Based on the programming principal of Encapsulation

Why Encapsulated CSS?

 

The biggest enemy to composing layouts is a component with built-in opinions on how it should be laid out.

The rules of Encapsulated CSS will help you avoid layout conflicts.

What is Encapsulation?

 

Encapsulation involves grouping related things and restricting access to those things, except through appropriate channels.

For Example

 

import React from "react";

React.createElement();
React.useState();
React.useEffect();
// etc

Where is the boundary of related properties in CSS?

 

The Two Principles of Encapsulated CSS

 

1. Elements do not lay themselves out

/* Not ok to set on the component */
.my-class{
  display: inline;
  width: 100px;
  margin-top: 2rem;
  position: relative;
}

(Layout properties === position, size and margin)

The Two Principles of Encapsulated CSS

 

2. Elements style themselves and layout only their immediate children.

/* Ok to set on the component */
.my-class{
  border: 1px solid #08228c;
  color: #4c6ef5;
  padding: 3rem;
  font-family: sans-serif;
}

.my-class > * {
  margin-block-start: 1rem;
  min-height: 50vh;
}

Applying The Principles

 

Normal Flow

 

We could use props

export const Component = (props) => {
  return (
    <div style={{ minWidth: props.minWidth }}>
      {/* Component content goes here */}
    </div>
  );
};

<Component minWidth={300} />;

It becomes unweildy very quickly

<Component
  width="50%"
  minWidth={300}
  height={500}
  display="inline-block"
  marginLeft="0.5rem"
  marginRight="2rem"
/>

*Use sparingly and only for components that specifically are used for layout.

Normal Flow

 

Direct child combinator

.parent > * {
  margin: 1rem;
}

.parent > h1 {
  display: inline-block;
}

This tool allows us to select any or all of the parent container's immediate children.

CSS Flexbox / Grid

 

//flex-container
<div style={{ display: "flex" }}>
  <div /> {/* flex-item */}
  <div>
    <div /> {/* not a flex-item */}
    <div /> {/* not a flex-item */}
  </div>
</div>

//grid-container
<div style={{ display: "grid" }}>
  <div /> {/* grid-item */}
  <div>
    <div /> {/* not a grid-item */}
    <div /> {/* not a grid-item */}
  </div>
</div>

Both Flexbox and Grid create containers that layout their direct children

CSS Flexbox / Grid

 

.parent{
  display: grid;
  gap: 1rem;
}

.parent > p {
  place-self: center;
}

Both Flexbox and Grid follow the rules of Encapsulated CSS very well.

 

A few flex and grid properties must be set on the direct children.

A Contrived Blog Post Componet

 

export const BlogPost = (props) => {
  return (
    <article className="blog-post">
      <h2 className="blog-title">{props.blogTitle}</h2>
      {props.paragraphs.map((paragraph, i) => {
        return <p key={i}>{paragraph}</p>;
      })}
    </article>
  );
};

Following Encapsulated CSS​

.blog-post {
  padding: 1rem;
}

.blog-post > h2 {
  margin-bottom: 3rem;
  max-width: 75%;
}

.blog-post > p + p {
  margin-top: 2rem;
}

.blog-title {
  text-transform: uppercase;
  font-size: 1.5rem;
  color: darkgray;
}

What about Layout Components?

Layout Components shouldn't try solve every potential layout

The rules Encapsulated CSS will help you when you need to build those one-off layout situations.

What makes a good layout component?

Solves common layout patterns

i.e. Any abstraction should be worth it

What makes a good layout component?

Should be focused on the problem it solves and not how it does it.

i.e. We don't want to make "FlexBox" Components or  "Margin" Components

What makes a good layout component?

Stylable​

i.e. They expose the `className` and/or `style` props

What makes a good layout component?

Should not break accessibility

i.e. Should not keep you from using the correct semantic HTML

The polymorphic `as` prop is a good solution for this

A Few Examples

(The examples will be using styled-components, but these can be recreated using any tooling, even just vanilla CSS.)

Stack

Stack

const spacingMap = {
  xs: "0.125rem",
  sm: "0.25rem",
  md: "0.5rem",
  lg: "1rem",
  xl: "2rem",
  xxl: "4rem",
};

const Stack = styled.div`
  display: grid;
  gap: ${(props) => spacingMap[props.gutter] ?? spacingMap.lg};
`;

Stack

export default function Subscribe() {
  return (
    <Stack as="section" gutter="xl">
      <Stack as="header" gutter="md">
        <h2>Subscribe To Our Newsletter</h2>
        <p>
          Subscribe to our newsletter to keep up to date on all our amazing
          products.
        </p>
      </Stack>

      <Stack as="form">
        <Stack as="label" gutter="sm">
          Name
          <input type="text" />
        </Stack>

        <Stack as="label" gutter="sm">
          Email
          <input type="text" />
        </Stack>

        <button>Subscribe</button>
      </Stack>
    </Stack>
  );
}

InlineCluster

InlineCluster

InlineCluster

const justifyAlignMap = {
  start: "flex-start",
  end: "flex-end",
  center: "center",
};

const InlineCluster = styled.div`
  display: flex;
  flex-wrap: wrap;
  gap: ${(props) => spacingMap[props.gutter] ?? spacingMap.lg};

  justify-content: ${(props) =>
    justifyAlignMap[props.justify] ?? justifyAlignMap.start};

  align-items: ${(props) =>
    justifyAlignMap[props.align] ?? justifyAlignMap.start};
`;

InlineCluster

export default function MenuBar() {
  return (
    <Menu>
      <InlineCluster as="nav" gutter="lg" justify="end" align="center">
        <a>Product</a>
        <a>Features</a>
        <a>Marketplace</a>
        <a>Company</a>
        <a>Log in</a>
      </InlineCluster>
    </Menu>
  );
}

Learn More

Thank You React India

 

@travisWaithMair

non-traditional.dev

How to achieve layout composition in React

By Justin Travis Waith-Mair

How to achieve layout composition in React

  • 317