Understanding Component Hierarchy
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 healthy 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
Note: This Composition would likely take a prop like items and map over them to create each
ListItem, but to keep the example straight-forward, I’ve omitted that.
These relationships allow
LeftNav to be an opinionated instance of its structure. However, if we needed to create another instance that allowed
Icons to be paired with the
Links, we could compose the Primitives and Elements instead of modifying this instance to support it. We’re providing flexibility and structure through composability.
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.
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 is an opinionated layout Composition. It exists to help create vertical rhythm (spacing) between elements.
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.
Great! Not too much more work. And if for some reason we needed even more control, we could use our lowest-level Primitive,
Note: This high degree of customization also comes with a responsibility to use them in ways that are harmonious with the rest of the library. You still have the safeguards of using system values, but you should know how to keep UI coherent.
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
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:
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.
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.
“How are Primitives, Elements, and Compositions different from Atomic Design’s Atoms, Molecules, and Organisms?
To me, 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.