<?xml-stylesheet href="/pretty-feed-v2.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Trys Mudford's Blog</title>
    <link>https://www.trysmudford.com/tags/shadow-dom/</link>
    <description>Posts, thoughts, links and photos from Trys</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Wed, 09 Oct 2024 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://www.trysmudford.com/blog/index.xml" rel="self" type="application/rss+xml"/>
    
    <item>
      <title>Hyper-responsive web components</title>
      <link>https://www.trysmudford.com/blog/hyper-responsive-web-components/</link>
      <pubDate>Wed, 09 Oct 2024 00:00:00 +0000</pubDate>
      
      <guid>https://www.trysmudford.com/blog/hyper-responsive-web-components/</guid>
      <description><![CDATA[
<p>Some time ago, I was assigned the task to build a new component: an embeddable call to action to sign up for email alerts. Unbranded, it looked roughly like this:</p>
<p><img src="/images/blog/hyper-alt-1.jpg" alt="A rough wireframe of an email signup form rendered on a wide monitor - on the left is a heading, paragraph, email address field, and a sign up button, and on the right is a presentational illustration."></p>
<p>This component needed to be incredibly portable, looking great on <em>any third-party website</em>, in <em>any position</em>, at <em>any viewport</em>, with <em>any amount of content</em>. It had to be a “hyper-responsive” component.</p>
<p>Three immediate approaches came to mind:</p>
<ol>
<li>A script that inserted HTML into the page</li>
<li>An iframe pointing to a website rendering this component</li>
<li>A web component</li>
</ol>
<p>The script option had the primary drawback of CSS leakage. Not only would page styles easily leak into the component, but styles inserted from the component could also pollute the page.</p>
<p>Iframes solved the style encapsulation challenge; an iframe acts as if it’s a window within a window, but they can’t dynamically resize based on internal content changes. Furthermore, if a <code>&lt;form&gt;</code> <code>POST</code>s from within an iframe, the iframe itself navigates, not the wrapping page. this was a no-go.</p>
<p>Web components have been gradually gathering momentum and browser support over the years. Although my preference would usually be the flavour of a <a href="https://adactio.com/journal/20618">HTML web component</a>, this feature called for the use of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">Shadow DOM</a>. The Shadow DOM handles the style encapsulation we’re after, preventing our styles from leaking out, and (most) page styles from leaking in. Web components can be placed anywhere on the page, like any other HTML element, which makes them incredibly portable and practical. Problem solved.</p>
<h2 id="writing-an-encapsulated-web-component">Writing an encapsulated web component</h2>
<p>You can begin building an encapsulated Web Component in just a few lines of Javascript; with no build system required:</p>
<div class="highlight"><pre class="chroma"><code class="language-jsx" data-lang="jsx"><span class="kr">class</span> <span class="nx">MyWebComponent</span> <span class="kr">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
    <span class="nx">renderCSS</span> <span class="o">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="sb">`&lt;style&gt;
</span><span class="sb">        /* Component CSS */
</span><span class="sb">    &lt;/style&gt;`</span><span class="p">;</span>

    <span class="nx">connectedCallback</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// Opt into the Shadow DOM
</span><span class="c1"></span>        <span class="k">this</span><span class="p">.</span><span class="nx">attachShadow</span><span class="p">({</span> <span class="nx">mode</span><span class="o">:</span> <span class="s1">&#39;open&#39;</span> <span class="p">});</span>

        <span class="c1">// Create a wrapper for the component &amp; add to the Shadow DOM
</span><span class="c1"></span>        <span class="kr">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
        <span class="nx">wrapper</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;class&#39;</span><span class="p">,</span> <span class="s1">&#39;wrapper&#39;</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">);</span>

        <span class="c1">// Render the component&#39;s HTML
</span><span class="c1"></span>        <span class="nx">wrapper</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="sb">`
</span><span class="sb">            </span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">renderCSS</span><span class="p">()</span><span class="si">}</span><span class="sb">
</span><span class="sb">            &lt;!-- Component HTML --&gt;
</span><span class="sb">        `</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="nx">customElements</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;my-web-component&#39;</span><span class="p">,</span> <span class="nx">MyWebComponent</span><span class="p">);</span>
</code></pre></div><p>This can then be inserted into a page by including the script, then adding the HTML <code>&lt;my-web-component&gt;&lt;/my-web-component&gt;</code> wherever you’d like it to render. Because HTML is intrinsically fault-tolerant, any HTML placed between the component tag is automatically rendered if the web component script fails to load.</p>
<p>All the CSS in this web component is encapsulated, meaning nothing from the page will seep <em>into</em> the component, and nothing will leak <em>out</em> from it. Any CSS added to that style tag will only be applied to the elements within the component. By no means is this the perfect way to write CSS, and this could definitely be improved with a build step, but for this use-case, it works pretty well.</p>
<h2 id="responsive-typography--space">Responsive typography &amp; space</h2>
<p>I’m a <a href="https://www.trysmudford.com/categories/web/">huge fan</a> of fluid typography and space, and have co-founded an open source project in this <em>ahem</em> space, called <a href="https://utopia.fyi/">Utopia</a>. We provide a set of tools to help designers and engineers get started with fluid responsive design.</p>
<p>Taking the small and large screen designs for this component, I created a set of <code>clamp()</code> functions to interpolate the typography and space fluidly:</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">.</span><span class="nc">wrapper</span> <span class="p">{</span>
    <span class="nv">--body-size</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mi">1</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">0.9565</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.2174</span><span class="n">vi</span><span class="p">,</span> <span class="mf">1.125</span><span class="kt">rem</span><span class="p">);</span>
    <span class="nv">--heading-size</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mf">1.5</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">1.3261</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.8696</span><span class="n">vi</span><span class="p">,</span> <span class="mi">2</span><span class="kt">rem</span><span class="p">);</span>
    <span class="nv">--item-spacing</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mf">0.5</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">0.3261</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.8696</span><span class="n">vi</span><span class="p">,</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div><p>Applying these to our component is pretty straightforward thanks to the wonder of custom properties:</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">.</span><span class="nc">content</span> <span class="p">{</span>
    <span class="k">display</span><span class="p">:</span> <span class="kc">flex</span><span class="p">;</span>
    <span class="k">flex-direction</span><span class="p">:</span> <span class="kc">column</span><span class="p">;</span>
    <span class="n">gap</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">item</span><span class="o">-</span><span class="n">spacing</span><span class="p">);</span>
    <span class="k">font-size</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">body</span><span class="o">-</span><span class="n">size</span><span class="p">);</span>
<span class="p">}</span>

<span class="nt">h2</span> <span class="p">{</span>
    <span class="k">font-size</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">heading</span><span class="o">-</span><span class="n">size</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div><p>The component looks pretty good on a large and small screen (although the illustration is a bit large and in charge):</p>
<p><img src="/images/blog/hyper-alt-2.jpg" alt="A side-by-side of the component on a large screen (as above), and small screen where the left and right columns are stacked."></p>
<p>But remember, this component needed to be portable, and able to be placed <em>anywhere</em> on a page. So what happens if we render the component in a sidebar and view it on a large screen…</p>
<p><img src="/images/blog/hyper-alt-3.jpg" alt="A broken layout where large text is rendering in a small container."></p>
<p>Yeah… Not ideal. This situation comes about because the fluid typography uses the <code>vi</code> unit. The <code>vi</code>/<code>vw</code> unit is calculated as a proportion of the viewport, not the container. So when we render the component on a large screen, but in a small area, the typography assumes there’s space to play with and explodes out of the component.</p>
<p>Traditionally, we’d create variants at this point, or use Javascript to handle component sizing, but we have another option available to us: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries">CSS container queries</a>!</p>
<p>They’re finally here and stable! Container queries brought along a new length unit: <code>CQI</code>. This unit is calculated as a proportion of the nearest named container; the element opted in with <code>container-type: inline-size</code>. In the same way that <code>1vi</code> === 1% of the viewport, <code>1cqi</code> === 1% of the container.</p>
<p>Using the <code>CQI</code> unit keeps our typography in tune with the component, rather than the whole page. Rendering the component in a column or sidebar therefore tells the typography to shrink down to a size that is appropriate for that space, even if the viewport is much wider.</p>
<p>This is incredibly powerful, but isn’t appropriate for <em>every</em> component. Typography is strongest when it’s in harmony across a page. This provides clear hierarchy for users to help them distinguish what’s important and linked on a page. If everything becomes in tune with itself, nothing is in tune. For this encapsulated component, however, it’s the perfect solution.</p>
<h3 id="applying-container-driven-typography">Applying container-driven typography</h3>
<p>It might be tempting to hot-swap out our custom properties, but it’s always a good idea to build in graceful degradation, particularly when using shiny new CSS features. CSS has the <code>@supports</code> query for this very reason. It’s a way to feature detect and layer on progressive enhancement for browsers that support the feature.</p>
<p>Our wrapper stays as it was, with the inclusion of <code>container-type: inline-size</code>, then we add a <code>@support</code> query to see if the current browser can handle the <code>CQI</code> unit:</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">.</span><span class="nc">wrapper</span> <span class="p">{</span>
    <span class="n">container-type</span><span class="p">:</span> <span class="kc">inline</span><span class="o">-</span><span class="k">size</span><span class="p">;</span>

    <span class="nv">--body-size</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mi">1</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">0.9565</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.2174</span><span class="n">vi</span><span class="p">,</span> <span class="mf">1.125</span><span class="kt">rem</span><span class="p">);</span>
    <span class="nv">--heading-size</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mf">1.5</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">1.3261</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.8696</span><span class="n">vi</span><span class="p">,</span> <span class="mi">2</span><span class="kt">rem</span><span class="p">);</span>
    <span class="nv">--item-spacing</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mf">0.5</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">0.3261</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.8696</span><span class="n">vi</span><span class="p">,</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">@</span><span class="k">support</span> <span class="o">(</span><span class="nt">font-size</span><span class="o">:</span> <span class="nt">1cqi</span><span class="o">)</span> <span class="p">{</span>
    <span class="p">.</span><span class="nc">wrapper</span> <span class="p">{</span>
        <span class="nv">--body-size</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mi">1</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">0.9565</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.2174</span><span class="n">cqi</span><span class="p">,</span> <span class="mf">1.125</span><span class="kt">rem</span><span class="p">);</span>
        <span class="nv">--heading-size</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mf">1.5</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">1.3261</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.8696</span><span class="n">cqi</span><span class="p">,</span> <span class="mi">2</span><span class="kt">rem</span><span class="p">);</span>
        <span class="nv">--item-spacing</span><span class="p">:</span> <span class="nf">clamp</span><span class="p">(</span><span class="mf">0.5</span><span class="kt">rem</span><span class="p">,</span> <span class="mf">0.3261</span><span class="kt">rem</span> <span class="o">+</span> <span class="mf">0.8696</span><span class="n">cqi</span><span class="p">,</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>Better!</p>
<p><img src="/images/blog/hyper-alt-4.jpg" alt="A &lsquo;fixed&rsquo; component where the typography has scaled to fit the space available."></p>
<p><em><strong>Note:</strong> when I said earlier that the web component is totally encapsulated from the page, that wasn’t entirely true. Because a web component does not have a <code>:root</code> element, it calculates any internal <code>REM</code> values from the page <code>:root</code> size. This isn’t normally a problem, but if the page you place the component on has altered the <code>html/:rootfont-size</code>, the component will also be scaled accordingly, for better or worse.</em></p>
<h2 id="intrinsic-layouts">Intrinsic layouts</h2>
<p><a href="https://every-layout.dev/">Every Layout</a> is a fantastic resource that should be in a design engineer’s arsenal. Much like <a href="https://utopia.fyi/">Utopia</a>, Every Layout advocates avoiding imperative media queries, and letting the content and computer decide how best to render a component. <a href="https://every-layout.dev/layouts/sidebar/">‘The Sidebar’</a> is their take on the classic ‘two-column’ problem, where a secondary element gets placed below the primary content when space gets tight. But rather than anchoring to a set viewport width, we can use some clever <code>flex</code> magic to perform the change when the content area itself drops below a certain width.</p>
<p>This intrinsic approach is perfect for not only the overall component layout, but also for the input/button combination:</p>
<p><img src="/images/blog/hyper-alt-6.jpg" alt="Two images of the form, one with the email field and button aside one another, and the other with them stacked."></p>
<p>In this case, the ‘sidebar’ is the button, and the input should expand to take up as much space as is available. When the input gets thinner than our defined minimum width, the elements stack and expand to fill the space.</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">.</span><span class="nc">form</span> <span class="p">{</span>
  <span class="k">display</span><span class="p">:</span> <span class="kc">flex</span><span class="p">;</span>
  <span class="k">flex-wrap</span><span class="p">:</span> <span class="kc">wrap</span><span class="p">;</span>
  <span class="n">gap</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="kc">space</span><span class="mi">-2</span><span class="n">xs</span><span class="p">)</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="kc">space</span><span class="o">-</span><span class="n">xs</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">.</span><span class="nc">form</span> <span class="o">&gt;</span> <span class="nt">button</span> <span class="p">{</span>
  <span class="k">flex-basis</span><span class="p">:</span> <span class="mi">11</span><span class="kt">rem</span><span class="p">;</span> 
  <span class="k">flex-grow</span><span class="p">:</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>

