@travisWaithMair
non-traditional.dev
Components let you split the UI into independent, reusable pieces, and think about each piece in isolation
-React Docs
CSS was invented to solve problems from the top down.
A typical CSS styled-sheet
/* General Styles */
body {
}
/* Sitewide */
.main-nav {
}
.footer {
}
/* More Specific */
.blog-feed {
}
.sign-up-form {
}
#first-name-input {
}
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
.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.
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 */
}
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>
);
}
The key to compositional layout
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
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.
Encapsulation involves grouping related things and restricting access to those things, except through appropriate channels.
import React from "react";
React.createElement();
React.useState();
React.useEffect();
// etc
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)
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;
}
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.
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.
//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
.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.
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>
);
};
.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;
}
The rules Encapsulated CSS will help you when you need to build those one-off layout situations.
Solves common layout patterns
i.e. Any abstraction should be worth it
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
Stylable
i.e. They expose the `className` and/or `style` props
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
(The examples will be using styled-components, but these can be recreated using any tooling, even just vanilla CSS.)
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};
`;
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>
);
}
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};
`;
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>
);
}
@travisWaithMair
non-traditional.dev