The usefulness of this articled is lessened because it's React-specific and React is bad with DOM and style isolation in general.
The base problem here is that both a component and its host may want to style the component. The component model should account for this and offer some guidance.
Web components do this with the `:host` selector that styles the component from within its shadow root. The styles applied with the `:host` selector can be overridden by the outside styles, without the component needing to weaken encapsulation by allowing styling from the outside via JS properties. This means it's not really bad for a component to give itself default outside styling like block vs inline display, or even margins, because the user can always reset them as needed, much like built-in elements.
For instance, a web component might have these styles:
<my-element>
#shadow-root
:host {
margin: 1em;
}
And at its use-site, the margins can be set differently:
Shadow DOM also helps with specificity fights, as the cascade goes: shadow style -> light style -> light !important -> shadow !important. This way a component can define which styles are only defaults.
Edit to clarify: one of the big problems with the article is the use of inline styles. They have the highest specificity and there's no built-in way to "merge" inline styles like you normally get via inheritance and the cascade.
> Web components do this with the `:host` selector that styles the component from within its shadow root. The styles applied with the `:host` selector can be overridden by the outside styles, without the component needing to weaken encapsulation by allowing styling from the outside via JS properties.
I don't see how the React approach of accepting styles as JS props is any weaker in terms of encapsulation compared to your example above. If anything, it offers the opportunity for stronger encapsulation because it supports limiting/transforming the styling props we accept through those JS props, and even allows us to define a styling API for our component that's completely independent of CSS semantics, enabling components APIs that can span multiple platforms with independent implementations.
You're right. If we're talking about only the root element, it is similar to :host, because the custom element is itself stylable from the outside.
What makes me say that encapsulation is weakened is that threading JS property bags to other internal elements is also relatively common, and inline styles don't really offer any encapsulation there. Other selectors in the page can still select and style properties that are not directly styled by the element. I should have clarified that.
Shadow DOM provides true runtime style encapsulation, and the selectors on the page can't select the internal elements at all.
Ah I see, that makes sense. Shadow DOM definitely offers much stronger style encapsulation than a React component where you can only "enforce" that components shouldn't reach into it to style internals by convention.
That's not stylable from CSS though, so you have to pass JS props and can't use selectors. It also duplicates styling strings across all similar style attributes which is not good for memory or perf.
A fundamental problem of building isolated components in HTML/CSS is that the CSS `display` property sets both an element's inner layout (how it lays out its children) and its outer layout (how it is laid inside it parent).
E.g. you have to make an element "display: inline-flex" to make it an inline flexbox. You can't just make it "flex" and have the parent decide whether it is inline, a block or whatever.
The consequence of this is that a parent component cannot properly layout a child component without knowing its internal layout structure. And if it doesn't don't know the child's layout (if its dynamically supplied for example) then it can't safely do anything with it. So long as this limitation exists, there will never be true layout encapsulation on the web platform.
For a while, the CSS Working Group planned to address this problem through the introduction of separate `display-inside` and `display-outside` properties. This would mean an element could specify its internal layout using `display-inside` while its parent could control its external layout using `display-outside`. Then, in their wisdom, they dropped it from the spec[1].
I mean, it's not like developing with encapsulated components is a popular approach on the web nowadays. It's not like many people are using React, or Angular, or Vue, or Web Components—the W3C's own goddamn multi-year effort to push component-based development—so why bother wasting time adding features that would help?
Your vitriol towards CSSWG is misplaced. The CSSWG didn't remove the functionality of `display-inside` and `display-outside`, they just made `display` multi-valued so you can write `display: inline grid`.
Child selectors (including eg "adjacent siblings") can solve the hardest part of this problem. Some layouts require a wrapper element, but that is just a minor inconvenience, doesn't have to be a blocker. See https://every-layout.dev
While I agree with all of the problem statements in this article, I don't quite agree with the proposed solution of just exposing style props for parents to pass through arbitrary values, at least not as a general solution (it could still be the appropriate solution for certain special cases).
I think this approach actually results in _less_ useful component isolation in that you can no longer reason about layout of the component _itself_ in isolation, because it can be much more difficult, sometimes impossible, to predict how the styles in the component itself will interact with the styles passed through by the parent.
Instead, I'd recommend always using plain block elements with no explicit height/width/flex properties for the outermost bounding box of your reusable components, so parents can _indirectly_ control how they're laid out using flexbox/grid and add containers that constrain the available space for them to render in and add flex properties if necessary.
Totally agree with the suggestion of using spacing components over margin though. We called this the "golden rule of components" in that reusable components should be entirely responsible for rendering everything within its outermost bounding box (outside of delegating certain parts/properties of it through explicit APIs), and should render nothing outside of it (including empty space, i.e. margins). Of course, edge cases exist where it's necessary to break away from this guideline, but I feel it's a great general rule of thumb for creating flexible reusable components.
reusable components [...] should render nothing outside of it (including empty space, i.e. margins).
Could collapsing margins be an exception to this rule, in principle?
I’ve mostly worked in Android, iOS and React Native, none of which have collapsing margins. But I’ve often wished they had, as it would make some layouts a lot simpler! It would be great to just smoosh components together and have them agree between themselves how much space is needed.
HTML has collapsing margins but they seem kind of half-baked. Is there enough functionality there to be useful, or is it best avoided entirely?
> HTML has collapsing margins but they seem kind of half-baked. Is there enough functionality there to be useful, or is it best avoided entirely?
Generally I'd recommend avoiding them entirely, as the rules for when margins should collapse are nuanced and sometimes applied inconsistently between browsers.
It just adds too much cognitive overhead to reasoning about your layouts to be worthwhile imo. Whereas with spacing components, what you see in the virtual dom is what you'll get.
OP here. Totally agree passing arbitrary styles down in props is not a great solution and makes to incredibly hard to reason about components. We defined explicitly using typescript the styles which can be set from the parent which makes it much easier to reason about and we can even use the "find all references" feature of VSCode to identify usages.
@media queries based on browser width are especially bad. Unfortunately there is no css way for stylings dependent on the width of the _component_ itself.
It's not CSS based, but I've had reasonable success with ResizeObserver based component size queries, at least in the context of small components that don't accept other nodes to render.
I've found this hook (https://github.com/react-spring/react-use-measure) to be reasonably robust, as long as you only use the size properties and don't rely on the accuracy of the positioning properties, which is not something that ResizeObserver is designed to handle (it can get out of date when the component moves due to dynamic content appearing/disappearing around it without actually resizing: https://github.com/react-spring/react-use-measure/issues/9).
It remains to be seen how this could scale when applied to larger "container" type components and the entire app though, as I imagine cascading re-renders as higher level elements resize could become an issue. Curious if people have experience with using ResizeObserver for component size queries at scale?
Focusing on window-based instead of component-based responsive queries was a mistake (or at least, it has overstayed its welcome). CSS Grid actually has some component-based responsiveness with auto-fit/auto-fill and minmax, although it's fairly limited.
I really like the idea proposed here of <Spacer /> components. I always thought it was weird to assign a margin to elements of a list to achieve spacing. To maintain proper spacing it's often best to either make the top and bottom margins of each element equal, or remove the top/bottom margin from the first/last element. Both techniques come with complications.
Abstracting that out makes it easier to achieve a commonly used layout. Not quite sure the best way to implement this, but I'll definitely be giving the idea a shot.
> Layout-isolated component - A component that is unaffected by the parent it is placed within, and does not itself affect the size and position of its siblings.
I'm honestly still surprised that this isn't the default. Beyond maintainability, it's an obvious security problem, which clickjacking made apparent years ago.
The security implications of user interface API apparently isn't well understood outside the capability security community though:
> Remember when we were building our apps as a set of screens and pages instead of thinking in components?
If pages were components in the browser, there wouldn't be any difference. Xanadu had something like this way back in the early 90s, ie. page transclusion.
I saw it more as "purely functional", where components are the functions and styling outside of the component is a side-effect. Avoiding side-effects like margin or align-self makes components more like pure functions than objects IMO.
Margins and align-self aren’t side-effects. Their semantics are consistent and reproducible and depend only on the context a component is placed in, just as a property like width does.
They’re awkward because they have bigger knock-on effects on the overall layout of components within the parent.
In an OO mindset, I see this as being all about what interfaces your object exposes. For something like a button or slider control, it obviously exposes an action callback of some kind. But it also exposes a generic “child component” interface, which is the only thing a generic parent component cares about.
What are useful things and what are obnoxious things for a child component to do? Flexibly adapting itself to a range of sizes is useful. Demanding that it should be centered is obnoxious. In fact it would be hard to express at all in most OO widget toolkits. So I think this problem is very closely related to good OO design.
Their semantics might be consistent, but they sort of leak into the parent. Height and width are local to what the component renders, and they don't depend on the parent (as long as they aren't relative sizes). But things like margin and align-self change how the parent renders all of its children.
In other words, the view rendered by the component is a function of properties like height and width. But when you throw in margin or align-self, the component now depends on its siblings.
The OO perspective is interesting — I agree that it effectively makes these things hard to express. I now think that it's more of a problem relating to encapsulation and isolation. OO would isolate it through a child component interface while FP would have a function that can't affect siblings.
It's crucial to account properly for relationships between components. The best approach to this I've encountered in over 20 years of web-related experience is found at https://every-layout.dev. Compose complex, responsive layouts from expertly crafted primitives, using axiomatic CSS, and positioning becomes a breeze. The non-layout-related styling can be handled however you like; my preference is emotion for component-scoped styles (`sx` prop and styledcomponent API FTW), and design tokens for colors and typographic scale. I've been meaning to write up my approach bc it's exciting to be freed (and free others) from fighting endless css battles.
You can use 'contain' CSS properties to guarantee that layout inside the element won't affect anything outside it and vice versa. 'contain: strict' is basically a bulletproof container for arbitrary content, with performance benefits too. I'm not sure if it covers everything the article was trying to achieve, but it seems surprising not to mention it.
The base problem here is that both a component and its host may want to style the component. The component model should account for this and offer some guidance.
Web components do this with the `:host` selector that styles the component from within its shadow root. The styles applied with the `:host` selector can be overridden by the outside styles, without the component needing to weaken encapsulation by allowing styling from the outside via JS properties. This means it's not really bad for a component to give itself default outside styling like block vs inline display, or even margins, because the user can always reset them as needed, much like built-in elements.
For instance, a web component might have these styles:
And at its use-site, the margins can be set differently: Shadow DOM also helps with specificity fights, as the cascade goes: shadow style -> light style -> light !important -> shadow !important. This way a component can define which styles are only defaults.Edit to clarify: one of the big problems with the article is the use of inline styles. They have the highest specificity and there's no built-in way to "merge" inline styles like you normally get via inheritance and the cascade.