
Layout-Isolated Components - emilsjolander
https://visly.app/blog/layout-isolated-components
======
spankalee
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:

    
    
        <style>
          my-element {
            margin-botton: 0;
          }
        </style>
        <h1>...</h1>
        <my-element></my-element>
        <div>...</div>
    

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.

~~~
ss3000
> 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.

~~~
spankalee
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.

~~~
ss3000
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.

------
stupidcar
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?

[1] [https://drafts.csswg.org/css-display/#changes-
wd](https://drafts.csswg.org/css-display/#changes-wd)

~~~
spankalee
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`.

[https://developer.mozilla.org/en-
US/docs/Web/CSS/display](https://developer.mozilla.org/en-
US/docs/Web/CSS/display) [https://developer.mozilla.org/en-
US/docs/Web/CSS/display-ins...](https://developer.mozilla.org/en-
US/docs/Web/CSS/display-inside)

------
lewisl9029
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.

~~~
iainmerrick
_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?

~~~
lewisl9029
> 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.

[https://developer.mozilla.org/en-
US/docs/Web/CSS/CSS_Box_Mod...](https://developer.mozilla.org/en-
US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing)

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.

------
fabb64
@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.

~~~
lewisl9029
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](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](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?

------
andrewrothman
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.

[https://seek-oss.github.io/braid-design-
system/components/St...](https://seek-oss.github.io/braid-design-
system/components/Stack/)

[https://github.com/seek-oss/braid-design-
system/blob/master/...](https://github.com/seek-oss/braid-design-
system/blob/master/lib/components/Stack/Stack.tsx)

------
naasking
> 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:

Design of the EROS Trusted Window System,
[http://srl.cs.jhu.edu/courses/600.439/shap04windowsystem.pdf](http://srl.cs.jhu.edu/courses/600.439/shap04windowsystem.pdf)

> 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.

------
iainmerrick
Or, “Object-oriented”

 _Remember when we were building our apps as a set of screens and pages
instead of thinking in components?_

Sure, but I also remember building web apps in GWT, using a toolkit of
reusable widgets, with an OO interface.

 _Edit to add:_ I guess I’m more agreeing with the author than arguing with
them; I just find it amusing how what goes around comes around.

~~~
kbr
How does it become any more object-oriented?

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.

~~~
iainmerrick
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.

~~~
kbr
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.

~~~
iainmerrick
Yeah, I think we’re mostly agreeing! Encapsulation and isolation are the key
bits, and both FP and OO suggest ways to achieve them.

------
chrisweekly
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](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.

~~~
tln
I hope you do write it up!

Here's a working link:

[https://every-layout.dev/](https://every-layout.dev/)

~~~
chrisweekly
Thanks! (fixed link)

------
aero142
So, the article talks about what not to do. Are there good examples of layout
isolated components inside a complex UI?

~~~
AshleysBrain
We make a browser-based game development IDE (www.construct.net), and IMO an
important aspect of this is CSS containment:
[https://developer.mozilla.org/en-
US/docs/Web/CSS/CSS_Contain...](https://developer.mozilla.org/en-
US/docs/Web/CSS/CSS_Containment)

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.

------
tenaciousDaniel
It isn't entirely true that padding is safe in terms of layout isolation.

~~~
emilsjolander
Correct :) But it generally is and I didn't wan't to go all too deep

~~~
Izkata
Not much need to go in a lot of depth, box-sizing:border-box simply makes it
so.

------
jbverschoor
So.... non-responsive?

