Introduction

WIP

This page documents changes proposed in the RFC for V1, and therefore prone to bugs/sudden API changes. Please use with caution.

raam (estonian) ˈrɑːm n. frame.

Beautifully boring cosmetic-free primitives for structure and layout.


Getting Started

Choose your own boring adventure:

  1. Mixins - complimented by Recipes (recommended)
  2. Components

Mixins

JavaScript style functions to quickly roll-your-own resilient layout components (not just for React.js).

Installation

npm i --save raam
or
yarn add raam

Flexbox

At the time of writing, "flex-gap" is only supported by newer versions of Edge, Chrome and Firefox.

It's often the case to approach new CSS features with a @supports query, but unfortunately, this is not an option we can take advantage of.

flexbox() aims to address this "gap" in support, with a plain CSS polyfill powered by custom properties.

At the root of your component, flexbox({ ...options }) is called with your specified options.
This returns functional styles for:

  • .parent()
  • .child()
// import { flexbox } from "raam";
() => {
const { parent, child } = flexbox({
gap: "1rem",
});
return (
<div style={parent()}>
{Array.from({ length: 3 }).map((item, index) => (
<div
key={index}
style={{
...child({ index }),
backgroundColor: "var(--color-primary)",
color: "var(--color-white)",
padding: "0.5rem 1rem",
}}
>
Item {index + 1}
</div>
))}
</div>
);
};

How It Works

Under-the-hood, raam replaces gap with a set of custom properties to control parent and child margins. These are toggled off (back to initial) depending on the requirements of specific flex properties; a technique heavily inspired by David Khourshid's "prop-and-lock" technique.

For example, when flexWrap is nowrap (i.e. 'stack'-based layouts) the negative offset margin on the flex parent is toggled off.

In nowrap based layouts, margins are used in a single direction excluding the first-child.

In wrap based layouts, negative margins are used on the outer component to counteract margins inside.
It will not affect adjacent margins, but you will experience issues if you try to adjust it directly - instead consider wrapping the element.

Options

NameTypeDefault
gapResponsiveStyleValue1rem
variantwrap | hStack | vStackhStack
alignContentResponsiveStyleValue
alignItemsResponsiveStyleValue
flexDirectionResponsiveStyleValuerow
flexWrapResponsiveStyleValuenowrap
justifyContentResponsiveStyleValue
justifyItemsResponsiveStyleValue
.child()
NameTypeDefaultDescription
index*numberUsed in place of :*-child pseudo selectors
flexResponsiveStyleValue0 0 auto
flexBasisResponsiveStyleValue
flexGrowResponsiveStyleValue
flexShrinkResponsiveStyleValue

Types

  • ResponsiveStyleValue

    Accepts either a single value for the style property's value, or an array of value or { "@media query": value }.

    For example, the following options are acceptable for gap:

    • gap: "2rem"
    • gap: { initial: "2rem", "@media (min-width: 40em)": "1rem" }

Components

If rolling-your-own components hasn't got you hooked, you may be interested in a pre-built option.

Currently offering solutions for:

Migrating from an earlier version of raam?

Prior to version 1, raam exported a set of individual Theme-UI components. These have now moved to into a single <Flex /> component with variants:

  • <Stack {...options} /> => <Flex variant="vStack" {...options} />
  • <Inline {...options} /> => <Flex variant="hStack" sx={{ overflowX: "auto" }} {...options} />
  • <Wrap {...options} /> => <Flex variant="wrap" {...options} />

Installation

npm i --save theme-ui @raam/theme-ui
or
yarn add theme-ui @raam/theme-ui

Flexbox

A flexbox-based layout primitive that aims to address the gap.

// import { Flexbox } from "@raam/theme-ui";
// import { Box } from "theme-ui";
<Flexbox
alignItems={["center", "start", "end"]}
variant="hStack"
gap={[3, 4, 5]}
>
{Array.from({ length: 6 }).map((item, index) => (
<FlexboxItem key={index}>
<Box
sx={{
width: "2rem",
height: `${index + 1}rem`,
backgroundColor: "primary",
filter: index > 0 && `brightness(${100 - index * 10}%)`,
}}
/>
</FlexboxItem>
))}
</Flexbox>

Props

  • Props for each flexbox() option.

    • gap also accepts a key from theme.space (as a string or number), but if that's not found it'll render the provided string (e.g. em or rem) or number as a px value.
  • as: change the HTML element rendered (via Theme UI)

    raam makes an opinionated choice on how to render a component's children based on the element provided:

    aschildren rendered as
    div (default)div
    olli (with list-style-type reset)
    ulli (with list-style-type reset)
    spanspan
    pspan
    h1-h6span
  • sx: apply themed styles to the component (via Theme UI).

Recipes

  1. Box
  2. Wrap
  3. VStack
  4. HStack
  5. Inline
  6. Combo

Box

Each recipe assumes you have a Box component defined for base styles. In this case, an sx prop is used to pass themed style values (like Theme UI).

You don't necessarily need to follow this approach, feel free to make it your own.

Wrap

A flex-based layout that renders and 'wraps' children inline, spaced by the defined gap.

