David GossI'm a software developer based in the UK.2022-02-17T00:00:00Zhttps://davidgoss.coDavid Gossdavid@davidgoss.coReact apps as Chrome extensions2022-02-17T00:00:00Zhttps://davidgoss.co/blog/react-apps-as-chrome-extensions/<p>Recently, I had a reason to build a Chrome extension for the first time <a href="https://davidjgoss.github.io/inspector-clouseau/">in years</a>. Since it needed to have some non-trivial UI, I wanted to build it with a modern framework like React. Chrome extensions (all browser extensions really — <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions">they share a standard now</a>) are built with the core web platform pillars of HTML, CSS and JavaScript, meaning you can basically implement them however you want, but there are some quirks compared to regular web apps.</p>
<p>The most obvious quirk is that there’s no server delivering your app; a Chrome extension’s code lives and runs entirely on the client device. This frees you from many of today’s concerns associated with the latency, slowness, volatility and expense of delivering lots of code over the network. But these concerns also drive a lot of the built-in behaviour of popular “meta-frameworks” like Next.js and Gatsby — server-side rendering, lazy loading — that generally make these tools great choices for a web app. Sensing I might spend a lot of time fighting the defaults, I chose <a href="https://create-react-app.dev/">Create React App</a> instead.</p>
<p>Here’s how to get off the ground with the standard template (TypeScript flavour):</p>
<pre class="language-shell"><code class="language-shell">npx create-react-app my-chrome-extension --template typescript<br /><span class="token builtin class-name">cd</span> my-chrome-extension<br /><span class="token function">npm</span> run build</code></pre>
<p>Now you have a React app in the <code>build</code> directory and can run it locally with <code>npm start</code>.</p>
<h2>Getting to Hello World</h2>
<p>There are <a href="https://developer.chrome.com/docs/extensions/mv3/architecture-overview/">many aspects you can have in a Chrome extension</a>. Here, we’ll focus on the popup - this is the actual UI that’ll be shown when a user clicks your extension icon. To tell Chrome about the popup, you’ll need to declare it in the <code>manifest.json</code>.</p>
<pre class="language-json"><code class="language-json"><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"my-chrome-extension"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"It does something cool!"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"0.0.1"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"manifest_version"</span><span class="token operator">:</span> <span class="token number">3</span><span class="token punctuation">,</span></span><br /><mark class="highlight-line highlight-line-active"> <span class="token property">"action"</span><span class="token operator">:</span> <span class="token punctuation">{</span></mark><br /><mark class="highlight-line highlight-line-active"> <span class="token property">"default_popup"</span><span class="token operator">:</span> <span class="token string">"build/index.html"</span><span class="token punctuation">,</span></mark><br /><mark class="highlight-line highlight-line-active"> <span class="token property">"default_title"</span><span class="token operator">:</span> <span class="token string">"My Chrome Extension"</span></mark><br /><mark class="highlight-line highlight-line-active"> <span class="token punctuation">}</span></mark><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>With that done, you can <a href="https://developer.chrome.com/docs/extensions/mv3/getstarted/#unpacked">load the extension in Developer Mode</a> and hit the icon to show the popup, which shows…a small white square. This doesn’t look very promising, but it’s only a couple of tweaks away from working.</p>
<p>When doing a production build, <code>react-scripts</code> inlines some JavaScript in the <code>index.html</code> that helps bootstrap the app faster. However, Chrome extension popups have a special <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">content security policy</a> that disallows inline scripts, so your React app is shut down before it can even get going. Fortunately, this optimisation <a href="https://create-react-app.dev/docs/advanced-configuration/">can be turned off</a>:</p>
<pre class="language-json"><code class="language-json"><span class="highlight-line"><span class="token comment">// package.json</span></span><br /><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"react-scripts start"</span><span class="token punctuation">,</span></span><br /><del class="highlight-line highlight-line-remove"> <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"react-scripts build"</span><span class="token punctuation">,</span></del><br /><ins class="highlight-line highlight-line-add"> <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"INLINE_RUNTIME_CHUNK=false react-scripts build"</span><span class="token punctuation">,</span></ins><br /><span class="highlight-line"> <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"react-scripts test"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"eject"</span><span class="token operator">:</span> <span class="token string">"react-scripts eject"</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>The other issue is with the paths to the JS and CSS files which, if you check the console for the extension popup, you’ll see are not being found. Like most modern frameworks, <code>react-scripts</code> outputs root-based paths for style and script imports (e.g. <code>/static/js/main.1228eada.js</code>), which is a smart default based on various assumptions including a web server, client-side routing and distinct “pages”, none of which are applicable to this context. Again, there’s a handy escape hatch, which outputs relative paths instead (e.g. <code>static/js/main.1228eada.js</code>):</p>
<pre class="language-json"><code class="language-json"><span class="highlight-line"><span class="token comment">// package.json</span></span><br /><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"private"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span></span><br /><ins class="highlight-line highlight-line-add"> <span class="token property">"homepage"</span><span class="token operator">:</span> <span class="token string">"."</span><span class="token punctuation">,</span></ins><br /><span class="highlight-line"> <span class="token property">"dependencies"</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>As you might expect, this has implications if you actually <em>want</em> to add routing; we’ll get to that later. Anyway, if you try the extension icon again now, you’ll see the coveted React starter app in there.</p>
<h2>Using Chrome APIs</h2>
<p>At some point, you’ll want your popup to interact with other parts of your extension, which is done with <a href="https://developer.chrome.com/docs/extensions/mv3/messaging/">message passing</a>. To make this easy to work with, you can isolate your calls to Chrome APIs in targeted helper functions that live outside your components and hooks:</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">getThing</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NODE_ENV</span> <span class="token operator">===</span> <span class="token string">'production'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name"><span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token operator">></span></span><span class="token punctuation">(</span><span class="token punctuation">(</span>resolve<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> chrome<span class="token punctuation">.</span>runtime<span class="token punctuation">.</span><span class="token function">sendMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> type<span class="token operator">:</span> <span class="token string">'THING'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>response<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">resolve</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token builtin">Promise</span><span class="token punctuation">.</span><span class="token function">resolve</span><span class="token punctuation">(</span><span class="token string">'Example thing!'</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>The conditional on <code>NODE_ENV === 'production'</code> means that when your app has been built and is running in the extension, it’ll call the Chrome API, and when developing locally with <code>npm start</code> it’ll hit your stub instead. Helpfully, the production build <a href="https://create-react-app.dev/docs/adding-custom-environment-variables/">will strip out the condition</a> meaning your development-only stubs won’t end up in the extension bundle.</p>
<p>Having the interactions with Chrome isolated like this also means you can mock them when writing tests for your app using <a href="https://jestjs.io/docs/mock-functions#mocking-modules"><code>jest.mock</code></a>.</p>
<h2>Routing</h2>
<p>As your extension’s functionality grows, you might want to spread the UI out across multiple views, and instinctively start setting up routing in your app. Here’s a refresher on how routing generally works on single-page apps:</p>
<ul>
<li>You have one <code>index.html</code> which loads the app</li>
<li>The web server will serve up that one <code>index.html</code> regardless of the path in the URL</li>
<li>The app uses the path as the source of truth for which components to render</li>
<li>The app will unmount and rerender as needed when the path changes (e.g. a link is clicked) without a window-level navigation</li>
</ul>
<p>Not for the first time, this isn’t really applicable to the Chrome extension context. There’s no web server, and the URL of your popup will be something like:</p>
<pre><code>chrome-extension://nomnigokoajgkahpaabcdandnpeijbjp/build/index.html
</code></pre>
<p>This doesn’t seem like something we should be messing with or relying on. However, we can still get the benefit from our familiar tools and patterns by using <a href="https://v5.reactrouter.com/web/api/HashRouter">the “hash” flavour of routing</a>, which only uses the hash portion of the URL and leaves the actual path alone.</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> <span class="token punctuation">{</span><br /> HashRouter <span class="token keyword">as</span> Router<span class="token punctuation">,</span><br /> Route<span class="token punctuation">,</span><br /> Routes<br /><span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'react-router-dom'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Router</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">className</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>App<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Routes</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Route</span></span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/<span class="token punctuation">"</span></span> <span class="token attr-name">element</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Main</span></span><span class="token punctuation">/></span></span><span class="token punctuation">}</span></span><span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Route</span></span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/about<span class="token punctuation">"</span></span> <span class="token attr-name">element</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">About</span></span><span class="token punctuation">/></span></span><span class="token punctuation">}</span></span><span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Routes</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Router</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>This works nicely, although it’s worth bearing in mind that this flavour of routing is considered legacy — originating from before browsers widely supported the <a href="https://developer.mozilla.org/en-US/docs/Web/API/History">History API</a> — and could fall out of support at some point.</p>
<h2>Packaging</h2>
<p>To get your extension uploaded to the Chrome store, you’ll need have it in a zip file, so it makes sense to automate this. Since you’re not doing anything too complex, you can handle it in a simple npm script with the help of <a href="https://www.npmjs.com/package/bestzip">bestzip</a>:</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">npm</span> <span class="token function">install</span> --save-dev bestzip </code></pre>
<pre class="language-json"><code class="language-json"><span class="highlight-line"><span class="token comment">// package.json</span></span><br /><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"react-scripts start"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"INLINE_RUNTIME_CHUNK=false react-scripts build"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"react-scripts test"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"eject"</span><span class="token operator">:</span> <span class="token string">"react-scripts eject"</span><span class="token punctuation">,</span></span><br /><ins class="highlight-line highlight-line-add"> <span class="token property">"zip"</span><span class="token operator">:</span> <span class="token string">"bestzip extension.zip assets/* build/* background.js content.js manifest.json"</span></ins><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<h2>Wrapping up</h2>
<p>That’s it! There’s <em>tons</em> more to extensions than I’ve touched on here, but hopefully this’ll help you work React into the places you want it when you build your next one.</p>
<p>My code and configuration examples above are abridged in places, so I put <a href="https://github.com/davidjgoss/chrome-extension-cra-example">an example project on GitHub</a> that fully demonstrates the approach.</p>
On Air in the home office2021-12-06T00:00:00Zhttps://davidgoss.co/blog/on-air-in-the-home-office/<p>Since the pandemic started, I’ve gone all in on working from home. I quit my job at a local software company for a fully remote role, and built<sup class="footnote-ref"><a href="https://davidgoss.co/blog/on-air-in-the-home-office/#fn1" id="fnref1">[1]</a></sup> a wooden cabin in my garden to work from. Being at home and mostly accessible to my family is great, but sometimes I need a way to signal to them that I’m on a call and ideally shouldn’t be disturbed.</p>
<p>The most obvious analogy is the “ON AIR” light box seen at radio and TV studios, designed to stop people unwittingly disrupting a live broadcast. This kind of thing:</p>
<figure><img src="https://davidgoss.co/blog/images/on-air-lightbox.jpg" alt="" /><figcaption>Photo by Samuel Regan-Asante</figcaption></figure>
<p>Amazon is filled with cheap (and they look it) remote-controlled versions of this idea. There are some <a href="https://www.etsy.com/uk/listing/1051415857/on-air-streaming-led-display-twitch">more interesting</a> interpretations on Etsy, but I need something that works outside, so that narrows the field a lot. What I <em>do</em> have is a light fitting on the side of the cabin that takes a regular screw bulb, which opens it up to the smorgasbord that is the smart bulbs market.</p>
<p>When I last looked at the home automation scene a few years ago, I decided it was a bit of an enthusiast’s pursuit - i.e. requiring lots of time and money and still mostly turning out a bit crap. Today, though, there’s a lot you can do quite cheaply and easily.</p>
<p>After a bit of searching I wound up getting a <a href="https://www.amazon.co.uk/gp/product/B0753SGLCC/ref=ppx_yo_dt_b_search_asin_title">LIFX Mini LED bulb</a> - it’s not expensive, works with Apple HomeKit and crucially is compact enough to fit in the light housing on the outside of my cabin (most bulbs are a little to long).</p>
<p>Being a light that supports any colour and brightness combination, it’s useful to me in other ways too - it can be a security feature at night, and give me some nice soft lighting outside in the evening.</p>
<figure><img src="https://davidgoss.co/blog/images/on-air-bulb-red.jpg" alt="" /><figcaption>The LIFX bulb, lit up red when I’m busy</figcaption></figure>
<p>The bulb is in my Apple HomeKit setup, which is easily done by scanning a code (it supports Alexa and Google Home as well; most smart bulbs seem to). What I want is a simple toggle to pop it on and off, ideally from my phone’s home screen. Enter <a href="https://support.apple.com/en-gb/guide/shortcuts/welcome/ios">Shortcuts</a>, Apple’s friendly framework for automating home devices.</p>
<p>I have a “Busy/Free” shortcut which toggles the light. It’s a bit less simple than it should be in practise because there’s no way to toggle a device, so it has to check the current status with if/else logic and act accordingly. The shortcut also fires a notification, just to give me some feedback that it worked in case I can’t see the light from where I’m sitting.</p>
<figure><img src="https://davidgoss.co/blog/images/on-air-screenshots.png" alt="" /><figcaption>Setup in Home and Shortcuts</figcaption></figure>
<p>The logical next step down the automation rabbit hole would be to toggle the light automatically when a call starts or ends, which I could probably do via <a href="https://ifttt.com/">IFTTT</a> and its integration with Zoom. I don’t really want or need that complexity though; sometimes a simple on/off switch is perfect.</p>
<p>Stray observations:</p>
<ul>
<li>Shortcuts works on the Mac too, so I can pin this switch to the menu bar, which is a nice touch.</li>
<li>I’ll be interested to see how the new <a href="https://www.theverge.com/22726456/ios-15-iphone-focus-distractions-how-to">Focus modes</a> feature develops, and whether home scenes could become a factor in there.</li>
<li><a href="https://github.com/sindresorhus/Actions">Actions</a> gives you lots of extra things to do in Shortcuts (and it’s free).</li>
</ul>
<hr class="footnotes-sep" />
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Okay, <em>I</em> didn’t build it. Some nice people came and built it for me. <a href="https://davidgoss.co/blog/on-air-in-the-home-office/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
Adaptive headlines with CSS functions2021-01-01T00:00:00Zhttps://davidgoss.co/blog/adaptive-headlines-with-css-functions/<p>Like many blogs, this site is basically just text, so options for making it visually interesting are mostly limited to typography. I quite like using big, bold headlines for this, and think the effect at <code>3.5rem</code> on a large display is pretty nice.</p>
<p>However, that aspect of the design doesn’t work very well on a small display, and is just plain <em>bad</em> when the headline is long and/or includes long words:</p>
<figure><img src="https://davidgoss.co/blog/images/css-min-max-before-screenshots.png" alt="" /><figcaption>Screenshots, in portrait and landscape, of an article with a long headline in an iPhone-sized viewport</figcaption></figure>
<p>I really want at least the first paragraph of content to be readable on a small display without having to scroll. Media queries could help, but in cases like this they’re a bit of a blunt instrument. I’d need to set one or more breakpoints at which the size would increase, accounting for both width and height, and there’d still be some sizes at which it wasn’t quite right.</p>
<p>CSS has other tools available to us in the form of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions">functions</a>, where we can compute values based on various other values, including the current font size and the viewport width and height.</p>
<p>After some experimentation, here’s what I have now:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">h1</span> <span class="token punctuation">{</span><br /> <span class="token property">font-size</span><span class="token punctuation">:</span> 2rem<span class="token punctuation">;</span><br /> <span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token function">clamp</span><span class="token punctuation">(</span>2rem<span class="token punctuation">,</span> <span class="token function">min</span><span class="token punctuation">(</span>10vw<span class="token punctuation">,</span> 10vh<span class="token punctuation">)</span><span class="token punctuation">,</span> 3.5rem<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token property">line-height</span><span class="token punctuation">:</span> 1em<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>The <code>2rem</code> declaration is in as a safe fallback for older browsers; if the syntax of the next declaration isn’t recognised, it’ll be ignored and the <code>2rem</code> will stand.</p>
<p>The next declaration using a combination of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clamp()"><code>clamp()</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/min()"><code>min()</code></a> breaks down like this:</p>
<blockquote>
<p>Use whichever is the smaller of 10% of the viewport width or 10% of the viewport height<sup class="footnote-ref"><a href="https://davidgoss.co/blog/adaptive-headlines-with-css-functions/#fn1" id="fnref1">[1]</a></sup>, but prevent it from being any smaller than 2x the root font size or any larger than 3.5x the root font size.</p>
</blockquote>
<p>Finally, a <code>line-height</code> of <code>1em</code> provides an appropriate leading based on the computed font size.</p>
<hr />
<p><code>clamp()</code>, <code>min()</code> and other similar functions are things you can’t do at build time with preprocessors, because the values are evaluated at runtime.</p>
<p>My example isn’t anything special, but I think the ability to nest functions, as well as their support for mixing units as I have done here, makes them very powerful for achieving precision in contexts that are both complex and dynamic, and adaptiveness in ways that aren’t tied to specific pixel breakpoints. This is especially true of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/calc()"><code>calc()</code></a>, which (as I sometimes forget) is supported all the way back to Internet Explorer 9.</p>
<p>Stray observations:</p>
<ul>
<li>As you might expect, we have <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/max()"><code>max()</code></a> to go with <code>min()</code></li>
<li>Though most examples show them used with two values, <code>min()</code> and <code>max()</code> can take as many different values as you’d like</li>
<li>Take care with <code>vh</code>; things like the disappearing browser chrome on iOS Safari can make it behave in unexpected ways, so I tend to avoid it for layout - but it’s great for things like this</li>
</ul>
<hr class="footnotes-sep" />
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>The 10% number just came out of trial and error. <a href="https://davidgoss.co/blog/adaptive-headlines-with-css-functions/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
Richer API documentation with Redoc and Docusaurus2022-08-03T00:00:00Zhttps://davidgoss.co/blog/api-documentation-redoc-docusaurus/<p>I’ve been focusing a lot on APIs this year. It’s been fun in lots of ways, but the aspect I’ve found most interesting compared to UI work is the different target audience - developers rather than end users - and the consequent shift in what defines a good experience. Unlike with end users, where we want everything to be natural and intuitive, documentation is really important for an audience of developers.</p>
<p>If you’re working on a REST API, chances are it’s somehow expressed as an <a href="https://www.openapis.org/">OpenAPI</a> spec (whether you write it as one and generate code, or vice versa, is a good topic for another day). If you’ve put some effort into the descriptions, you can produce a pretty good API reference for not much effort.</p>
<p>The best-known tool for this is <a href="https://github.com/swagger-api/swagger-ui">Swagger UI</a>. But lately I’ve switched to using <a href="https://github.com/Redocly/redoc">Redoc</a>, and to me it feels like a real step up. On a practical level, it’s responsive and makes smart use of the space in a wider viewport. It also has a clearer presentation of schema objects, particularly with <code>oneOf</code> (where a field can be one of several different shapes). It’s aesthetically just a bit nicer too.</p>
<figure><img src="https://davidgoss.co/blog/images/redoc-screenshot.png" alt="" /><figcaption>Screenshot of Redoc rendering the Petstore API</figcaption></figure>
<p>That’s only half the story though. For an API reference to be useful to a developer, they probably need to know what to look for and why. What are the principles and conventions in play here? What do all these terms mean? What sequence of requests will I need to get this thing done? Where is this platform headed? If the answers to these questions are there, you could avoid a lot of repetitive support.</p>
<p>Whilst the OpenAPI specification format does have some affordance for high-level descriptive content - and it can be Markdown - I don’t think it’s enough for the kind of structured, in-depth documentation that’s needed for a good developer experience. In other words, we don’t just want a page, we want a site.</p>
<h2>Docusaurus</h2>
<p>One of the neat things about Redoc (and Swagger UI for that matter) is that it’s built as a <a href="https://reactjs.org/">React</a> component. If you just use it to generate a standalone page, you wouldn’t need to know or care about that. But if you want to make your API reference part of something bigger, React opens a lot of doors.</p>
<p>Currently, there are a lot of site frameworks built on top of React. I mean, <a href="https://jamstack.org/generators/">really a lot</a>. Of the popular ones, I’ve experimented quite a bit with <a href="https://www.gatsbyjs.com/">Gatsby</a> and <a href="https://nextjs.org/">Next.js</a> - both are impressively powerful, and it feels like you could use them to build just about anything for the web.</p>
<p><a href="https://v2.docusaurus.io/">Docusaurus</a> (from Facebook) is a bit different, in that it’s much more focused on content in general, and - as the name suggests - documentation in particular. I’ve been using Docusaurus 2 on <a href="https://liquibase-linter.dev/">some projects</a> recently, and whilst it’s officially still in Alpha, it seems pretty stable to me (although that might be because I’m not doing versioning or internationalisation just yet).</p>
<p>Docusaurus has the usual kind of theme and plugin architecture so that, if you want, you can fully control all aspects of the output. But the “classic” preset provides really good typographic and layout styles that make the content look clean and highly readable, via the <a href="https://facebookincubator.github.io/infima/">Infima CSS framework</a>. Every Docusaurus site I’ve seen so far has kept and built on the classic styling, which I think is a sign that they’ve got the balance right.</p>
<p>The content itself is of course written in Markdown, bolstered with <a href="https://v2.docusaurus.io/docs/markdown-features">documentation-focused extras</a> like callouts, syntax highlighting, and MDX. If you’re new to <a href="https://mdxjs.com/">MDX</a>, it’s basically Markdown but with support for inline JSX (and therefore React components). My first reaction on seeing it was something like “we have now reached peak JavaScript”, but it’s great for things like this because you can mix rich interactive components (e.g. flowcharts, live code samples) into your content without throwing out the simplicity of Markdown and alienating content authors who don’t happen to be JavaScript developers.</p>
<p>I’ve put up a <a href="https://github.com/davidjgoss/docusaurus-redoc-example">simple example repo</a> showing Redoc in action on a Docusaurus site. You can work through the individual commits to get a good step-by-step of what’s needed, but essentially it’s this:</p>
<ol>
<li>Initialise a Docusaurus project</li>
<li>Make your OpenAPI spec available to your project</li>
<li>Add it as a custom field in your <code>docusaurus.config.js</code></li>
<li>Install Redoc and its peer dependencies<sup class="footnote-ref"><a href="https://davidgoss.co/blog/api-documentation-redoc-docusaurus/#fn1" id="fnref1">[1]</a></sup></li>
<li>Add a new page (with a navbar link) and include the <code>RedocStandalone</code> component</li>
</ol>
<p>The React code for your page could look something like:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> React <span class="token keyword">from</span> <span class="token string">'react'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> Layout <span class="token keyword">from</span> <span class="token string">'@theme/Layout'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> useDocusaurusContext <span class="token keyword">from</span> <span class="token string">'@docusaurus/useDocusaurusContext'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span>RedocStandalone<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'redoc'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">ApiReference</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span>siteConfig<span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useDocusaurusContext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">scrollYOffset</span><span class="token operator">:</span> <span class="token string">'.navbar'</span><span class="token punctuation">,</span> <span class="token comment">// makes the fixed sidebar and scrolling play nicey with docusaurus navbar</span><br /> <span class="token literal-property property">theme</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">sidebar</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">width</span><span class="token operator">:</span> <span class="token string">'300px'</span> <span class="token comment">// about the same as the sidebar in the docs area, for consistency</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Layout</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>main</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">RedocStandalone</span></span> <span class="token attr-name">spec</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>siteConfig<span class="token punctuation">.</span>customFields<span class="token punctuation">.</span>apiSpec<span class="token punctuation">}</span></span> <span class="token attr-name">options</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>options<span class="token punctuation">}</span></span><span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>main</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Layout</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<h3>Theming</h3>
<p>The <code>theme</code> option supports a lot of customisation of typography and other styling. Since Infima <a href="https://v2.docusaurus.io/docs/styling-layout#styling-your-site-with-infima">uses CSS custom properties</a> to drive its own customisation, you can reuse many of those properties to theme Redoc as well, like this (some other stuff is omitted for brevity):</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">theme</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">typography</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">fontSize</span><span class="token operator">:</span> <span class="token string">'var(--ifm-font-size-base)'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">lineHeight</span><span class="token operator">:</span> <span class="token string">'var(--ifm-line-height-base)'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">fontFamily</span><span class="token operator">:</span> <span class="token string">'var(--ifm-font-family-base)'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">headings</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">fontFamily</span><span class="token operator">:</span> <span class="token string">'var(--ifm-font-family-base)'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">fontWeight</span><span class="token operator">:</span> <span class="token string">'var(--ifm-heading-font-weight)'</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">code</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">lineHeight</span><span class="token operator">:</span> <span class="token string">'var(--ifm-pre-line-height)'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">fontFamily</span><span class="token operator">:</span> <span class="token string">'var(--ifm-font-family-monospace)'</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">RedocStandalone</span></span> <span class="token attr-name">options</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>options<span class="token punctuation">}</span></span><span class="token punctuation">/></span></span></code></pre>
<p>This is a great example of web standards making things better, not by providing the whole solution, but by making it easier to use tools together and write less code.</p>
<p>Unfortunately if you try to use the custom property names like that with most of what’s under <code>colors</code>, you’ll hit an error, since Redoc <a href="https://github.com/Redocly/redoc/blob/master/src/theme.ts#L18">does some processing</a> to generate light/dark variants etc and expects the values you give it to be parseable as HEX or RGB. I think this could be addressed though.</p>
<h3>Links</h3>
<p>Since Redoc supports deep links for operations and tags, you can link straight to relevant bits of your API reference from a relevant context in your documentation content:</p>
<pre class="language-markdown"><code class="language-markdown">See the docs for <span class="token url">[<span class="token content">creating a quote</span>](<span class="token url">/api-reference#operation/create-quote</span>)</span>.</code></pre>
<p>You could potentially add a <code><LinkToOperation></code> React component to abstract this, if you want.</p>
<h3>Distribution</h3>
<p>Docusaurus builds your site as a set of static HTML, CSS and JavaScript files which you can then deploy with Netlify, GitHub Pages or whatever your thing is.</p>
<p>It’s worth noting that Redoc has <a href="https://github.com/Redocly/redoc/tree/master/cli">a neat CLI</a> that allows you to generate zero-dependency HTML file from your API spec. This portability could be useful if your API isn’t public and circulation of your documentation is controlled. However, once you start building a site around it, this sadly isn’t an option any more. Besides the fact you now have multiple pages, most site frameworks (including Docusaurus) are pretty opinionated in expecting your site to be served from a web server, at a <a href="https://github.com/gatsbyjs/gatsby/discussions/14161">known path</a>. That’s today, anyway; the new <a href="https://web.dev/web-bundles/">Web Bundles API</a> looks like it might solve this problem eventually.</p>
<hr />
<p>I’m pretty happy with how this combination of tools has worked on my projects this year. I’m looking at a few other ideas to improve it:</p>
<ul>
<li>Using <a href="https://v2.docusaurus.io/docs/blog">Docusaurus blog functionality</a> to add a changelog for the API.</li>
<li>Using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> to enable trying API operations from the docs without pointing at a live service.</li>
</ul>
<hr />
<p><strong>Update on 12 April 2021:</strong> Turns out there’s <a href="https://github.com/facebook/docusaurus/issues/638">a bit of demand</a> for this. <a href="https://github.com/rohit-gohri">Rohit Gohri</a> has pulled it together into <a href="https://www.npmjs.com/package/redocusaurus">a Docusaurus preset</a> that you can pull into your project fairly easily.</p>
<p><strong>Update on 3 August 2022:</strong> Docusaurus just <a href="https://docusaurus.io/blog/2022/08/01/announcing-docusaurus-2.0">released their stable 2.0</a> - congratulations to Sébastien and the rest of the team.</p>
<hr class="footnotes-sep" />
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>In the latest alphas of Docusaurus 2, you also need to add a small Webpack override (via Docusaurus’s <a href="https://v2.docusaurus.io/docs/lifecycle-apis#configurewebpackconfig-isserver-utils">plugin system</a>) to polyfill <code>Buffer</code>. This is because in its recent version 5 release, Webpack <a href="https://blog.sindresorhus.com/webpack-5-headache-b6ac24973bf1">stopped automatically polyfilling</a> Node.js APIs that aren’t available in browsers. <a href="https://davidgoss.co/blog/api-documentation-redoc-docusaurus/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
Better Pull Requests2021-04-22T00:00:00Zhttps://davidgoss.co/2019/09/03/better-pull-requests/<p><em>I spend a lot of my time on pull requests. I have a few thoughts about what works and what doesn’t in the pull request process.</em></p>
<hr />
<h2>Be a good requester</h2>
<p><em>TLDR: Make it easy for reviewers.</em></p>
<p>As the person creating a pull request, you’re going to be asking reviewers for some of their time and attention; both are finite, so do what you can to make it easy for them. It’ll help you get this change in faster and, if you keep it up, might earn you a reputation in your company as someone who does good work and is thoughtful.</p>
<h3>Keep it small (if you can)</h3>
<p>Small, focused pull requests from <a href="https://trunkbaseddevelopment.com/short-lived-feature-branches/">short-lived feature branches</a> have the easiest time getting merged. They are:</p>
<ul>
<li>Quicker and more digestible (and reviewers are less likely to bail)</li>
<li>Less likely to need lengthy rework if you’ve taken a wrong turn</li>
<li>Less likely to get conflicted with the main branch and need manual resolution</li>
</ul>
<p>In reality you’ll sometimes need to do a large change that takes several days or even weeks to finish, but that doesn’t mean you should sit on one branch for that time and raise a “big bang” pull request at the end. Instead, find ways to split it up into smaller changes that can be done incrementally without breaking anything.</p>
<p>For example, you might be doing some <a href="https://martinfowler.com/articles/preparatory-refactoring-example.html">preparatory refactoring</a> to lay the groundwork for your new functionality, or bolstering test coverage of existing behaviour to give you the confidence to start making changes. Both are good candidates for a pull request in their own right.</p>
<p>Once you get into actually adding your new functionality, keep to this mantra of small and incremental changes. Try working from the outside in — starting with the UI or the API entry point, stubbing the next layer in as you go, and using feature flags to keep it hidden until it’s ready. As well as preventing you from drifting off the mainline and/or doing too-big pull requests, this approach gives you a chance to tag expert reviewers when working in their area, without dragging in too many people at a time.</p>
<h3>Housekeeping</h3>
<p>Away from the actual code, there are a few things you can do to make sure your pull request is approachable.</p>
<p>Don’t leave the description blank. Use it to clearly outline<sup class="footnote-ref"><a href="https://davidgoss.co/2019/09/03/better-pull-requests/#fn1" id="fnref1">[1]</a></sup>:</p>
<ul>
<li>The reason for the changes</li>
<li>What they consist of at a high level</li>
<li>What behaviour they produce</li>
</ul>
<p>This should give reviewers some good context for what they’re about to see, and save them having to go off and read issue descriptions or acceptance criteria. If the changes have a non-trivial UI impact, think about adding a couple of screenshots.</p>
<p>If you can predict any queries that reviewers might have, try to address them pre-emptively by adding comments yourself, inline with the code. You can call out mitigating factors and/or other approaches that were considered and discarded.</p>
<p>Finally, spend five minutes looking over the pull request yourself before you add any reviewers; you’ll be amazed how often you find typos, commented-out code you forgot to delete, defunct tests etc. This will save your reviewers time and might save you some embarassment.</p>
<h3>Choice of reviewers</h3>
<p>Pull requests are a really good opportunity for people to learn, so it’s a bit of a waste if just one person is tagged for review (especially if it’s always the same person). On the flipside, having the whole team tagged on every pull request can become a source of distraction, and makes it more likely that no one will review as each person expects that someone else will get to it. So figure out what works for your team here on the balance of throughput and knowledge sharing.</p>
<p>Also, think about people outside the immediate team: is there someone who has worked in this area a lot and could give some valuable feedback, or someone who’s expressed an interest in a tool or technique you’ve used and might like to see what you’ve done?</p>
<p>If there’s one person whose approval you <em>really</em> want before you’re happy to merge, make sure you tell them, so they can avoid being an unwitting blocker.</p>
<h2>Be a good reviewer</h2>
<p><em>TLDR: Be nice.</em></p>
<h3>Manage expectations</h3>
<p>There will be times when you aren’t going to review a pull request you’ve been tagged on. It might be that you’re too busy to give it the proper attention in a timely manner, or that you don’t feel you’re best-placed to give a meaningful review, given your skills/knowledge etc. Either way, say so.</p>
<h3>Critique with care</h3>
<p>Coming into a code review, resist the urge to go straight in and start commenting on stuff that jumps out at you. Instead, do a quick pass over the whole pull request — the answers to your questions might be in there, if you take a minute to look.</p>
<p>When you do come to leave some critical feedback, think carefully about what to say and how. As with most communication that isn’t face-to-face, it’s easy to come across like a jerk even if you don’t mean to<sup class="footnote-ref"><a href="https://davidgoss.co/2019/09/03/better-pull-requests/#fn2" id="fnref2">[2]</a></sup>, so it’s worth going out of your way to be cordial and constructive. When you’ve spotted a problem, consider putting to the requester as a question, like “Not sure, but this looks to me like it will break if the second argument is null, could you add a test to check?”, or “This might read better using a stream, what do you think?”.</p>
<p>Many comments or suggestions you add will be subjective to some degree, so it’s good to provide information that supports your point of view. For example, if you’re pointing out that some internal standard or convention is being broken, link to where the standards are documented, and to a conforming example elsewhere in the codebase. If you are suggesting a different tool or technique for something, link to an article that explains it.</p>
<p>Also, don’t forget about positive feedback. As much as anything else, a review is a chance for you to call out any good stuff you see. The odd “This is a nice pattern, would like to see more of it” won’t do any harm, and might provide a bit of balance if you have left some more critical feedback elsewhere.</p>
<h3>Automate</h3>
<p>If you find yourself leaving comments on pull requests about code style (indents, spacing, etc), something is wrong. Tools like <a href="https://eslint.org/">ESLint</a> and <a href="https://prettier.io/">Prettier</a> were built to do exactly this sort of thing, so get going with one now if you haven’t already. Being told your code doesn’t look right hurts your feelings; better that it comes from a robot than a person.</p>
<p>Aside from stylistic stuff, if you find yourself repeatedly picking up on the same pitfall or anti-pattern in code reviews, look into whether you can automate that too; many lint tools provide a way to <a href="https://liquibase-linter.dev/docs/custom-rules">write custom rules</a>.</p>
<hr />
<p>Should you even do pull requests though?</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Pull requests introduce a human delay into a team's integration workflow. <a href="https://twitter.com/kief?ref_src=twsrc%5Etfw">@kief</a> explains how and outlines other ways to do code review<a href="https://t.co/EBAKG9Mxl8">https://t.co/EBAKG9Mxl8</a></p>— Martin Fowler (@martinfowler) <a href="https://twitter.com/martinfowler/status/1345375503277023232?ref_src=twsrc%5Etfw">January 2, 2021</a></blockquote>
<p>I think the article cited there is a very fair critique of the way that pull requests often work in practise. Long-lived branches and big pull requests sitting around for days aren’t good on any level - but it doesn’t have to be that way. There are some great approaches outlined in the article that you can use with pull requests to avoid them becoming a bottleneck, like pairing as you go.</p>
<p>Further reading:</p>
<ul>
<li><a href="https://medium.engineering/code-reviews-at-medium-bed2c0dce13a"><em>Code Reviews at Medium</em></a> by Christine Donovan</li>
<li><a href="https://trishagee.github.io/post/code_review_best_practices/"><em>Code Review Best Practices</em></a> by Trisha Gee</li>
</ul>
<hr class="footnotes-sep" />
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>If you don’t have a template for PRs, you might consider trying one - but keep it simple and don’t overenforce it. <a href="https://davidgoss.co/2019/09/03/better-pull-requests/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>And remember, other people are watching - you are setting the tone. <a href="https://davidgoss.co/2019/09/03/better-pull-requests/#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
Designing responsive components in a UI library2020-11-07T00:00:00Zhttps://davidgoss.co/2016/02/28/designing-responsive-components-in-a-ui-library/<p>I’ve been working on a standard CSS framework and UI library at work for the past little while, and it’s thrown up some tricky challenges. One of the more interesting ones has been that of how to design components to be responsive without knowing which context(s) they’ll be used in.</p>
<p>Designing a UI library component in isolation is quite a fun task. I can tackle its “default” presentation first<sup class="footnote-ref"><a href="https://davidgoss.co/2016/02/28/designing-responsive-components-in-a-ui-library/#fn1" id="fnref1">[1]</a></sup>, then try some different viewport sizes and write alternate styles within media queries where the default stops looking right.</p>
<pre class="language-less"><code class="language-less"><span class="token selector">.component</span> <span class="token punctuation">{</span><br /> <span class="token comment">// default presentation</span><br /><span class="token punctuation">}</span><br /><br /><span class="token atrule">@media <span class="token punctuation">(</span>min-width<span class="token punctuation">:</span>500px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br /> <span class="token selector">.component</span> <span class="token punctuation">{</span><br /> <span class="token comment">// better presentation for wider viewports</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token atrule">@media <span class="token punctuation">(</span>min-width<span class="token punctuation">:</span>800px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br /> <span class="token selector">.component</span> <span class="token punctuation">{</span><br /> <span class="token comment">// even better presentation for widest viewports</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>Now fast forward to a real project that uses our UI library. The layout my component is being used within has some responsive design of its own, and switches to two equal columns once the viewport is 700px wide. Suddenly, my component is being presented in a way that works in 500px or more of space, but it only has 350px to play with so the design falls down.</p>
<p>I’ve been designing and building this UI library at the same time as designing screens for a big-ish app that’s going to consume it. This has been a good process — each entity has informed and influenced the other — and because of it, the issue of responsive variants not working presented itself pretty early on.</p>
<h2>Theoretical solution: Container queries</h2>
<p>The concept of container queries, where you would be able to write alternate styles for a component based on the dimensions of <em>the container it’s in</em> would solve this problem. In fact, container queries (or “element queries” as some call them) would be nothing short of revolutionary to the world of UI libraries.</p>
<p>In the case of our component from earlier, we could use container queries instead of media queries to wrap the alternate styles, so instead of saying “when the viewport is 500px wide or more, apply these styles”, we’d be saying “when the component is in 500px of space or more, apply these styles”. Something like this (beware, this syntax is speculative at best):</p>
<pre class="language-less"><code class="language-less"><span class="token selector">.component</span> <span class="token punctuation">{</span><br /> <span class="token comment">// default presentation</span><br /><span class="token punctuation">}</span><br /><br /><span class="token atrule">@container <span class="token punctuation">(</span>min-width<span class="token punctuation">:</span>500px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br /> <span class="token selector">.component</span> <span class="token punctuation">{</span><br /> <span class="token comment">// better presentation for wider viewports</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token atrule">@container <span class="token punctuation">(</span>min-width<span class="token punctuation">:</span>800px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br /> <span class="token selector">.component</span> <span class="token punctuation">{</span><br /> <span class="token comment">// even better presentation for widest viewports</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>Then, it wouldn’t matter where in a project the component got dropped into - two columns, three columns, whatever – it would just work. Perfect!</p>
<p>Unfortunately, container queries are unlikely to happen any time soon, if at all. <a href="http://www.xanthir.com/b4VG0">Tab Atkins explains it</a> better than I could, but essentially there are circularity issues with no apparent way around them.</p>
<h2>Hacky workaround: Mixins</h2>
<p>We can use <a href="https://sass-lang.com/documentation/at-rules/mixin">mixins</a> to make the responsive variants of our components composable in projects that use them.</p>
<p>Obviously, I’m making a fairly large assumption at this point that any serious UI library is going to be using a CSS preprocessor like Sass. Yes, preprocessors are a double-edged sword and people can make a mess by abusing extends etc, but your UI library is important and its source should be carefully controlled, and contributions well-reviewed.</p>
<p>With our example component, we can pull out the container-specific styles into a couple of mixins, so the library itself is only writing out the default presentation to its own CSS, but providing the tools to apply the responsive variants as required by consuming projects<sup class="footnote-ref"><a href="https://davidgoss.co/2016/02/28/designing-responsive-components-in-a-ui-library/#fn2" id="fnref2">[2]</a></sup>.</p>
<p>In library:</p>
<pre class="language-less"><code class="language-less"><span class="token selector">.component</span> <span class="token punctuation">{</span><br /> <span class="token comment">// default presentation</span><br /><span class="token punctuation">}</span><br /><br /><span class="token selector">.component-wider-presentation()</span> <span class="token punctuation">{</span><br /> <span class="token comment">// better presentation for wider containers (about 500px+)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token selector">.component-widest-presentation()</span> <span class="token punctuation">{</span><br /> <span class="token comment">// even better presentation for widest containers (about 800px+)</span><br /><span class="token punctuation">}</span></code></pre>
<p>In consuming project:</p>
<pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stats-page-component component<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<pre class="language-less"><code class="language-less"><span class="token selector">.stats-page-component</span> <span class="token punctuation">{</span><br /> <span class="token atrule">@media <span class="token punctuation">(</span>min-width<span class="token punctuation">:</span>500px<span class="token punctuation">)</span> and <span class="token punctuation">(</span>max-width<span class="token punctuation">:</span>699px<span class="token punctuation">)</span>, <span class="token punctuation">(</span>min-width<span class="token punctuation">:</span>1000px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br /> <span class="token mixin-usage function">.component-wider-presentation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>Whilst working this way will ensure your UI library doesn’t ship with counter-productive responsive behaviour, it does mean that in the majority of cases things won’t “just work” out of the box — that is, you’ll tend to have to use the mixins to make a component look right in your project’s context, rather than just chuck it in. You could worry about whether people working on the consuming projects will get this right, but instead spend that time writing documentation so you can help them get it right.</p>
<hr />
<p><strong>Update on 7 November 2020:</strong> Container queries are now going to be a thing:</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Blink: Intent to Prototype: Container Queries <a href="https://t.co/sIlv6klTay">https://t.co/sIlv6klTay</a></p>— Intent To Ship (@intenttoship) <a href="https://twitter.com/intenttoship/status/1324351881251033089?ref_src=twsrc%5Etfw">November 5, 2020</a></blockquote>
<hr class="footnotes-sep" />
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>People often cite “mobile first” here, saying the default should be the smallest variant, but I don’t necessarily agree; the default should be whatever takes the least code to write and then undo. For instance, it’s <em>much</em> simpler in terms of code to style data tables in their tabular form and then use <code>max-width</code> media queries to “flatten” them to a single column than try to do it all mobile first. In <del>most</del> many cases, media queries support is now a given and you don’t need to protect against its absence by doing <em>everything</em> mobile first. <a href="https://davidgoss.co/2016/02/28/designing-responsive-components-in-a-ui-library/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>This might raise a red flag with some people, like “If the component is being used in several different places, you’ll be writing the same styles several times in the output CSS”, which is factually correct but not really a problem. Firstly, as to the repetition itself, this is not something to be afraid of in the output CSS — computers are really good at dealing with repetition! — as long as the repetition stays <em>out of the source</em> (which is what mixins are for). Secondly, although the size of the output CSS file might grow as a result of the repetition, <a href="http://csswizardry.com/2016/02/mixins-better-for-performance/">Harry Roberts points out</a> that our trusty friend gzip will mostly negate that. <a href="https://davidgoss.co/2016/02/28/designing-responsive-components-in-a-ui-library/#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
Elegant Async Code at MK.js2016-02-24T00:00:00Zhttps://davidgoss.co/2016/02/24/elegant-async-talk/<p>Last night I gave a brief talk at <a href="http://www.meetup.com/mkjscript/">MK.js</a> about writing elegant async code with ES2015 Promises.</p>
<p>It’s a topic that’s held my attention since digging into ES2015<sup class="footnote-ref"><a href="https://davidgoss.co/2016/02/24/elegant-async-talk/#fn1" id="fnref1">[1]</a></sup> late last year. I did a lunch and learn at work on many of the new features, and found Promises both the most interesting (for me and the audience) and the trickiest to cover in a couple of slides, so it seemed to merit its own talk.</p>
<p>I’ve put my slides up on Speakerdeck, such as they are, and you might want to check out the <a href="https://github.com/davidjgoss/elegant-async-code">example code project</a> on GitHub as well.</p>
<p>As for the talk itself, I’m pretty happy with how I did — it was my first try at public speaking — and the audience seemed to like the content. I should say as well that MK.js is a brilliant little event and <a href="https://twitter.com/captain_cow1">James</a> does a great job organising and hosting it in his own time with little funding.</p>
<p>If you’re near Milton Keynes on the last Tuesday of a month, <a href="http://www.meetup.com/mkjscript/">join us</a>.</p>
<script async="" class="speakerdeck-embed" data-id="6e75587c10bf4b4dad46c74ef7236867" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script>
<hr class="footnotes-sep" />
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>It’s really hard not to call it ES6 all the time. <a href="https://davidgoss.co/2016/02/24/elegant-async-talk/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>