04 September 2020 • 5 minute read
Understanding Component Hierarchy
organizing components in a meaningful structure
Overview
This article will describe the basics of component hierarchy and why organizing components in composable structures creates more flexible and adaptable UI. The hierarchy we'll discuss is Primitives, Elements, and Compositions.
├ Primitives├ Elements├ Compositions
Understanding the Hierarchy
The main purpose of the hierarchy is to provide a focused scope of responsibility for individual components and meaningful relationships between them. This allows components to be composed in ways that provide a healthy balance of structure and flexibility. Without structure the components become misaligned and inconsistent. But without flexibility, the components become brittle and tightly constrained in their use. For a resilient component library, you need both.
If we think of flexibility and structure as a spectrum, different types of components will live along that scale. Primitives at the bottom of the hierarchy are the most flexible. And as you move up in the hierarchy to Elements and Compositions, components become less flexible and more structured. This allows more specific, opinionated components within your library to be consistent, while also providing space for lower-level components to maintain their flexibility.
But these components do not exist in isolation. The hierarchy also establishes meaningful relationships between them. Elements are used to build Elements, which are composed to create Compositions. For example, this LeftNav
is a Composition of a Box
Primitive and Nav
, List
, ListItem
, and Link
Elements.
// LeftNav.tsxconst LeftNav = () => {return (<Box padding="m"><Nav><List><ListItem><Link href="/">Home</Link></ListItem><ListItem><Link href="/about">About</Link></ListItem><ListItem><Link href="/blog">Blog</Link></ListItem><ListItem><Link href="/contact">Contact</Link></ListItem></List></Nav></Box>);};
These relationships allow LeftNav
to be an opinionated instance of its structure. However, if we needed to create another instance that allowed Icon
s to be paired with the Link
s, we could compose the Primitives and Elements instead of modifying this instance to support it. We're providing flexibility and structure through composability.
// LeftNavWithIcons.tsxconst LeftNavWithIcons = () => (<Box padding="m"><Nav><List><ListItem><Icon type="home" /><Link href="/">Home</Link></ListItem><ListItem><Icon type="about" /><Link href="/about">About</Link></ListItem><ListItem><Icon type="blog" /><Link href="/blog">Blog</Link></ListItem><ListItem><Icon type="contact" /><Link href="/contact">Contact</Link></ListItem></List></Nav></Box>);
Primitives
Primitives are our lowest-level component abstraction. They are only constrained by their purpose and our tokens and theme values. Because they are so flexible and can be formed into many different Elements, there are relatively few of them. They are solely responsible for visual concerns. They don't think about different states or behaviors.
// Primitives Example<Box padding="s" border={`solid 1px ${borderColor}`}>A bordered box</Box><Text as="p" fontSize="body" color="text">This is a short line of text.</Box>
In the internal library you'll want to use Primitives often to create other Elements and Compositions, but external consumers likely won't use them directly as often as other components. To understand why, let's look at an example, Stack
. Stack
is an opinionated layout Composition. It exists to help create vertical rhythm (spacing) between elements.
// Stack Example<Stack space="m"><Stack.Item>Item 1</Stack.Item><Stack.Item>Item 2</Stack.Item><Stack.Item>Item 3</Stack.Item></Stack>
By using Stack
, its child element Stack.Item
, and the space prop, we can quickly create even spacing between each element. But if that doesn't work for our use case, we could use the lower-level Element, Flex
, that it's built upon. Flex
is a layout component that knows about our spacing scale and how to use CSS Flexbox for aligning items. Let's say instead of even spacing between all elements, we need a little more space around the middle child.
// Flex Example<Flex direction="column"><Flex padding="s">Item 1</Flex><Flex padding="m">Item 2</Flex><Flex padding="s">Item 3</Flex></Flex>
Great! Not too much more work. And if for some reason we needed even more control, we could use our lowest-level Primitive, Box
.
// Box Example<Box><Box paddingTop="l">Item 1</Box><Box paddingY="s">Item 2</Box><Box paddingBottom="l">Item 3</Box></Box>
Elements
Like their chemical counterparts, Elements cannot be broken down into a simpler substance without losing their essential characteristics. They are closely aligned to HTML elements but are not limited to them. While they are still highly flexible, they are more constrained to a particular instance. Some will have particular variants and states, but they typically will not have behaviors. They can be used in a variety of contexts and are only concerned with their own internal attributes. In the LeftNav
example above, these are the Nav
, List
, ListItem
, Icon
, and Link
Elements. You can also extend Elements to create more specific instances. For example, we could have a NavLink
that activates a variant when it's the current route:
// NavLink Exampleconst NavLink = ({ href, variant, ...props }) => {const isActiveLink = window.location.href === href;return (<Link href={href} variant={isActiveLink ? 'active' : variant} {...props} />);};
Compositions
Compositions are our highest-level component abstraction. They can be composed of Primitives, Elements, or, in some cases, other Compositions. While they have distinct parts, they also have associated behaviors. Some example behaviors are: how and where a popup appears and disappears, how a dropdown menu responds to keyboard commands, and how a side panel opens and closes. These behaviors are often connected to states (open, closed, hover, focus, etc) and accessibility attributes. Ideally, all of these behaviors, states, and accessibility attributes are composable as well. This allows us to build dropdown menus to exhibit the some of the same behaviors as our tooltips even though the UI is completely different. You could build entirely new compositions with behaviors from several existing compositions and have them feel cohesive. Let's add a little to our LeftNav example from earlier.
// CollapsibleSideNav.tsximport { useSideNav collapseOnEsc } from './hooks';const CollapsibleSideNav = () => {const {isNavOpen,toggleNav,a11yTargetProps,a11yHeadingProps} = useSideNav();// collapse the nav on escapecollapseOnEsc();return (<SidenavContainer variant={isNavOpen ? 'expanded' : 'collapsed';}><Heading as="h2" variant="h1" {...a11yHeadingProps}>My Blog<IconButton icon="menu" onClick={toggleNav} {...a11yTargetProps} /></Heading><Nav><List><ListItem><NavLink href="/">Home</NavLink></ListItem><ListItem><NavLink href="/about">About</NavLink></ListItem><ListItem><NavLink href="/blog">Blog</NavLink></ListItem><ListItem><NavLink href="/contact">Contact</NavLink></ListItem></List></Nav></Box>);}
Conclusion
The component hierarchy focuses the component's scope of responsibility, creates meaningful relationships between components, encourages composability, and provides balance between flexibility and structure in our component library. This empowers external teams to build and evolve their UI more effectively and allows maintainers to more sustainably support them.
FAQ
“How are Primitives, Elements, and Compositions different from Atomic Design's Atoms, Molecules, and Organisms?
In this hierarchy, Primitives are a lower level than AD's Atoms, though I understand that's subjective. Elements are effectively Atoms and Molecules are Compositions. (Tokens would be subatomic particles, but we can talk about that another time.) I also think anything large enough to be considered an “Organism” is too large to live in a component library. It makes more sense for that to live in a product application.
The distinction between Primitives and Elements is that Elements are one type of thing (Button, Icon, Paragraph, etc), and Primitives have the flexibility to be lots of types of things. If we want to use a cellular analogy: Elements are a single type of cell (skin, brain, blood) and Primitives are closer to stem cells.