Positioning UI components is one of the main disciplines of frontend engineering as part of the overall layout of a page or application.
While in pre-component era this was pretty straight forward using CSS classes for each section and element, it can be quite a challenging task in modern component-based architectures.
To visualize the problem, let's take a simple Button component. It has some styles including 3 different size variants, a click handler and a button text which is passed as children.
In React, it would look something like this:
const variants = { small: 6, medium: 8, big: 10,}
function Button({ children, variant = 'medium', onClick }) { // using inline style here for the sake of simplicity // but this could also be some CSS in JS library or even plain classes const style = { padding: variants[variant], backgroundColor: 'rgb(55, 110, 109)', borderRadius: 5, color: 'white', minWidth: 100, fontSize: 16, margin: 5, border: 0, }
return ( <button style={style} onClick={onClick}> {children} </button> )}
By default, each button has an outer-margin of 5px which is, for example, set by the design language. For most common cases that's just fine, but now comes the hard part.
Imagine at some point you want a button that has a bigger right margin or is aligned at the end of its container.
The hard decision here is whether those styles should belong to the component directly or rather passed to a wrapping container. This problem applies to all properties controlling the layout flow, outer spacings and relative/absolute position.
const Tree = <Button style={{ marginRight: 20 }}>Bigger Margin</Button>
const Tree = ( <div style={{ marginRight: 15 }}> <Button>Bigger Margin</Button> </div>)
Obviously, both ways have different tradeoffs and it might be hard to decide which one to use after all. Let's try to compare both:
The major benefit of the component-based approach is that we don't have to create unnecessary DOM elements. Also all the styles are tied to the component that they actually "belong" to.
Yet, it couples context-sensitive layout information to generic components. That requires style extending logic for every component that can be positioned that way. This can get quite complex, especially when dealing with responsive styles where the same component might be positioned differently for different devices and viewports.
Knowing the pros and cons, we still have to decide which one gives more benefits. To do that, I'd like to add another concept into consideration.
Huh? What does separation of concerns have to do with positioning components?
In my opinion, we're dealing with 2 different concerns hear:
For me, those should not be mixed. Thus, the Button should not know how it is positioned relative to its siblings. That's what it's parent, in that case a wrapping container, should handle.
Therefore, using the container-based approach maintains a clean separation between styling and layout and helps to focus on components themselves. That way, we ensure that components can be reused in any part of the application and also keep their API as simple as possible.
It also enforces a consistent visual appearance as we do not directly alter the components.
Fair enough, adding new DOM nodes is considered a bad practice as it increases the bundle size and might have a performance impact - especially when updating the DOM frequently. But, tools like React help us here. They make sure that DOM nodes only update when their properties actually change.
Using the container-based approach reduces the dependencies and responsibilities of a component to a bare minimum which comes in handy at scale.
To reduce the level of nesting and redundant container components, we can leverage a Spacer component that takes care of spacings between individual siblings.
Rather than having a nested tree like this:
const Tree = ( <div> <div style={{ marginRight: 15 }}> <Button>First</Button> </div> <div style={{ marginRight: 15 }}> <Button>Second</Button> </div> <Button>Last</Button> </div>)
we can do the following:
const Tree = ( <div> <Button>First</Button> <Spacer size={15} /> <Button>Second</Button> <Spacer size={15} /> <Button>Last</Button> </div>)
The Spacer component can either be implemented with absolute sizes or using flexbox basis which makes it direction agnostic.
// requires a direction propconst Spacer = ({ size, vertical }) => ( <div style={{ [vertical ? 'minHeight' : 'minHeight']: size + 'px', }} />)
// automatically horizontal/vertical depending on flex-directionconst Spacer = ({ size }) => ( <div style={{ flexGrow: 0, flexShrink: 0, flexBasis: size + 'px', }} />)
Thanks to my friend Daniel(new tab) for reviewing this post.
Thanks for reading!
I'm Robin, Freelance Engineering Manager from Germany.
Any question regarding this post? Reach out to me on Twitter(new tab)!
You can also find me on GitHub(new tab).
Subscribe to my newsletter and don't miss new posts!