// import { flexbox } from "raam";
// import { Box } from "./box";
() => {
const { parent, child } = flexbox({ variant: "wrap" });
return (
<Box as="ul" sx={parent()}>
{Array.from({ length: 32 }).map((item, index) => (
<Box
as="li"
key={index}
sx={{
...child({ index }),
width: "2rem",
height: "2rem",
backgroundColor: "primary",
filter: index > 0 && `brightness(${100 - index * 2}%)`,
}}
/>
))}
</Box>
);
};

Customisation

Let's make it more interesting, let's say we want the last item to fill the remaining available space.

When the index indicates the "last child" (the array's length - 1), we'll pass the additional flexGrow parameter to the .child and set it to 2.

With the gap prop, we also have the ability to create responsive styles. In this example, we'll reduce the gap size on larger screens.

// import { flexbox } from "raam";
// import { Box } from "./box";
() => {
const { parent, child } = flexbox({
variant: "wrap",
gap: {
initial: "2rem",
"@media (min-width: 40em)": "1rem",
},
});
return (
<Box as="ul" sx={parent()}>
{Array.from({ length: 32 }).map((item, index, arr) => (
<Box
as="li"
key={index}
sx={{
...child({
index,
flexGrow: arr.length - 1 === index && 2,
}),
width: "2rem",
height: "2rem",
backgroundColor: "primary",
filter: index > 0 && `brightness(${100 - index * 2}%)`,
}}
/>
))}
</Box>
);
};

Stack

Popularised by Seek's "Braid", the "Stack" is a flex-based layout that renders children in a single column or row, spaced by the defined gap.

VStack

Renders items in a single column.

// import { flexbox } from "raam";
// import { Box } from "./box";
() => {
const { parent, child } = flexbox({ variant: "vStack", gap: "1rem" });
return (
<Box sx={parent()}>
{Array.from({ length: 4 }).map((item, index) => (
<Box
key={index}
sx={{
...child({ index }),
height: "2rem",
backgroundColor: "primary",
}}
/>
))}
</Box>
);
};

"Hold up, why don't you just…"

  • "…use display: grid;"
    Grid is fantastic for page layouts, but has its caveats for a 'simple' Stack:

HStack

The default setting of flexbox; the HStack renders items in a single row and makes no assumptions on your overflow.

// import { flexbox } from "raam";
// import { Box } from "./box";
() => {
const { parent, child } = flexbox({ variant: "hStack", gap: "1rem" });
return (
<Box sx={parent()}>
{Array.from({ length: 8 }).map((item, index) => (
<Box
key={index}
sx={{
...child({ index }),
width: "2rem",
height: "2rem",
backgroundColor: "primary",
}}
/>
))}
</Box>
);
};

Inline

If you'd rather let hStack items scroll elegantly in a single line, add an overflowX declaration alongside your .child() styles.

// import { flexbox } from "raam";
// import { Box } from "./box";
() => {
const { parent, child } = flexbox({ variant: "hStack", gap: "1rem" });
return (
<Box sx={{ ...parent(), overflowX: "auto" }}>
{Array.from({ length: 32 }).map((item, index) => (
<Box
key={index}
sx={{
...child({ index }),
width: "2rem",
height: "2rem",
backgroundColor: "primary",
filter: index > 0 && `brightness(${100 - index * 2}%)`,
}}
/>
))}
</Box>
);
};

or with some more chaotic values…

// import { flexbox } from "raam";
// import { Box } from "./box";
() => {
const size = () => `${Math.floor(Math.random() * 4) + 1}rem`;
const { parent, child } = flexbox({
variant: "hStack",
gap: "1rem",
alignItems: "center",
});
return (
<Box sx={{ ...parent(), overflowX: "auto" }}>
{Array.from({ length: 32 }).map((item, index) => (
<Box
key={index}
sx={{
...child({ index }),
width: size(),
height: size(),
backgroundColor: "primary",
filter: index > 0 && `brightness(${100 - index * 2}%)`,
}}
/>
))}
</Box>
);
};

Combo

When combining flexbox() styles, split them across different elements to avoid conflicts.

Let's take a look at a more real-world example; a "tag"-list at the bottom of an article:

// import { flexbox } from "raam";
// import { Box } from "./box";
() => {
const { parent: stackParent, child: stackChild } = flexbox({
variant: "vStack",
gap: "1rem",
});
const { parent: wrapParent, child: wrapChild } = flexbox({
variant: "wrap",
gap: "1rem",
});
return (
<Box sx={{ ...stackParent(), padding: 3 }}>
<Heading as="h2" sx={stackChild({ index: 0 })}>
Tags
</Heading>
<Box sx={stackChild({ index: 1 })}>
<Box as="ul" sx={wrapParent()}>
{Array.from({ length: 8 }, (v, k) => k + 1).map((item, index) => (
<Box key={index} as="li" sx={wrapChild({ index })}>
<Link href={`#list-item-${item}`}>Tag {item}</Link>
</Box>
))}
</Box>
</Box>
</Box>
);
};

Acknowledgements

Without these projects/people, this project wouldn't exist…