<span class="p">.</span><span class="nc">form</span> <span class="o">&gt;</span> <span class="nt">input</span> <span class="p">{</span>
  <span class="k">flex-basis</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
  <span class="k">flex-grow</span><span class="p">:</span> <span class="mi">999</span><span class="p">;</span>
  <span class="n">min-inline-size</span><span class="p">:</span> <span class="mi">60</span><span class="kt">%</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><h3 id="limitations-of-intrinsic-design">Limitations of intrinsic design</h3>
<p>One limitation of ‘The Sidebar’ approach is the ability to hide content when the element reaches a certain size. We may wish to hide illustrative or presentational elements on smaller screens and only show them when there is the real-estate available.</p>
<p>To handle this, we need to use container queries themselves. Once again, we begin with a usable baseline and layer on fidelity for browsers that support it. ‘The Sidebar’ uses <code>display: flex</code>, a feature that’s very well supported. And although it’s not ideal to show the illustration on smaller devices, it doesn’t look broken, and the feature still works, so this is an acceptable trade-off.</p>
<p>Using a <code>@supports (container-type: inline-size)</code> query, we can convert the wrapper from <code>flex</code> to <code>grid</code>, effectively nullifying ‘The Sidebar’ for browsers that support container queries. Then we can write a targeted container query to hide the illustration column when the component is smaller than our desired size:</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">.</span><span class="nc">layout</span> <span class="p">{</span>
  <span class="k">display</span><span class="p">:</span> <span class="kc">flex</span><span class="p">;</span>
  <span class="k">flex-wrap</span><span class="p">:</span> <span class="kc">wrap</span><span class="p">;</span>
  <span class="n">gap</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="kc">space</span><span class="o">-</span><span class="n">s</span><span class="p">)</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="kc">space</span><span class="o">-</span><span class="n">xl</span><span class="mi">-2</span><span class="n">xl</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">.</span><span class="nc">illustration</span> <span class="p">{</span>
  <span class="k">flex-basis</span><span class="p">:</span> <span class="mi">18</span><span class="kt">rem</span><span class="p">;</span> 
  <span class="k">flex-grow</span><span class="p">:</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>

<span class="p">.</span><span class="nc">primary</span> <span class="p">{</span>
  <span class="k">flex-basis</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
  <span class="k">flex-grow</span><span class="p">:</span> <span class="mi">999</span><span class="p">;</span>
  <span class="n">min-inline-size</span><span class="p">:</span> <span class="mi">50</span><span class="kt">%</span><span class="p">;</span>
<span class="p">}</span>

<span class="p">@</span><span class="k">supports</span> <span class="o">(</span><span class="nt">container-type</span><span class="o">:</span> <span class="nt">inline-size</span><span class="o">)</span> <span class="p">{</span>
    <span class="p">.</span><span class="nc">layout</span> <span class="p">{</span>
        <span class="k">display</span><span class="p">:</span> <span class="k">grid</span><span class="p">;</span>
        <span class="k">grid-template-columns</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="p">@</span><span class="k">container</span> <span class="o">(</span><span class="nt">max-width</span><span class="o">:</span> <span class="nt">600px</span><span class="o">)</span> <span class="p">{</span>
        <span class="p">.</span><span class="nc">illustration</span> <span class="p">{</span>
            <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p><img src="/images/blog/hyper-alt-5.jpg" alt="A fully &lsquo;fixed&rsquo; component where the typography has scaled to fit the space available and illustration is hidden."></p>
<h2 id="the-finer-details">The finer details</h2>
<p>For a couple of finishing touches, we can use the reasonably new <code>text-wrap: balance</code> on headings within the content. When the width of the element forces the heading text onto multiple lines, CSS will aim to ‘balance’ the text across the available space. This leads to fewer <a href="https://fonts.google.com/knowledge/glossary/widows_orphans">orphans</a> hanging on the final line, improving the aesthetic quality and readability of the heading.</p>
<p>Finally, we can also use the <code>ch</code> unit to limit text width to ensure lines remain readable. This unit maps to the width of the “0” glyph in the rendered typeface, and provides a good way for us to keep line lengths under control based on the current font-size.</p>
]]>
      </description>
    </item>
    
  </channel>
</rss>