<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Ugly Duck</title>
<link href="https://uglyduck.ca/atom.xml" rel="self"/>
<link href="https://uglyduck.ca/"/>
<updated>2021-11-11T12:15:34-05:00</updated>
<id>https://uglyduck.ca</id>
<author>
<name>Bradley Taunt</name>
<email>hello@uglyduck.ca</email>
</author>
<entry>
<title>Modern Improvements for Default Browser Styles</title>
<link href="https://uglyduck.ca/default-html-style-updates/"/>
<updated>2021-11-09T00:00:00-05:00</updated>
<id>https://uglyduck.ca/default-html-style-updates</id>
<content type="html"><p>This website <em>almost</em> exclusively uses the browser’s (whichever one that might be) default styling to render it’s HTML. I firmly believe, and have <a href="/css-js-mistake/">stated in a previous post</a>, that the default HTML styling across all browsers is a thing of beauty. “Consistent and boring” is how I tend to refer to default browser styles - and I mean that in a <em>good way</em>.</p>
<p>But that doesn’t mean some minor, modern improvements couldn’t be made…</p>
<h2 id="boosting-margins-and-increasing-font-size">Boosting Margins and Increasing Font Size</h2>
<p>A little extra breathing room for a website’s content never hurts. Browser defaults set the inner content too close to the main window borders, creating mild eye strain to focus on the far edges of the screen when reading. Pair this with a typeface set too small and you’ve got a recipe for disaster (in terms of user experience and accessibility). Luckily for us, adding two basic CSS properties fixes all of our readability woes. All that is required is a simple boost to the existing <code class="language-plaintext highlighter-rouge">margin</code> property set on the <code class="language-plaintext highlighter-rouge">body</code> element (I personally lean towards a very specific <code class="language-plaintext highlighter-rouge">1.5em</code>) and overriding the default <code class="language-plaintext highlighter-rouge">font-size</code> to <code class="language-plaintext highlighter-rouge">18px</code><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>body {
font-size:18px;
margin:1.5em;
}
</code></pre></div></div>
<p>There is one <em>small</em> caveat with setting the <code class="language-plaintext highlighter-rouge">font-size</code> across the whole <code class="language-plaintext highlighter-rouge">body</code> element: code elements set in <code class="language-plaintext highlighter-rouge">monospace</code>. They will stand out larger than the other fonts found in the document (due to variations in different typeface heights, spacing etc.) so we will need to target these elements specifically:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code {
font-size:14px;
/* Word wrap is optional if you plan to have long inline code snippets */
word-wrap:break-word;
}
</code></pre></div></div>
<h2 id="code--pre-tags">Code &amp; Pre Tags</h2>
<p>Since we’ve mentioned <code class="language-plaintext highlighter-rouge">code</code> elements, let’s fix those as well. The existing styling for inline code snippets and larger pre-formatted text sections leave a lot to be desired. They don’t provide any means to wrap their inner content or make use of <code class="language-plaintext highlighter-rouge">overflow</code> properties to avoid vertically scrolling on smaller device screens. Sharing code examples becomes quite a pain when your webpage’s flow and layout is broken just by including them. Browsers could fix this easily enough by defaulting to:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pre {
overflow:auto;
}
</code></pre></div></div>
<h2 id="basic-dark-mode-support">Basic Dark Mode Support</h2>
<p>Barebones styling in current web browsers have no sane defaults<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> for system-level dark mode. What a huge letdown. This is where the most “drastic” changes will be implemented with our browser default updates. We will need the browser to change the main <code class="language-plaintext highlighter-rouge">background-color</code>, along with resetting both the text and anchor link <code class="language-plaintext highlighter-rouge">color</code> for improved accessibility. Browser defaults for anchor link color in “light mode” are blue/purple - so I’ve opted towards using gold, orange and orangered in dark mode respectively:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* Dark mode */
@media (prefers-color-scheme: dark) {
@media not print {
html {background:#0e0e0e;color:#e1e1e1; }
a {color: gold;}
a:visited {color: orange;}
a:hover,a:focus{color: orangered;}
}
}
</code></pre></div></div>
<p>That is probably the most streamlined dark mode on the web…</p>
<h2 id="the-reading-length-debate">The “Reading Length” Debate</h2>
<p>Proper reading length tends to be quite the point of contention on the web. Hell, even I’ve <a href="/character-unit/">written about it quite a bit</a> in the past (and many of my side projects follow that standard). The main problem I have with this is lack of <em>user control</em>. I don’t think the browser (or designers for that matter) should determine the best reading length for my own personal reading preferences. UX testing and group feedback has (somewhat) agreed upon 66-75 characters per line to be the most optimal reading experience. That is good to know. I <em>still</em> believe it should come down to user preference.</p>
<p>Do you want to know an incredible feature built into browsers? <em>Window resizing</em>. Abandon the idea that you “know better” than your users and give them the power to adjust as they see fit. The web was meant to be personal and flexible.</p>
<h2 id="conclusion">Conclusion</h2>
<p>There isn’t much else to say, really. I think these tiny tweaks would greatly improve the default browser experience and maybe even convince others to just <em>use</em> these defaults instead of falling down the CSS rabbit hole (as fun as that might be sometimes). For easier convenience, I’ll leave the full set of CSS changes below:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>body {
font-size:18px;
margin:1.5em;
}
code {
font-size:14px;
}
pre {
overflow:auto;
}
@media (prefers-color-scheme: dark) {
@media not print {
html {background:#0e0e0e;color:#e1e1e1; }
a {color: gold;}
a:visited {color: orange;}
a:hover,a:focus{color: orangered;}
}
}
</code></pre></div></div>
<p><br /></p>
<h4 id="footnotes">Footnotes</h4>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p><code class="language-plaintext highlighter-rouge">18px</code> seems to be the perfect sweet spot between “almost too large, yet not small enough to strain my eyes” <a href="#fnref:1" class="reversefootnote" role="doc-backlink">[return]</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>At the time of this article’s publish date <a href="#fnref:2" class="reversefootnote" role="doc-backlink">[return]</a></p>
</li>
</ol>
</div>
</content>
</entry>
<entry>
<title>A Reality Where CSS and JavaScript Don't Exist</title>
<link href="https://uglyduck.ca/css-js-mistake/"/>
<updated>2021-11-03T00:00:00-04:00</updated>
<id>https://uglyduck.ca/css-js-mistake</id>
<content type="html"><p><em>This is my personal opinion. Please leave your pitchforks at the door…</em></p>
<p>I love CSS. I can spend hours deep diving into a website’s CSS system and never find myself getting bored. It’s pretty amazing to see the problems other designers are able to solve with just a little bit of custom styling and determination.</p>
<p>I like JavaScript. It serves it’s purpose and makes complex functionality on the web easier to wrangle in and understand.</p>
<p><em>But I think both should have never come into existence</em>.</p>
<h3 id="heresey">Heresey!</h3>
<p>I know, I know - this website itself uses a little, teeny-tiny amount of CSS. I am indeed a hypocrite - but did I ever claim that I <em>practice</em> what I preach? At least this personal website is JavaScript-free… (apart from a handful of CodePen examples embedded in some of the tutorials).</p>
<h3 id="moving-on">Moving On…</h3>
<p>I’m not a complete idiot. I realize that the web has evolved significantly over the years, and to propose that it should have remained stagnant with it’s original concept of “paged documents” is foolish. But that <em>is</em> what I’m suggesting - at least, partially.</p>
<h3 id="consistent--boring">Consistent &amp; Boring</h3>
<p>Out there in the multiverse is a reality where the web is a complete borefest. Information is the only driving factor to visit a “web page” and PWAs have never come to exist. Custom styling, fancy interactive animations and single-page functionality isn’t even something that can be implemented. The web is just a system of HTML/plaintext documents sharing information and data. Users browse the web in quick bursts to satisfy their queries or read something interesting. Then, they return to <em>real life</em>.</p>
<p>My goodness what a <em>beautiful</em> reality that would be. Consistent, boring and wonderful.</p>
<h3 id="wait---arent-you-a-designer">“Wait - Aren’t You a Designer?”</h3>
<p>Yes - and again more hypocrisy. My livelihood depends on software requiring custom UIs and properly audited UX flows. By suggesting this change I am throwing myself under the bus and putting myself out of work. All my experience would become worthless and the world of software <em>design</em> would cease to exist.</p>
<p><strong>I would be okay with that</strong>. If it meant the web as a whole was a better place - so be it.</p>
<h3 id="a-look-at-the-new-world">A Look at the “New World”</h3>
<p>Sometimes it is easier to visualize a concept instead of just discussing it. Below you can find an example of a “converted” website<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> showcasing how sites would look and feel in this design-less reality:</p>
<ul>
<li>Wealthsimple: <a href="https://www.wealthsimple.com/en-ca/">Current Website</a> / <a href="/html-only/wealthsimple/">New Version</a></li>
</ul>
<p>As you can see, all the <em>fluff</em> has been removed from the existing design and only the content remains. No scroll-jacking or extra JavaScript libraries are downloaded. Users can easily skim through all the content and screen readers won’t struggle through any custom elements. It also loads incredibly fast.</p>
<p>Of course, to our<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> eyes, this design might look ugly or seem as though the site’s CSS failed to load - but in a reality where this is the standard, it is beautifully minimal. Either way, I find that this was at least a fun thought experiment and hopefully leaves you thinking about how the web <em>could have been</em> as well.</p>
<p>Now, back to designing UIs for the web…</p>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>Selected site based on my own personal preference <a href="#fnref:1" class="reversefootnote" role="doc-backlink">[return]</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>Referring to current users of the web <a href="#fnref:2" class="reversefootnote" role="doc-backlink">[return]</a></p>
</li>
</ol>
</div>
</content>
</entry>
<entry>
<title>Setting Up 1.1.1.1 for Families on a Pi-Hole</title>
<link href="https://uglyduck.ca/pihole-cloudflare/"/>
<updated>2021-10-28T00:00:00-04:00</updated>
<id>https://uglyduck.ca/pihole-cloudflare</id>
<content type="html"><p>After seeing Cloudflare’s 1.1.1.1 for Families mentioned on the <a href="https://news.ycombinator.com/item?id=29024195">front page of HackerNews</a>, I thought it might be helpful to show those currently using a <a href="https://pi-hole.net">pi-hole device</a> how to include 1.1.1.1 alongside it.</p>
<h3 id="keeping-things-updated">Keeping Things Updated</h3>
<p>It should go without saying that you should be running the latest stable pi-hole version for security and full feature support. To do so, simply <code class="language-plaintext highlighter-rouge">ssh</code> into your device (or connect to it directly if you prefer) and run the following to check your pi-hole version:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pihole -v
</code></pre></div></div>
<p>And if your version is out of date, run:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pihole -up
</code></pre></div></div>
<p>Once it completes the update everything will be good to go!</p>
<h3 id="adding-1111-1112">Adding 1.1.1.1 (1.1.1.2)</h3>
<p>Now you need to navigate to your main pi-hole admin in your browser (most likely the url will be <em>pi.hole</em>). Login in using your credentials and the do the following steps:</p>
<ol>
<li>Navigate to <strong>Settings</strong></li>
<li>Navigate to <strong>DNS</strong></li>
<li>Under “Upstream DNS Servers” enter:
<ul>
<li><em>Custom 1 (IPv4)</em>: 1.1.1.2</li>
<li><em>Custom 2 (IPv4)</em>: 1.0.0.2</li>
</ul>
</li>
</ol>
<p><img src="/public/images/pihole-cloudflare.webp" alt="pihole cloudflare DNS setup" /></p>
<h3 id="ad-block--malware-protection">Ad-Block &amp; Malware Protection</h3>
<p>You now officially have protection against both intrusive advertisements <em>and</em> sites flagged with malware. Happy (and now safer) browsing!</p>
</content>
</entry>
<entry>
<title>Batch Converting Images to webp with macOS Automator</title>
<link href="https://uglyduck.ca/batch-webp-conversion/"/>
<updated>2021-10-15T00:00:00-04:00</updated>
<id>https://uglyduck.ca/batch-webp-conversion</id>
<content type="html"><p>A great deal of my time working as a web/UI designer is spent exporting and/or converting images for software products and websites. Although a lot of modern applications can render image conversions at build time, a custom conversion is sometimes requested for an image to be set as <code class="language-plaintext highlighter-rouge">webp</code>.</p>
<p>You <em>could</em> download one of the many native apps from the Mac App Store to do this for you - but why not create your own script and run it with a simple right-click directly inside Finder? <strong>Let’s do just that!</strong></p>
<h2 id="basic-requirements">Basic requirements</h2>
<p><strong>Important!</strong>: As of this time of writing, the official <code class="language-plaintext highlighter-rouge">libwebp</code> package release is <em>libwebp-1.2.1-mac-10.15</em>. If this has been updated since then, change the command below to match that of the proper release version.</p>
<p>1) First you will need to download the <code class="language-plaintext highlighter-rouge">libwebp</code> package to your Downloads folder: <a href="https://developers.google.com/speed/webp/download">developers.google.com/speed/webp/download</a></p>
<ul>
<li><em>Look for the “Download for macOS link”</em></li>
</ul>
<p>2) Next we will need to copy the <code class="language-plaintext highlighter-rouge">cwebp</code> folder to our <code class="language-plaintext highlighter-rouge">/usr/local/bin</code> directory:</p>
<ul>
<li>Open macOS Terminal</li>
<li>Run <code class="language-plaintext highlighter-rouge">sudo cp /Downloads/libwebp-1.2.1-mac-10.15/bin/cwebp /user/local/bin</code></li>
<li><em>Note:</em> if the <code class="language-plaintext highlighter-rouge">/usr/local/bin</code> directory doesn’t exist, simply create it by running: <code class="language-plaintext highlighter-rouge">sudo cd /usr/local &amp;&amp; mkdir bin</code></li>
</ul>
<h2 id="creating-our-custom-automator-script">Creating our custom Automator script</h2>
<p>1) Open the macOS Automator from the Applications folder</p>
<p>2) Select <code class="language-plaintext highlighter-rouge">Quick Option</code> from the first prompt</p>
<p>3) Set “Workflow recieves current” to <code class="language-plaintext highlighter-rouge">image files</code></p>
<p>4) Set the label “in” to <code class="language-plaintext highlighter-rouge">Finder</code></p>
<p>5) From the left pane, select “Library &gt; Utilities”</p>
<p>6) From the presented choices in the next pane, drag and drop <code class="language-plaintext highlighter-rouge">Run Shell Script</code> into the far right pane</p>
<p>7) Set the area “Pass input” to <code class="language-plaintext highlighter-rouge">as arguments</code></p>
<p>8) Enter the following code below as your script and type <code class="language-plaintext highlighter-rouge">⌘-S</code> to save (name it something like “Convert to webp”)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>for f in "$@"
do
/usr/local/bin/cwebp -q 85 "$f" -o "${f%.*}.webp"
done
</code></pre></div></div>
<p>For visual reference, it should look something like this:</p>
<p><img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/automator.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/automator.webp 320w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/automator.webp 540w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/automator.webp 760w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/automator.webp 980w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/automator.webp 1200w" sizes="100vw" alt="Automator quick action" width="2224" height="1984" crossorigin="anonymous" /></p>
<p>And when right-clicking an image file in the Finder window, it should now give you the option to convert:</p>
<p><img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/automator-2.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/automator-2.webp 320w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/automator-2.webp 540w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/automator-2.webp 760w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/automator-2.webp 980w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/automator-2.webp 1200w" sizes="100vw" alt="Right click to convert to webp" width="1982" height="1158" crossorigin="anonymous" /></p>
<h2 id="making-edits-to-your-script">Making edits to your script</h2>
<p>If you ever have the need to edit this script (for example, changing the default <code class="language-plaintext highlighter-rouge">85</code> quality parameter), you will need to navigate to your <code class="language-plaintext highlighter-rouge">~/Library/Services</code> folder and open your custom webp Quick Action in the Automator application.</p>
<p>Simple as that!</p>
</content>
</entry>
<entry>
<title>Enabling Safari Extensions with the macOS Catalina Patcher</title>
<link href="https://uglyduck.ca/safari-extensions-catalina-patcher/"/>
<updated>2021-09-23T00:00:00-04:00</updated>
<id>https://uglyduck.ca/safari-extensions-catalina-patcher</id>
<content type="html"><p><em>I have an old 2011 MacBook Air that is running the latest version</em> of macOS Catalina thanks to the very wonderful <a href="http://dosdude1.com/catalina/">Catalina Patcher</a> by dosdude1. This project has made it possible for me to run and test some of the latest software from Apple - namely Safari 15.</p>
<p>I ran into a small bug early on though – Safari extensions couldn’t be activated via the preferences menu. Luckily I discovered a very simple fix. My hope is this might help others (as small of a demographic that might be) who run into the same issue with the Catalina Patcher.</p>
<ul>
<li>Close/Quit Safari completely</li>
<li>Open the Finder and use the shortcut <code class="language-plaintext highlighter-rouge">⌘ Shift G</code></li>
<li>Paste the following to navigate to the proper folder: <code class="language-plaintext highlighter-rouge">~/Library/Containers/com.apple.Safari/Data/Library/Safari/AppExtensions</code></li>
<li>Open the <code class="language-plaintext highlighter-rouge">Extensions.plist</code> in your favourite text editor (I’m a big fan of Panic’s Nova)</li>
<li>Below each extensions <code class="language-plaintext highlighter-rouge">&lt;dict&gt;</code> tag, add the following: <code class="language-plaintext highlighter-rouge">&lt;key&gt;Enabled&lt;/key&gt;&lt;true/&gt;</code></li>
<li>Save the file and relaunch Safari</li>
</ul>
<p>Reference of what it should look like (AdBlock for this example):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;key&gt;org.adblockplus...&lt;/key&gt;
&lt;dict&gt;
&lt;key&gt;AddedDate&lt;/key&gt;
&lt;date&gt;2021-09-23T14:00:47Z&lt;/date&gt;
&lt;key&gt;Enabled&lt;/key&gt;
&lt;true/&gt;
&lt;key&gt;WebsiteAccess&lt;/key&gt;
&lt;dict&gt;
&lt;key&gt;Allowed Domains&lt;/key&gt;
&lt;array/&gt;
&lt;key&gt;Has Injected Content&lt;/key&gt;
&lt;true/&gt;
&lt;key&gt;Level&lt;/key&gt;
&lt;string&gt;All&lt;/string&gt;
&lt;/dict&gt;
&lt;/dict&gt;
</code></pre></div></div>
<p>After following those simple steps you should have working extensions in Safari. Have fun on your old, “unsupported” Apple devices!</p>
</content>
</entry>
<entry>
<title>Create a Performance-Focused WordPress Blog for $4/month</title>
<link href="https://uglyduck.ca/performance-focused-wordpress-theme/"/>
<updated>2021-09-08T00:00:00-04:00</updated>
<id>https://uglyduck.ca/performance-focused-wordpress-theme</id>
<content type="html"><p><em>With my recent switch back to WordPress</em>, and having read Kev Quirk’s <a href="https://kevq.uk/core-web-vitals-and-wordpress/" target="_blank" rel="noreferrer noopener">latest post about Core Web Vitals</a>, I wanted to make sure my blog still prioritized speed and performance above all else. I’m happy to say that I have closely replicated the same speed of my original <em>static</em> Jekyll-based version of this blog.</p>
<p>And I’ve achieved this with barely any effort at all. All that’s needed is:</p>
<ul>
<li>a lightweight WordPress theme</li>
<li>two free WordPress plugins</li>
<li>cheap &amp; fast web hosting</li>
</ul>
<p>Let me breakdown each component so those of you interested can do the same without hitting any roadblocks.</p>
<h2 id="choosing-your-wordpress-theme">Choosing Your WordPress Theme</h2>
<p>Selecting an existing theme or trying to build your own can be a daunting experience. For my needs, I forced myself to find an existing theme and just run with it. This gives me more time to focus on writing instead of constantly tweaking the blog’s visuals.</p>
<p>I would suggest either using the default starter theme (as of this writing: Twenty Twenty One theme by Automattic) or looking for specific “lightweight” themes across the web. I highly recommend checking out Anders Norén’s <a href="https://andersnoren.se/teman/">theme collection</a>. Others can be found on the official <a href="https://wordpress.org/">wordpress.org</a> website.</p>
<h2 id="two-simple-plugins">Two Simple Plugins</h2>
<p>Once you have decided on a theme, you’ll also want to be sure to install two very important plugins that greatly help with performance:</p>
<ol>
<li>Jetpack Boost – <a href="https://wordpress.org/plugins/jetpack-boost/">wordpress.org/plugins/jetpack-boost/</a></li>
<li>Yoast SEO – <a href="https://wordpress.org/plugins/wordpress-seo/">wordpress.org/plugins/wordpress-seo/</a></li>
</ol>
<h2 id="jetpack-boost">Jetpack Boost</h2>
<p>This plugin is fantastic. Not only will it run a proper audit (based off Google’s Lighthouse tool) but also gives you the ability to activate optimized CSS, lazy image loading and deferring non-essential JavaScript (if applicable). All of this is done in a super-clean user interface directly inside your WordPress admin.</p>
<p>Check out my own testing site metrics below:</p>
<figure>
<img src="/public/images/jetpack-boost.webp" alt="Jetpack boost numbers" />
<figcaption>The Jetpack Boost metrics for this website (and yes, that 99 score for mobile kills me)</figcaption>
</figure>
<h2 id="yoast-seo">Yoast SEO</h2>
<p>If you’ve worked with WordPress at all you have surely heard of Yoast. Adding this plugin to your site is really a no-brainer. The automatic generation of meta data and SEO tags can save you a lot of time while also improving your overall Lighthouse score.</p>
<p>Here is the Lighthouse audit for the test WordPress website:</p>
<figure>
<img src="/public/images/ugly-duck-audit.webp" alt="Lighthouse audit" />
<figcaption>It might not be perfect, but it’s pretty damn close!</figcaption>
</figure>
<p>Those scores were achieved without any custom work on my end – I simply activated the plugin and let it work it’s magic. Great stuff.</p>
<h2 id="host-everything-on-easywp">Host Everything on EasyWP</h2>
<p>Up to this point we haven’t spent a single penny. Unfortunately, web hosting isn’t free and you’re going to need it if you actually want your website to have a home on the interwebs!</p>
<p>I know – I can hear your collective voices shouting out:</p>
<blockquote>
<p>Isn’t web hosting a little expensive just for my simple hobby blog/site?</p>
<p>– <cite>Some of you</cite></p>
</blockquote>
<p>I host this test blog for just <strong>$3.88/month</strong> and the service is actually good. Unreal, right? That’s the price of a medium coffee, so I’m pretty sure it won’t break the bank.</p>
<p>This “cheap” hosting is through EasyWP and the Starter plan ($3.88/month) comes with the following:</p>
<ul>
<li>10 GB SSD storage</li>
<li>50k visitors/month</li>
<li>Unlimited bandwidth</li>
<li>Backups (though not automatic)</li>
<li>SFTP access</li>
<li>EasyWP Cache plugin (and it’s actually good!)</li>
</ul>
<p>You will need to snag your own SSL certificate though, but this is easily obtained with something like <a href="https://letsencrypt.org">letsencrypt.org</a>.</p>
<p>Best of all, if in the future you require more features the next pricing tier is still only $7.88. Not too shabby!</p>
<h2 id="try-yourself">Try Yourself</h2>
<p>The fact that EasyWP gives you a free month trial, there really is no reason not to try spinning up your own blog or online store or whatever the heck you want to build. The more people with personal spaces on the web the better.</p>
</content>
</entry>
<entry>
<title>Stuffing an SSD Inside the Raspberry Pi 400</title>
<link href="https://uglyduck.ca/pi-400-internal-ssd/"/>
<updated>2021-08-13T00:00:00-04:00</updated>
<id>https://uglyduck.ca/pi-400-internal-ssd</id>
<content type="html"><p><em>I have successfully jammed an mSATA SSD into the main shell of my Raspberry Pi 400</em>. It wasn’t as straightforward as I thought it would be - in fact, most <em>real</em> hardware tinkerers will probably vomit in their mouths once they see how I achieved this…</p>
<p>But I’m happy with my build. Those with better skills and knowledge can most likely improve upon this concept (and please do if you can - I’d love to see it!)</p>
<p>Enough chit-chat - on to the build!</p>
<h2 id="the-finished-masterpiece">The Finished “Masterpiece”</h2>
<p>Below you can see the final look of my modded Raspberry Pi 400, which I have personally named the <strong>Raspberry Pi 400X</strong>:</p>
<figure>
<img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-400-final.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/pi-400-final.webp 320w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/pi-400-final.webp 540w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/pi-400-final.webp 760w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/pi-400-final.webp 980w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-400-final.webp 1200w" sizes="100vw" alt="Pi 400 with internal SSD" width="4032" height="3024" crossorigin="anonymous" />
<figcaption>The finished Raspberry Pi 400X (<a href="/public/images/pi-400-final.webp">direct link to image</a>)</figcaption>
</figure>
<p>Do you see that ugly black USB-C to USB-A cable jutting out from the top? That little guy connects directly to the mSATA SSD <em>inside</em> the plastic keyboard structure and allows us to boot via USB. It also gives us the flexibility to easily <em>unplug</em> the internal SSD for times when we desire to boot from micro SD or a different USB device altogether.</p>
<p>So, how did I make this?</p>
<h2 id="the-shopping-list">The Shopping List</h2>
<p>Before we deep dive into the terrible hardware modifications I’ve made to my Pi, I’ll list out all the items/tools I used during the making of this monstrosity:</p>
<ul>
<li>mSATA 64GB SSD Half Size (KingSpec) → <a href="https://www.aliexpress.com/item/32385499968.html?spm=a2g0s.9042311.0.0.210e4c4dIH8xWv">AliExpress</a></li>
<li>mSATA to USB 3.1 enclosure (gutted) → <a href="https://www.amazon.ca/gp/product/B07BBM3BVS/ref=ppx_yo_dt_b_asin_title_o09_s00?ie=UTF8&amp;psc=1">Amazon</a></li>
<li>Dremel rotary tool with soft sanding bits</li>
<li>Metal cutting scissors / knife</li>
<li>Electrical tape</li>
<li>Patience</li>
</ul>
<h2 id="modding-the-raspberry-pi-400">Modding the Raspberry Pi 400</h2>
<p>The final product requires us to attach the half size mSATA SSD to the (gutted) enclosure and then insert that directly inside the Pi (next to the lock port / ethernet). Our first step will be to disassemble the Pi 400, safely remove the keyboard module and remove the metal heatsink.</p>
<p>With your trusty metal cutting scissors (or whatever tool you prefer) you will need to cut out room for our enclosure internals to fit within:</p>
<figure>
<img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-heatsink.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/pi-heatsink.webp 320w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/pi-heatsink.webp 540w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/pi-heatsink.webp 760w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/pi-heatsink.webp 980w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-heatsink.webp 1200w" sizes="100vw" alt="The cutout heatsink" width="4032" height="3024" crossorigin="anonymous" />
<figcaption>I drew the outline of the enclosure on the metal first before cutting. (<a href="/public/images/pi-heatsink.webp">direct link to image</a>)</figcaption>
</figure>
<p>Next you will need to carefully remove the Pi board itself from the red part of the case. Once placed safely aside, it’s time to bust out our sanding dremel and remove the jutting plastic blocking our soon-to-be-added SSD. Remember to wear a mask during this phase, since breathing in plastic dust and fumes is not fun!</p>
<p><strong>Important to note:</strong> this will remove one of the screw slots needed to secure the heatsink into the board. Not a big deal if you ask me…</p>
<figure>
<img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-plastic-back.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/pi-plastic-back.webp 320w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/pi-plastic-back.webp 540w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/pi-plastic-back.webp 760w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/pi-plastic-back.webp 980w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-plastic-back.webp 1200w" sizes="100vw" alt="Plastic dremeled back" width="4032" height="3024" crossorigin="anonymous" />
<figcaption>Here you can see my terrible sanding job on the far left plastic snap-lock (<a href="/public/images/pi-plastic-back.webp">direct link to image</a>)</figcaption>
</figure>
<p>While you have the Pi board removed you should also cut out a slot for the USB-C to USB-A cable to connect our mSATA to one of our USB 3 ports on the Pi. For this I’ve opted to butcher the lock port (will I ever really use that anyway?)</p>
<figure>
<img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-outer-port.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/pi-outer-port.webp 320w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/pi-outer-port.webp 540w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/pi-outer-port.webp 760w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/pi-outer-port.webp 980w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-outer-port.webp 1200w" sizes="100vw" alt="Back of Pi opening" width="2016" height="1512" crossorigin="anonymous" />
<figcaption>Don't judge...I'm sure most people could do a cleaner job! (<a href="/public/images/pi-outer-port.webp">direct link to image</a>)</figcaption>
</figure>
<p>Now all that’s left is to insert our gutted mSATA enclosure (with the half size SSD attached of course)
, tape it down with some hideous electrical tape and close this bad boy back up!</p>
<figure>
<img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-internal-ssd.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/pi-internal-ssd.webp 320w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/pi-internal-ssd.webp 540w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/pi-internal-ssd.webp 760w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/pi-internal-ssd.webp 980w,
https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/pi-internal-ssd.webp 1200w" sizes="100vw" alt="The internal SSD" width="4032" height="3024" crossorigin="anonymous" />
<figcaption>Take note of the tiny triangle piece of foam on the Type-C connector. This helps avoid direct contact with the back of the keyboard module! (<a href="/public/images/pi-internal-ssd.webp">direct link to image</a>)</figcaption>
</figure>
<p>That’s it! You now have the portable power of the Raspberry Pi 400, but now with the speed and performance of an SSD!</p>
<h2 id="the-performance-gains">The Performance Gains</h2>
<table>
<thead>
<tr>
<th>Storage Type</th>
<th>Seq. Write</th>
<th>Random W</th>
<th>Random R</th>
</tr>
</thead>
<tbody>
<tr>
<td>Micro SD Card</td>
<td>17818 KB/sec</td>
<td>812 IOPS</td>
<td>2335 IOPS</td>
</tr>
<tr>
<td>mSATA SSD</td>
<td>206738 KB/sec</td>
<td>14615 IOPS</td>
<td>17925 IOPS</td>
</tr>
</tbody>
</table>
<p>I think it’s pretty clear that the SSD blows the default micro SD card out of the water…</p>
<h2 id="closing-thoughts">Closing Thoughts</h2>
<p>Now clearly you could just plug-in an external SSD and walk away with the same performance boosts - but where is the fun in that?</p>
<p>If anyone decides to improve on this or make a cleaner approach, please do share it with me. I’d love to see it!</p>
</content>
</entry>
<entry>
<title>Sharing The Things We Use</title>
<link href="https://uglyduck.ca/sharing-the-things-we-use/"/>
<updated>2021-07-24T00:00:00-04:00</updated>
<id>https://uglyduck.ca/sharing-the-things-we-use</id>
<content type="html"><p><em>I always love stumbling across personal websites</em> that include some form of a “uses” page. A place where the author lists out all the tools they use on a regular basis - whether it be hardware, software or something else entirely. It allows readers to get a slightly more personal peek into the daily work-life of that author and maybe even provides better context for <em>how</em> they work.</p>
<p>Since I realized how much I enjoy reading other people’s <em>uses</em> pages, I’ve decided to finally publish my own! My list of hardware and software is fairly boring and predictable for a designer/developer - but sharing is caring! My hope is that even one personal out in the great-wide web can find something useful (pun intended!) or least inspiring about my personal setup.</p>
<p>Fell free to check it out: <a href="/uses">The Things I Use →</a></p>
<p><em>PS. I plan to add a desktop picture of my complete setup once I find the time!</em></p>
</content>
</entry>
<entry>
<title>PS4 Download UI with Pure CSS</title>
<link href="https://uglyduck.ca/ps4-download-ui/"/>
<updated>2021-06-20T00:00:00-04:00</updated>
<id>https://uglyduck.ca/ps4-download-ui</id>
<content type="html"><p><em>Overall, I’m fairly impressed with the user interface design</em> of Sony’s PS4 system OS. It’s minimal and keeps the content front and center. Even with it’s sometimes spotty performance hiccups, I’ve come to enjoy interacting with it.</p>
<p>One of the key UI items I’ve always been a fan of is the download progress view under the <code class="language-plaintext highlighter-rouge">Notifications</code> settings. So I figured I’d try my hand at recreating this with pure CSS. Here is the final result:</p>
<p class="codepen" data-height="474" data-theme-id="light" data-default-tab="result" data-user="bradleytaunt" data-slug-hash="qBroORG" style="height: 474px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="PS4 Download UI">
<span>See the Pen <a href="https://codepen.io/bradleytaunt/pen/qBroORG">
PS4 Download UI</a> by Bradley Taunt (<a href="https://codepen.io/bradleytaunt">@bradleytaunt</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<p>Although I’ve added some of my own improvements (typography spacing, tweaks to the progress bar animation) - the concept it still pretty close to the original.</p>
<p>But enough chit-chat, let’s walkthrough how to make it!</p>
<h2 id="the-html">The HTML</h2>
<p>As with most of my demos, the HTML is very minimal and straightforward. The PS4 system OS download view needs to show the following:</p>
<ol>
<li>The game’s title</li>
<li>Full game size, amount downloaded and time remaining</li>
<li>Visual progress bar</li>
</ol>
<p>So we will place the game’s title inside our <code class="language-plaintext highlighter-rouge">h2</code> with a class of <code class="language-plaintext highlighter-rouge">title</code> (shocking, I know). The details about game size, downloaded amount and time remaining gets placed under a parent <code class="language-plaintext highlighter-rouge">div</code> with an accompanying <code class="language-plaintext highlighter-rouge">details</code> class. Finally, we create our progress bar by including a parent <code class="language-plaintext highlighter-rouge">div</code> with a class of <code class="language-plaintext highlighter-rouge">progress</code> that contains a child <code class="language-plaintext highlighter-rouge">div</code> with a class of <code class="language-plaintext highlighter-rouge">inner-progress</code>.</p>
<p>Pretty clean and easy to understand.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"wrapper"</span><span class="nt">&gt;</span>
<span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"https://upload.wikimedia.org/wikipedia/commons/0/00/PlayStation_logo.svg"</span> <span class="na">alt=</span><span class="s">"PS4"</span> <span class="na">class=</span><span class="s">"logo"</span><span class="nt">&gt;</span>
<span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"title"</span><span class="nt">&gt;</span>Detroit: Become Human<span class="nt">&lt;/h2&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"details"</span><span class="nt">&gt;</span>
<span class="nt">&lt;p&gt;</span>Update File<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;p&gt;</span>13.45/17.50 GB (21 Minutes Left)<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress"</span><span class="nt">&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"inner-progress"</span><span class="nt">&gt;&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>
<h2 id="the-css">The CSS</h2>
<p>Now it’s time to utilize all those classes in the HTML above to craft our PS4 UI recreation. I’ll break this section down into digestible chunks to avoid overwhelming you by vomiting out a bunch of CSS spaghetti.</p>
<p>First we’ll add a bunch of QOL improvements to help better showcase the demo (adding custom fonts, center content etc.).</p>
<p>This part is completely <em>optional</em>:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* Import fonts */</span>
<span class="k">@import</span> <span class="sx">url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@200;400&amp;display=swap')</span><span class="p">;</span>
<span class="c">/* Gradient background styling, height overrides */</span>
<span class="nt">body</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="n">linear-gradient</span><span class="p">(</span><span class="m">#226AB6</span> <span class="m">0%</span><span class="p">,</span> <span class="m">#144E8A</span> <span class="m">100%</span><span class="p">)</span> <span class="nb">no-repeat</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="nl">font-family</span><span class="p">:</span> <span class="s2">'Source Sans Pro'</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="m">200</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">100vh</span><span class="p">;</span>
<span class="p">}</span>
<span class="c">/* Wrapper to center content */</span>
<span class="nc">.wrapper</span> <span class="p">{</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">max-width</span><span class="p">:</span> <span class="m">800px</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">4rem</span> <span class="m">0</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="c">/* Optional PS4 logo */</span>
<span class="nc">.logo</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="nl">filter</span><span class="p">:</span> <span class="nb">invert</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">2rem</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">opacity</span><span class="p">:</span> <span class="m">0.5</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">60px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now for the styling that <em>actually matters</em>. First we will style the game’s title and accompanying details (<code class="language-plaintext highlighter-rouge">flexbox</code> to the rescue again!):</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">h2</span><span class="nc">.title</span> <span class="p">{</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="m">400</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.details</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
<span class="nl">justify-content</span><span class="p">:</span> <span class="n">space-between</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0.2rem</span> <span class="m">0</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.details</span> <span class="nt">p</span> <span class="p">{</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Not a whole lot of code to get things looking proper, eh? Next we move on to the progress bar. This is <em>slightly</em> more interesting since we are going to utilize the <code class="language-plaintext highlighter-rouge">before</code> pseudo element - which sounds more complex than it actually is. Pay close attention to the pseudo element and how it calls the <code class="language-plaintext highlighter-rouge">progress-bar-shine</code> animation - more on that later.</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.progress</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#226AB6</span><span class="p">;</span>
<span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">2rem</span> <span class="m">0</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.progress</span><span class="nd">:before</span> <span class="p">{</span>
<span class="nl">animation</span><span class="p">:</span> <span class="n">progress-bar-shine</span> <span class="m">2.5s</span> <span class="n">infinite</span><span class="p">;</span>
<span class="nl">background</span><span class="p">:</span> <span class="n">linear-gradient</span><span class="p">(</span><span class="n">to</span> <span class="nb">left</span><span class="p">,</span> <span class="no">white</span> <span class="m">0%</span><span class="p">,</span> <span class="nb">transparent</span> <span class="m">100%</span><span class="p">);</span>
<span class="nl">border-radius</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">content</span><span class="p">:</span><span class="s2">''</span><span class="p">;</span>
<span class="nl">filter</span><span class="p">:</span> <span class="n">blur</span><span class="p">(</span><span class="m">8px</span><span class="p">);</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="nl">opacity</span><span class="p">:</span> <span class="m">0.8</span><span class="p">;</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
<span class="nl">transform</span><span class="p">:</span><span class="n">translateX</span><span class="p">(</span><span class="m">0</span><span class="p">);</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">50px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.inner-progress</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="nl">opacity</span><span class="p">:</span> <span class="m">0.6</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">450px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Almost finished! We just need to animate that <code class="language-plaintext highlighter-rouge">before</code> pseudo element with a simple <code class="language-plaintext highlighter-rouge">keyframes</code> at-rule:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@keyframes</span> <span class="n">progress-bar-shine</span> <span class="p">{</span>
<span class="nt">to</span> <span class="p">{</span>
<span class="nl">transform</span><span class="p">:</span><span class="n">translateX</span><span class="p">(</span><span class="m">450px</span><span class="p">);</span>
<span class="nl">opacity</span><span class="p">:</span><span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>Although far from perfect, this experiment still explores what can be created (or in this case, <em>re</em>created) in the browser using just pure CSS. Remember, you don’t have to reach for JavaScript just because you can!</p>
</content>
</entry>
<entry>
<title>CSS Slope Graphs</title>
<link href="https://uglyduck.ca/css-slope-graphs/"/>
<updated>2021-06-07T00:00:00-04:00</updated>
<id>https://uglyduck.ca/css-slope-graphs</id>
<content type="html"><p><em>I am a huge sucker for simplistic and beautifully designed visual data on the web</em>. Most data tends to be graphed via line or bar systems - which is fine - but I think slope graphs are highly underrated. Let’s change that, shall we?</p>
<h2 id="the-demo">The Demo</h2>
<p>I’m basing this demo off the design patterns found in <a href="https://www.edwardtufte.com/tufte/books_vdqi">Edward Tufte’s visualization work</a>, specifically his slope graph designs:</p>
<p class="codepen" data-height="629" data-theme-id="light" data-default-tab="result" data-user="bradleytaunt" data-slug-hash="jOBzXMe" style="height: 629px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="CSS Slopegraphs">
<span>See the Pen <a href="https://codepen.io/bradleytaunt/pen/jOBzXMe">
CSS Slopegraphs</a> by Bradley Taunt (<a href="https://codepen.io/bradleytaunt">@bradleytaunt</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<h2 id="the-html">The HTML</h2>
<p>For this concept we will actually be building this graph out of <code class="language-plaintext highlighter-rouge">tables</code> - crazy, right? The greatest benefit of rendering all the data inside of a <code class="language-plaintext highlighter-rouge">table</code> element is the ability to easily support smaller screens and mobile devices. Larger viewports will get to see the pretty slope graph, while those below a certain threshold will view a simple table.</p>
<p>(But more on that in the CSS section)</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;p&gt;</span>Sales of the leading frozen pizza brands of the United States from 2011 to 2017 (in million US dollars) <span class="nt">&lt;br&gt;&lt;em&gt;</span>Source: Statisa 2018<span class="nt">&lt;/em&gt;&lt;/p&gt;</span>
<span class="nt">&lt;table&gt;</span>
<span class="nt">&lt;thead&gt;</span>
<span class="nt">&lt;tr&gt;</span>
<span class="nt">&lt;th&gt;</span>Pizza Brand<span class="nt">&lt;/th&gt;</span>
<span class="nt">&lt;th&gt;</span>2011<span class="nt">&lt;/th&gt;</span>
<span class="nt">&lt;th&gt;</span>2017<span class="nt">&lt;/th&gt;</span>
<span class="nt">&lt;/tr&gt;</span>
<span class="nt">&lt;/thead&gt;</span>
<span class="nt">&lt;tbody&gt;</span>
<span class="nt">&lt;tr&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-set=</span><span class="s">"677.0"</span><span class="nt">&gt;</span>DiGiorno<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;td&gt;&lt;span&gt;</span>677.0<span class="nt">&lt;/span&gt;&lt;/td&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-name=</span><span class="s">"DiGiorno"</span><span class="nt">&gt;</span>1014.6<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;/tr&gt;</span>
<span class="nt">&lt;tr&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-set=</span><span class="s">"294.8"</span><span class="nt">&gt;</span>Private Label<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;td&gt;&lt;span&gt;</span>294.8<span class="nt">&lt;/span&gt;&lt;/td&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-name=</span><span class="s">"Private Label"</span><span class="nt">&gt;</span>524.8<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;/tr&gt;</span>
<span class="nt">&lt;tr&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-set=</span><span class="s">"286.1"</span><span class="nt">&gt;</span>Red Baron<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;td&gt;&lt;span&gt;</span>286.1<span class="nt">&lt;/span&gt;&lt;/td&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-name=</span><span class="s">"Red Baron"</span><span class="nt">&gt;</span>572.3<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;/tr&gt;</span>
<span class="nt">&lt;tr&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-set=</span><span class="s">"257.9"</span><span class="nt">&gt;</span>Tombstone<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;td&gt;&lt;span&gt;</span>257.9<span class="nt">&lt;/span&gt;&lt;/td&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-name=</span><span class="s">"Tombstone"</span><span class="nt">&gt;</span>270.6<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;/tr&gt;</span>
<span class="nt">&lt;tr&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-set=</span><span class="s">"164.5"</span><span class="nt">&gt;</span>Totino's Party Pizza<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;td&gt;&lt;span&gt;</span>164.5<span class="nt">&lt;/span&gt;&lt;/td&gt;</span>
<span class="nt">&lt;td</span> <span class="na">data-name=</span><span class="s">"Totino's Party Pizza"</span><span class="nt">&gt;</span>347.2<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;/tr&gt;</span>
<span class="nt">&lt;/tbody&gt;</span>
<span class="nt">&lt;/table&gt;</span>
</code></pre></div></div>
<p>As you can see, nothing too fancy is happpening here. Pay close attention to the <code class="language-plaintext highlighter-rouge">data-set</code> and <code class="language-plaintext highlighter-rouge">data-name</code> variables though - those will be important for the CSS portion of this design, mainly the rendering of the line elements.</p>
<h2 id="the-css">The CSS</h2>
<p>To avoid overwhelming your brain all-at-once, let’s break the CSS down into bite-sized chunks, starting with the base styling:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@import</span> <span class="sx">url('https://opentype.netlify.com/et-book/index.css')</span><span class="p">;</span>
<span class="o">*</span> <span class="p">{</span>
<span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">html</span> <span class="p">{</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">body</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#fffff8</span><span class="p">;</span>
<span class="nl">font-family</span><span class="p">:</span> <span class="s1">"et-book"</span><span class="p">,</span> <span class="nb">serif</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">max-width</span><span class="p">:</span> <span class="m">800px</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span> <span class="m">0.5rem</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">p</span> <span class="p">{</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">18px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">4rem</span> <span class="m">0</span> <span class="m">6rem</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">table</span> <span class="p">{</span>
<span class="nl">border-collapse</span><span class="p">:</span> <span class="nb">collapse</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pretty basic stuff.</p>
<p>Now we need to design how our slope graph will look on larger screens / desktops. For this instance, we will target these larger devices with a <code class="language-plaintext highlighter-rouge">min-width</code> media query of <code class="language-plaintext highlighter-rouge">800px</code>. The rest of the CSS might look a little confusing but I assure you it is quite simple.</p>
<ol>
<li>On larger devices we hide the first <code class="language-plaintext highlighter-rouge">thead tr th</code> element with <code class="language-plaintext highlighter-rouge">display: none</code></li>
<li>The first and second <code class="language-plaintext highlighter-rouge">td</code> elements inside each <code class="language-plaintext highlighter-rouge">tbody</code> row need to be set as <code class="language-plaintext highlighter-rouge">position: absolute</code> to avoid duplicate content</li>
<li>The inner <code class="language-plaintext highlighter-rouge">span</code> that we include in our HTML inside the second <code class="language-plaintext highlighter-rouge">tbody tr td</code> also needs to be <code class="language-plaintext highlighter-rouge">display: none</code></li>
<li>Remember that <code class="language-plaintext highlighter-rouge">data-set</code> variable? We now use that for our <code class="language-plaintext highlighter-rouge">:before</code> pseudo element for <code class="language-plaintext highlighter-rouge">table tbody tr td:nth-of-type(1)</code></li>
<li>Remember that <code class="language-plaintext highlighter-rouge">data-name</code> variable? We now use that for our <code class="language-plaintext highlighter-rouge">:before</code> pseudo element for <code class="language-plaintext highlighter-rouge">table tbody tr td:nth-of-type(3)</code></li>
<li>After that, you can see the simple customization we include to render the angle / position of the slope lines and the corresponding labels</li>
</ol>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@media</span><span class="p">(</span><span class="n">min-width</span><span class="p">:</span><span class="m">800px</span><span class="p">)</span> <span class="p">{</span>
<span class="nt">table</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">25rem</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">table</span> <span class="nt">thead</span> <span class="nt">th</span> <span class="p">{</span>
<span class="nl">border-bottom</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="no">lightgrey</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">24px</span><span class="p">;</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">-50px</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">45%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">table</span> <span class="nt">thead</span> <span class="nt">th</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">)</span><span class="p">{</span> <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">table</span> <span class="nt">thead</span> <span class="nt">th</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span><span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">table</span> <span class="nt">thead</span> <span class="nt">th</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">3</span><span class="o">)</span><span class="p">{</span> <span class="nl">right</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">text-align</span><span class="p">:</span> <span class="nb">right</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:nth-of-type</span><span class="o">(</span><span class="err">1</span><span class="o">),</span>
<span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:nth-of-type</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;}</span>
<span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:nth-of-type</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="nt">span</span> <span class="p">{</span> <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:nth-of-type</span><span class="o">(</span><span class="err">1</span><span class="o">)</span><span class="nd">:before</span> <span class="p">{</span>
<span class="nl">content</span><span class="p">:</span> <span class="n">attr</span><span class="p">(</span><span class="n">data-set</span><span class="p">);</span>
<span class="nl">margin-right</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:nth-of-type</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="p">{</span> <span class="nl">padding-left</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:nth-of-type</span><span class="o">(</span><span class="err">3</span><span class="o">)</span> <span class="p">{</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
<span class="nl">right</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:nth-of-type</span><span class="o">(</span><span class="err">3</span><span class="o">)</span><span class="nd">:before</span> <span class="p">{</span>
<span class="nl">content</span><span class="p">:</span> <span class="n">attr</span><span class="p">(</span><span class="n">data-name</span><span class="p">);</span>
<span class="nl">margin-right</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span>
<span class="p">}</span>
<span class="c">/* Custom individual slopes -- Left */</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">),</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">60px</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">),</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">140px</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">3</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">),</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">3</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">165px</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">4</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">),</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">4</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">220px</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">5</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">),</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">5</span><span class="o">)</span> <span class="nt">td</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">270px</span><span class="p">;</span> <span class="p">}</span>
<span class="c">/* Custom individual slopes -- Right */</span>
<span class="o">[</span><span class="nt">data-name</span><span class="o">=</span><span class="s1">"DiGiorno"</span><span class="o">]</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="p">}</span>
<span class="o">[</span><span class="nt">data-name</span><span class="o">=</span><span class="s1">"Red Baron"</span><span class="o">]</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">65px</span><span class="p">;</span> <span class="p">}</span>
<span class="o">[</span><span class="nt">data-name</span><span class="o">=</span><span class="s1">"Private Label"</span><span class="o">]</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="p">}</span>
<span class="o">[</span><span class="nt">data-name</span><span class="o">=</span><span class="s1">"Tombstone"</span><span class="o">]</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">180px</span><span class="p">;</span> <span class="p">}</span>
<span class="o">[</span><span class="nt">data-name</span><span class="o">=</span><span class="s1">"Totino's Party Pizza"</span><span class="o">]</span> <span class="p">{</span> <span class="nl">top</span><span class="p">:</span> <span class="m">150px</span><span class="p">;</span> <span class="p">}</span>
<span class="c">/* The custom visual lines */</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:after</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="no">black</span><span class="p">;</span>
<span class="nl">content</span><span class="p">:</span> <span class="s2">''</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">1px</span><span class="p">;</span>
<span class="nl">left</span><span class="p">:</span> <span class="m">14.5%</span><span class="p">;</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">70%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">1</span><span class="o">)</span><span class="nd">:after</span> <span class="p">{</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">40px</span><span class="p">;</span>
<span class="nl">transform</span><span class="p">:</span> <span class="n">rotate</span><span class="p">(</span><span class="m">-6deg</span><span class="p">);</span>
<span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">2</span><span class="o">)</span><span class="nd">:after</span> <span class="p">{</span>
<span class="nl">left</span><span class="p">:</span> <span class="m">17.5%</span><span class="p">;</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">130px</span><span class="p">;</span>
<span class="nl">transform</span><span class="p">:</span> <span class="n">rotate</span><span class="p">(</span><span class="m">-4deg</span><span class="p">);</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">65%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">3</span><span class="o">)</span><span class="nd">:after</span> <span class="p">{</span>
<span class="nl">left</span><span class="p">:</span> <span class="m">15%</span><span class="p">;</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">125px</span><span class="p">;</span>
<span class="nl">transform</span><span class="p">:</span> <span class="n">rotate</span><span class="p">(</span><span class="m">-10.25deg</span><span class="p">);</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">70%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">4</span><span class="o">)</span><span class="nd">:after</span> <span class="p">{</span>
<span class="nl">left</span><span class="p">:</span> <span class="m">16%</span><span class="p">;</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">210px</span><span class="p">;</span>
<span class="nl">transform</span><span class="p">:</span> <span class="n">rotate</span><span class="p">(</span><span class="m">-4deg</span><span class="p">);</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">68%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">tbody</span> <span class="nt">tr</span><span class="nd">:nth-child</span><span class="o">(</span><span class="err">5</span><span class="o">)</span><span class="nd">:after</span> <span class="p">{</span>
<span class="nl">left</span><span class="p">:</span> <span class="m">22%</span><span class="p">;</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">222px</span><span class="p">;</span>
<span class="nl">transform</span><span class="p">:</span> <span class="n">rotate</span><span class="p">(</span><span class="m">-16deg</span><span class="p">);</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">56%</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>All that’s left are some minor styles to make everything look nice on mobile:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@media</span><span class="p">(</span><span class="n">max-width</span><span class="p">:</span><span class="m">800px</span><span class="p">)</span> <span class="p">{</span>
<span class="nt">p</span> <span class="p">{</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">2rem</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">table</span> <span class="nt">td</span><span class="o">,</span> <span class="nt">table</span> <span class="nt">th</span> <span class="p">{</span>
<span class="nl">border-bottom</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="n">grey</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">table</span> <span class="nt">td</span><span class="nd">:last-of-type</span><span class="o">,</span> <span class="nt">table</span> <span class="nt">th</span><span class="nd">:last-of-type</span> <span class="p">{</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">right</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="not-the-most-practical">Not the most practical</h2>
<p>This slope graph concept is far from perfect for use in real-world situations. The fact that you need to manually render each point of data yourself makes this implementation quite annoying for more in-depth projects.</p>
<p>But it was fun to mess around with and create, so who cares!</p>
</content>
</entry>
<entry>
<title>SOMA Inspired Terminal Display with CSS</title>
<link href="https://uglyduck.ca/soma-terminal-css/"/>
<updated>2021-05-29T00:00:00-04:00</updated>
<id>https://uglyduck.ca/soma-terminal-css</id>
<content type="html"><p><em>A few years back I played (and loved) <a href="https://store.steampowered.com/app/282140/SOMA/">SOMA</a></em>, a first-person sci-fi horror-adventure game. The story was intriguing and the developers nailed the overall atmosphere of Pathos-II. Though both those aspects were great, what I found the most enjoyable were the interactive computers and displays found sprinkled throughout the world.</p>
<p>Three years ago I wanted to see if I could recreate one of those terminal displays with HTML &amp; CSS. And I did just that.</p>
<p>So, why am I writing about this <em>three years later</em>? Well, I never did a proper write-up explaining how I achieved it. I’m sure someone out there in the wild west of the web could get some value out of this tutorial, right? I hope so!</p>
<h2 id="the-live-demo">The Live Demo</h2>
<p class="codepen" data-height="755" data-theme-id="dark" data-default-tab="result" data-user="bradleytaunt" data-slug-hash="ZEeLgmz" style="height: 755px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="SOMA Inspired Terminal Display (Updated)">
<span>See the Pen <a href="https://codepen.io/bradleytaunt/pen/ZEeLgmz">
SOMA Inspired Terminal Display (Updated)</a> by Bradley Taunt (<a href="https://codepen.io/bradleytaunt">@bradleytaunt</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<h2 id="the-html">The HTML</h2>
<p>Nothing fancy going on here - just some simple <code class="language-plaintext highlighter-rouge">div</code> elements holding a few paragraphs and spans:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;div class="outer-frame"&gt;
&lt;div class="screen"&gt;
&lt;div class="screen-guts"&gt;
&lt;h2&gt;Key Control&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;Panel Chip&lt;/span&gt; (Connector)&lt;/p&gt;
&lt;p class="column"&gt;
Security Keys: 023-027&lt;br&gt;C819738-23
&lt;br&gt;
&lt;span class="error"&gt;Error: Key Expired&lt;/span&gt;
&lt;br&gt;
&lt;em&gt;Please update...&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;
&lt;span&gt;Permission&lt;/span&gt;
&lt;br&gt;
Standard ThetaCipher
&lt;br&gt;
&lt;span&gt;Secop-Jstrohweier&lt;/span&gt;
&lt;/p&gt;
&lt;button class="update-button"&gt;Update Chip&lt;/button&gt;
&lt;/div&gt;
&lt;div class="bottom-controls"&gt;
&lt;button class="back-button"&gt;Back 背部&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</code></pre></div></div>
<h2 id="the-css">The CSS</h2>
<p>Let’s just dump all the CSS right here (don’t worry, it looks far more complicated than it really is):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@import url('https://opentype.netlify.com/sansation/index.css');
* {
box-sizing: border-box;
}
body {
background: #333;
font-family: "Sansation", sans-serif;
padding: 3rem 0;
}
button {
appearance: none;
border: 0;
cursor: pointer;
font-family: "Inconsolata", "Consolas", mono;
font-size: 18px;
transition: 0.3s ease all;
}
.outer-frame,
.screen {
display: block;
margin: 0 auto;
position: relative;
}
.outer-frame {
background: #ccc url("https://preview.ibb.co/iS9nz7/screen_grunge.webp") no-repeat center;
background-size: cover;
border-radius: 5px;
box-shadow: 0 5px 20px rgba(0,0,0,0.6);
height: 560px;
width: 750px;
}
.outer-frame:before {
background: rgba(0,0,0,0.3);
border-radius: 5px;
box-shadow: inset 5px 5px 10px rgba(0,0,0,0.4), 1px 1px 1px rgba(255,255,255,0.3);
content: '';
height: 538px;
left: 15px;
position: absolute;
top: 11px;
width: 720px;
}
.screen {
background: #000 url("https://image.ibb.co/gOqSz7/screen_dust.webp") no-repeat center;
background-size: cover;
border-radius: 5px;
height: 450px;
left: 75px;
padding: 60px;
position: absolute;
top: 55px;
width: 600px;
}
.screen:after {
background: url("https://s13.postimg.org/85ryuy1o7/dust.webp") no-repeat center;
background-size: cover;
border-radius: 5px;
box-shadow: inset 0 0 20px rgba(0,0,0,0.5), inset 5px 5px 10px rgba(0,0,0,0.4), 1px 1px 1px rgba(255,255,255,0.3);
content: '';
height: 100%;
left: 0;
opacity: 0.8;
pointer-events: none;
position: absolute;
top: 0;
width: 100%;
z-index: 2;
}
.screen-guts {
border-bottom: 2px solid #8fc8c8;
border-top: 2px solid #8fc8c8;
height: calc(100% - 50px);
position: relative;
width: 100%;
z-index: 0;
}
.screen-guts:after {
border: 2px solid #8fc8c8;
color: #8fc8c8;
content: '键';
padding: 5px 8px;
position: absolute;
right: 0;
top: 5px;
}
.screen-guts h2 {
background: #8fc8c8;
color: #fff;
display: inline-block;
font-size: 24px;
margin: 0 0 30px 0;
padding: 10px;
}
.screen-guts p {
color: #8fc8c8;
font-size: 18px;
margin: 0 0 30px 0;
}
.screen-guts p.column {
column-width: 200px;
}
.screen-guts p span {
text-transform: uppercase;
}
.screen-guts p span.error {
color: #f37c56;
}
.screen-guts p span em {
text-transform: none;
}
.screen-guts button.update-button {
background: #889314;
bottom: 10px;
color: #fff;
padding: 15px 20px;
position: absolute;
right: 0;
}
.screen-guts button.update-button:hover {
background: #515905;
}
.bottom-controls {
background: #8fc8c8;
border-radius: 0 0 5px 5px;
bottom: 0;
display: flex;
left: 0;
height: 50px;
padding: 5px;
position: absolute;
width: 100%;
}
.bottom-controls button.back-button {
background: rgba(0,0,0,0.2);
color: #fff;
line-height: 20px;
padding: 8px 20px;
text-transform: uppercase;
}
.bottom-controls button.back-button:hover {
background: rgba(0,0,0,0.6);
}
</code></pre></div></div>
<p>The overall styling is fairly simple (which is great for browser support) with only a couple pseudo elements. It’s important to include the custom <code class="language-plaintext highlighter-rouge">Sansation</code> typeface, since this is what the game uses for most UI elements and terminals. (I’m loading in this font via my <a href="https://opentype.netlify.app">OpenType side project</a>)</p>
<p>The most important properties that truly bring this design together are those associated with the <code class="language-plaintext highlighter-rouge">.outer-frame</code> and <code class="language-plaintext highlighter-rouge">.screen</code> pseudo elements. Both of these elements use custom <code class="language-plaintext highlighter-rouge">background-images</code> that give a little more depth and life to the terminal - in this case fingerprints, dust and smudges.</p>
<p>Feel free to swap these image textures out with your own and alter the terminal as you see fit!</p>
</content>
</entry>
<entry>
<title>My Changing Opinion on Personal Website Design</title>
<link href="https://uglyduck.ca/personal-website-opinions/"/>
<updated>2021-05-19T00:00:00-04:00</updated>
<id>https://uglyduck.ca/personal-website-opinions</id>
<content type="html"><p><em>Hey would you look at that</em> - my personal blog has been redesigned <em>again</em>! Although I am still using good ol’ Jekyll for the backend, I have now added a more fleshed-out CSS design which also includes a set of open source custom typefaces.</p>
<p><strong>Gasp!</strong> “How <em>could</em> you?!” I hear you ask. Let me explain.</p>
<h2 id="personal-sites-should-feel-personal">Personal sites should feel personal</h2>
<p>I can see how this change might seem hypocritical (it took some convincing myself) but I decided to follow in to footsteps of Kev Quirk and <a href="https://kevq.uk/adding-some-whimsy-character/">added a little whimsy and character</a> to my website. After all, personal websites should <em>feel personal</em>. My obsession with barebones HTML &amp; CSS serves its purpose on other public projects, but seems limiting for my own little space on the interwebs.</p>
<h2 id="banned-from-my-own-club">Banned from my own club</h2>
<p>I had originally converted this blog’s design to use <em>zero</em> CSS and instead rely solely on default browser styling. The main reasoning for doing so, was to have the ability to include my own personal website in the <a href="https://xhtml.club">XHTML Club</a> project. (I never said it was a <em>good</em> reason)</p>
<p>After giving it some thought, I’ve decided that this limitation seemed too extreme even for me.</p>
<h2 id="moving-forward">Moving forward</h2>
<p>I know I always say “With this new design, I can finally focus on just writing content!” - but this is a lie. I’ll probably be fiddling with my personal website until the day I die. The good news is that I <em>do</em> have a few tutorial blog posts lined up to publish soon - so be on the lookout for those!</p>
<p>Thanks for reading through my pointless ramblings about personal websites. It’s good to just vent about these things sometimes…</p>
</content>
</entry>
<entry>
<title>Shiny, Animated CSS Buttons</title>
<link href="https://uglyduck.ca/shiny-css-buttons/"/>
<updated>2021-04-27T00:00:00-04:00</updated>
<id>https://uglyduck.ca/shiny-css-buttons</id>
<content type="html"><p><em>Everyone can appreciate fancy, animated buttons</em> - but often times they come with a performance cost: <em>JavaScript</em>. Luckily for us, we can create our very own shiny, animated buttons with pure CSS.</p>
<h2 id="the-demo">The Demo</h2>
<p class="codepen" data-height="609" data-theme-id="dark" data-default-tab="result" data-user="bradleytaunt" data-slug-hash="oNBQevj" style="height: 609px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Shiny Buttons">
<span>See the Pen <a href="https://codepen.io/bradleytaunt/pen/oNBQevj">
Shiny Buttons</a> by Bradley Taunt (<a href="https://codepen.io/bradleytaunt">@bradleytaunt</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<h2 id="the-html">The HTML</h2>
<p>Nothing fancy going on here, just a set of <code class="language-plaintext highlighter-rouge">ahref</code> elements with specific <code class="language-plaintext highlighter-rouge">button</code> classes added:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;a href="#" class="button green"&gt;&lt;span&gt;Green Button&lt;/span&gt;&lt;/a&gt;
&lt;a href="#" class="button blue"&gt;&lt;span&gt;Blue Button&lt;/span&gt;&lt;/a&gt;
&lt;a href="#" class="button orange"&gt;&lt;span&gt;Orange Button&lt;/span&gt;&lt;/a&gt;
&lt;a href="#" class="button purple"&gt;&lt;span&gt;Purple Button&lt;/span&gt;&lt;/a&gt;
</code></pre></div></div>
<h2 id="the-css">The CSS</h2>
<p>First we set the default base styling for all the buttons. We also place the inner text into <code class="language-plaintext highlighter-rouge">span</code> elements (I will explain why in a little bit):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.button {
background: white;
border: 1px solid #a5b1c2;
border-radius: 6px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
color: #111111;
display: inline-block;
margin: 1rem auto;
min-width: 180px;
overflow: hidden;
padding: 15px 30px;
position: relative;
text-align: center;
text-decoration: none;
transition: .3s ease-in-out all;
}
.button span {
position: relative;
z-index: 2;
}
</code></pre></div></div>
<p>Now we need to create our <em>shiny</em> element that will pass across the button on <code class="language-plaintext highlighter-rouge">hover</code> or <code class="language-plaintext highlighter-rouge">focus</code>. For this object we will use the <code class="language-plaintext highlighter-rouge">before</code> pseudo element:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.button:before {
background: linear-gradient(transparent 0%, rgba(255,255,255,0.6) 50%, transparent 100%);
content:'';
height: 200%;
position: absolute;
right: calc(100% + 20px);
top: -55%;
transform: rotate(-70deg);
transition: .6s ease-in-out right;
width: 80px;
z-index: 0;
}
</code></pre></div></div>
<p>Next, we tell the <code class="language-plaintext highlighter-rouge">before</code> element to swipe across the main <code class="language-plaintext highlighter-rouge">.button</code> parent element when the user hovers or focuses on it. Remember placing our inner content into a <code class="language-plaintext highlighter-rouge">span</code> element? That insures that our shiny/swipe element doesn’t position itself <em>over</em> the text, but instead flows under it:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.button:hover:before {
right: -100%;
}
/* Extra visual styling for buttons on hover - optional */
.button:hover, button:focus {
box-shadow: 0 8px 12px rgba(0,0,0,0.1), inset 0 10px 30px rgba(255,255,255,0.3), inset 0 2px 2px rgba(255,255,255,0.2);
color: white;
}
</code></pre></div></div>
<p>All that’s left is adding some visual flare to each individual button - in this case background-color and border-color:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.button.green:hover, button.green:focus { background: #20bf6b; border-color: #20bf6b; }
.button.blue:hover, button.blue:focus { background: #0984e3; border-color: #0984e3; }
.button.orange:hover, button.orange:focus { background: #ff793f; border-color: #ff793f; }
.button.purple:hover, button.purple:focus { background: #6c5ce7; border-color: #6c5ce7; }
</code></pre></div></div>
<h2 id="browser-support">Browser Support</h2>
<p>These buttons work across all browsers flawlessly. See the details <a href="https://caniuse.com/css-gencontent">on the caniuse report itself</a>.
<br /><br /></p>
<h2 id="the-live-codepen">The Live CodePen</h2>
<p>You can find the live demo embedded at the top of this post, or <a href="https://codepen.io/bradleytaunt/pen/oNBQevj">directly on CodePen here</a>.</p>
</content>
</entry>
<entry>
<title>Create a Mac App Icon with Pure HTML &amp; CSS</title>
<link href="https://uglyduck.ca/macos-icon-css/"/>
<updated>2021-04-13T00:00:00-04:00</updated>
<id>https://uglyduck.ca/macos-icon-css</id>
<content type="html"><p><em>I have always been a huge fan of <a href="https://dribbble.com/bg-d">Bogdan’s work on Dribbble</a></em> and was recently inspired to see if I could replicate one of his awesome icon designs with only HTML &amp; CSS. What was the outcome? I think it’s a half-way decent copy - of course the original will always look significantly better.</p>
<p>Don’t care about reading through the tutorial? No problem! You can <a href="#demo">jump right down to the live demo</a></p>
<h2 id="the-comparison">The Comparison</h2>
<p>Let’s take a look at the original Dribbble shot:</p>
<figure>
<img src="/placeholder-image.webp" onclick="this.src='https://cdn.dribbble.com/users/4965/screenshots/14333170/media/e54ac9b2850f786471d7790bec3844a6.webp'" alt="Big Sur Icon" />
<figcaption><b>Click the placeholder to load in the real image</b><br />The original Dribbble shot (<a href="https://cdn.dribbble.com/users/4965/screenshots/14333170/media/e54ac9b2850f786471d7790bec3844a6.webp">direct link to image</a>)</figcaption>
</figure>
<p>And now let’s see what we will be creating with only HTML &amp; CSS:</p>
<figure>
<img src="/placeholder-image.webp" onclick="this.src='/public/images/big-sur-css.webp'" alt="Big Sur Icon" />
<figcaption><b>Click the placeholder to load in the real image</b><br />What we are going to create with pure HTML &amp; CSS (<a href="/public/images/big-sur-css.webp">direct link to image</a>)</figcaption>
</figure>
<p>Like I said - far from perfect but still a fun experiment!</p>
<h2 id="the-html">The HTML</h2>
<p>Let’s jump right in and build out the main skeleton of our project:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;div class="white-square"&gt;&lt;/div&gt;
&lt;div class="blue-square"&gt;
&lt;div class="row"&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="row"&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="row"&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="row"&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="row"&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="row"&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="row"&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;div class="item"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="play-button"&gt;
&lt;div class="triangle"&gt;&lt;/div&gt;
&lt;/div&gt;
</code></pre></div></div>
<ul>
<li>The <code class="language-plaintext highlighter-rouge">white-square</code> element is the white, rounded square in the background</li>
<li>The <code class="language-plaintext highlighter-rouge">blue-square</code> is the main blue square of the icon</li>
<li>The <code class="language-plaintext highlighter-rouge">row</code> elements inside the <code class="language-plaintext highlighter-rouge">blue-square</code> will be our individual <em>lines</em> spread across the icon</li>
<li>The <code class="language-plaintext highlighter-rouge">play-button</code> is obviously - the play button</li>
</ul>
<p>Right now it will look like nothing, but we can change that by adding the most important part…</p>
<h2 id="the-css">The CSS</h2>
<p>Pasting the entire CSS styling here would end up looking a little daunting. Instead, I’m just going to breakdown each individual section to make things more digestible.</p>
<h3 id="defaults--the-white-square">Defaults &amp; the White Square</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* {
box-sizing: border-box;
}
:root {
--row-distance: 42px;
}
.white-square {
background: white;
border-radius: 105px;
box-shadow: inset 0 -5px 8px rgba(0,0,0,0.25), 0 12px 10px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.1);
height: 420px;
left: 165px;
position: absolute;
transform: rotate(-8deg);
top: 95px;
width: 420px;
}
</code></pre></div></div>
<p>See that <code class="language-plaintext highlighter-rouge">--row-distance</code> variable? That will come into play a bit later. For now, we want to lay the Blue Square on top of this newly creating White Square:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.blue-square {
background: linear-gradient(#04BDFD 0%, #0585E4 100%);
border-radius: 105px;
box-shadow: inset 0 5px 8px rgba(255,255,255,0.5), inset 0 -5px 8px rgba(0,0,0,0.32), 0 12px 10px rgba(0,0,0,0.18), 0 2px 4px rgba(0,0,0,0.15);
height: 420px;
left: 205px;
padding: 75px 0 0;
position: absolute;
top: 75px;
width: 420px;
}
</code></pre></div></div>
<p>So far so good. The next part <em>looks</em> like a lot, but I assure you it’s fairly straightforward. We need to include each row inside the Blue Square like in the original Dribbble shot (7 total):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.blue-square .row {
display: flex;
height: 20px;
justify-content: space-between;
padding: 0 55px;
position: absolute;
width: 100%;
}
.blue-square .row:nth-of-type(2) { margin-top: var(--row-distance); }
.blue-square .row:nth-of-type(2) .item:nth-of-type(odd) {
width: 85px;
}
.blue-square .row:nth-of-type(2) .item:nth-of-type(even) {
width: calc(100% - 100px);
}
.blue-square .row:nth-of-type(3) { margin-top: calc(var(--row-distance) * 2); }
.blue-square .row:nth-of-type(3) .item:nth-of-type(odd) {
width: 115px;
}
.blue-square .row:nth-of-type(3) .item:nth-of-type(even) {
width: calc(100% - 130px);
}
.blue-square .row:nth-of-type(4) { margin-top: calc(var(--row-distance) * 3); }
.blue-square .row:nth-of-type(4) .item:nth-of-type(odd) {
width: 185px;
}
.blue-square .row:nth-of-type(4) .item:nth-of-type(even) {
width: calc(100% - 200px);
}
.blue-square .row:nth-of-type(5) { margin-top: calc(var(--row-distance) * 4); width: calc(100% - 115px); }
.blue-square .row:nth-of-type(5) .item:nth-of-type(odd) {
width: 105px;
}
.blue-square .row:nth-of-type(5) .item:nth-of-type(even) {
width: calc(100% - 120px);
}
.blue-square .row:nth-of-type(6) { margin-top: calc(var(--row-distance) * 5); width: calc(100% - 140px); }
.blue-square .row:nth-of-type(6) .item:nth-of-type(odd) {
width: 65px;
}
.blue-square .row:nth-of-type(6) .item:nth-of-type(even) {
width: calc(100% - 80px);
}
.blue-square .row:nth-of-type(7) { margin-top: calc(var(--row-distance) * 6); width: calc(100% - 160px); }
.blue-square .row:nth-of-type(7) .item:nth-of-type(odd) {
width: 40px;
}
.blue-square .row:nth-of-type(7) .item:nth-of-type(even) {
width: calc(100% - 55px);
}
.blue-square .row .item {
background: white;
border-radius: 20px;
box-shadow: inset 0 -2px 4px rgba(0,0,0,0.06), inset 0 2px 4px rgba(255,255,255,0.1), 0 4px 6px rgba(0,0,0,0.05);
width: 40px;
}
.blue-square .row .item:nth-of-type(even) {
background: #3FC0F5;
width: calc(100% - 55px);
}
</code></pre></div></div>
<p>Take a few moments to read everything over - it will help you better understand what’s going on. Basically, we are adding two inner elements to each row element. We calculate the <code class="language-plaintext highlighter-rouge">margin-top</code> distance by using that <code class="language-plaintext highlighter-rouge">--row-distance</code> variable I mentioned earlier. The inner elements are then styled based on their placement inside the row (<code class="language-plaintext highlighter-rouge">nth-of-type</code>).</p>
<h3 id="the-play-button">The Play Button</h3>
<p>Now we finish things off with a much simpler element to style:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.play-button {
backdrop-filter: blur(6px);
border-radius: 9999px;
box-shadow: inset 0 4px 0 rgba(255,255,255,0.3), inset 0 20px 15px rgba(255,255,255,0.6), 0 8px 12px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05);
height: 220px;
overflow: hidden;
position: absolute;
right: 140px;
top: 320px;
width: 220px;
}
.play-button::before {
background: rgba(255,255,255,0.9);
border-radius: 9999px;
content:'';
filter: blur(40px);
height: 150%;
left: -25%;
opacity: 0.8;
position: absolute;
top: -25%;
width: 150%;
}
.triangle {
position: absolute;
left: calc(50% - 2em);
background-color: #315074;
top: calc(50% - 2.1em);
text-align: left;
}
.triangle:before,
.triangle:after {
content: '';
position: absolute;
background-color: inherit;
}
.triangle,
.triangle:before,
.triangle:after {
width: 3.2em;
height: 3.2em;
border-top-right-radius: 30%;
}
.triangle {
transform: rotate(-90deg) skewX(-30deg) scale(1,.866);
}
.triangle:before {
transform: rotate(-135deg) skewX(-45deg) scale(1.414,.707) translate(0,-50%);
}
.triangle:after {
transform: rotate(135deg) skewY(-45deg) scale(.707,1.414) translate(50%);
}
</code></pre></div></div>
<p><del>We don’t have the ability to <em>perfectly</em> mimic the frosted glass effect like in the original (without the use of SVG background etc), but what we get is close enough.</del> Thanks to <a href="https://m.nintendojo.fr/@meduz/106059826445460903">meduz</a> for pointing out the <code class="language-plaintext highlighter-rouge">backdrop-filter</code> property. This allows for a frosted glass look on Chromium &amp; Safari (although sadly not on Firefox). The <code class="language-plaintext highlighter-rouge">triangle</code> element could also be improved by using an embedded <code class="language-plaintext highlighter-rouge">SVG</code> but I was determined to use only CSS for this experiment :P</p>
<p>That’s really all there is to it! You can see the embedded CodePen example below or <a href="https://codepen.io/bradleytaunt/pen/bGgBRaV">check it out directly here →</a></p>
<hr />
<h3 id="special-thanks">Special Thanks</h3>
<p>Thanks to Bogdan for letting me butcher the original Dribbble shot :D</p>
<ul>
<li><a href="http://bg-d.net/">bg-d.net</a></li>
<li><a href="https://dribbble.com/bg-d">Bogdan on Dribbble</a></li>
</ul>
<hr />
<h2 id="demo">Live Demo (CodePen)</h2>
<p class="codepen" data-height="710" data-theme-id="dark" data-default-tab="result" data-user="bradleytaunt" data-slug-hash="bGgBRaV" style="height: 710px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="BG-D: Big Sur Mac App Icon">
<span>See the Pen <a href="https://codepen.io/bradleytaunt/pen/bGgBRaV">
BG-D: Big Sur Mac App Icon</a> by Bradley Taunt (<a href="https://codepen.io/bradleytaunt">@bradleytaunt</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<p><br /></p>
</content>
</entry>
<entry>
<title>The Lazy Developer's Dark Mode</title>
<link href="https://uglyduck.ca/lazy-dev-dark-mode/"/>
<updated>2021-04-12T00:00:00-04:00</updated>
<id>https://uglyduck.ca/lazy-dev-dark-mode</id>
<content type="html"><p><em>After recently jumping back to Jekyll for my personal blog</em>, I decided to take a closer look at how I was supporting <code class="language-plaintext highlighter-rouge">dark mode</code> for my visitors. I was using the proper CSS query to target those who had system-wide dark mode enabled, but I found that the code had far too many caveats and targeted too many custom classes.</p>
<p>So I thought to myself, “There <em>has</em> to be a simpler way…”</p>
<h2 id="introducing-dark-mode---the-lazy-way">Introducing Dark Mode - The Lazy Way</h2>
<p>Here is the default dark mode for my current website in all it’s glory:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@media (prefers-color-scheme: dark) {
body{background:#2d2d2d;filter:invert(1);}
img,.cp_embed_wrapper,pre{filter:invert(1);}
}
</code></pre></div></div>
<p>Not much to look at, eh? Well, let’s still break it down.</p>
<p>First we set the <code class="language-plaintext highlighter-rouge">body</code> to use a nice dark background color (avoid using <code class="language-plaintext highlighter-rouge">#000000</code> directly since that can cause some minor eye strain). Next we tell the browser to invert all the child elements by using <code class="language-plaintext highlighter-rouge">filter:invert(1)</code>. At this point, you could consider your work done - but there are some edge case elements…</p>
<h2 id="images-codepens--code---oh-my">Images, CodePens &amp; Code - Oh My!</h2>
<p>Most of my articles on this site will include either an image(s), embedded CodePen examples or code snippets directly in the page. For these elements we probably <em>don’t</em> want to invert their color/text etc. All we need to do is run the filter property on these a second time (<em>after</em> the main <code class="language-plaintext highlighter-rouge">body</code> attribute):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@media (prefers-color-scheme: dark) {
img,.cp_embed_wrapper,pre{filter:invert(1);}
}
</code></pre></div></div>
<p>Of course, YMMV depending on what other custom elements you want to avoid inverting.</p>
<h2 id="minor-caveats">Minor Caveats</h2>
<p>I should mention that since my website doesn’t use any custom coloring for anchor links, inverting <code class="language-plaintext highlighter-rouge">ahref</code> elements works out of the box. Certain projects might still require some custom overrides if the inverted version of a certain custom color looks poor.</p>
<p>That’s it - enjoy being lazy!</p>
</content>
</entry>
<entry>
<title>Click to Load Website Images</title>
<link href="https://uglyduck.ca/load-image-on-click/"/>
<updated>2021-03-25T00:00:00-04:00</updated>
<id>https://uglyduck.ca/load-image-on-click</id>
<content type="html"><p><em>In my previous post about <a href="https://uglyduck.ca/#2021-03-22-89-posts-one-file">switching my Jekyll blog over to PHPetite</a></em>, I briefly mentioned how I only loaded in article images if the user <em>clicked or tapped</em> the empty file element.</p>
<p>In this post, I’m going to quickly breakdown the update I’ve done to my blog’s images since then and how you can easily implement the same thing in your own project.</p>
<h2 id="update">Update</h2>
<p>As pointed out by Gabriel <a href="https://github.com/bradleytaunt/phpetite/issues/1">in this Github issue</a>, this concept breaks things slightly for RSS users. Since then, I have just set the default images on this blog to <code class="language-plaintext highlighter-rouge">display: none</code> and render them as <code class="language-plaintext highlighter-rouge">block</code> elements when their specific <code class="language-plaintext highlighter-rouge">section</code> is loaded into the DOM visibly.</p>
<p>The example below is remaining the same as it was, to still provide context for this post.</p>
<h2 id="live-demo">Live Demo</h2>
<p>Before we jump head first into the details, let’s take a look at what we will be creating:</p>
<figure>
<div class="img-parent">
<img loading="lazy" src="/placeholder-image.webp" onclick="this.src='https://res.cloudinary.com/bradtaunt/image/fetch/q_auto:low/v1570124593/https://uglyduck.ca/public/images/aqua-ui-css-buttons.webp'" alt="Aqua UI buttons" />
</div>
<figcaption><b>Click the placeholder to load in the real image</b><br />Example Dribbble shot for testing. Feel free to click the default image in order to load the correct Dribbble source. <a href="https://res.cloudinary.com/bradtaunt/image/fetch/q_auto:low/v1570124593/https://uglyduck.ca/public/images/aqua-ui-css-buttons.webp">View full size image</a>.</figcaption>
</figure>
<p>Pretty neat, eh? Well let’s get into the nitty gritty.</p>
<h2 id="the-code">The Code</h2>
<p>Personally, I place everything into a <code class="language-plaintext highlighter-rouge">figure</code> element to keep things contained and clean - but this isn’t required by any means. We then include our <code class="language-plaintext highlighter-rouge">img</code> and <code class="language-plaintext highlighter-rouge">figcaption</code> elements. That’s it.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;figure&gt;
&lt;img src="/placeholder-image.webp" onclick="this.src='https://res.cloudinary.com/bradtaunt/image/fetch/q_auto:low/v1570124593/https://uglyduck.ca/public/images/aqua-ui-css-buttons.webp'" alt="Aqua UI buttons"&gt;
&lt;figcaption&gt;&lt;b&gt;Click the placeholder to load in the real image&lt;/b&gt;&lt;br&gt;
Example Dribbble shot for testing. Feel free to click the default image in order to load the correct Dribbble source.
&lt;a href="https://res.cloudinary.com/bradtaunt/image/fetch/q_auto:low/v1570124593/https://uglyduck.ca/public/images/aqua-ui-css-buttons.webp"&gt;View full size image&lt;/a&gt;.
&lt;/figcaption&gt;
&lt;/figure&gt;
</code></pre></div></div>
<h3 id="the-image-element">The Image Element</h3>
<p>This is where the <em>magic</em> happens. By default all images will target the default placeholder image: <code class="language-plaintext highlighter-rouge">placeholder-image.webp</code>. This image is just 16KB in size and only needs to load in once.</p>
<p>Next we include an inline <code class="language-plaintext highlighter-rouge">onclick</code> attribute, which targets the current image’s <code class="language-plaintext highlighter-rouge">src</code> attribute and changes it based on the URL provided. (Note: I use Cloudinary for my blog’s image storage, but you could even host your images relative to your root directory if you wanted)</p>
<p>Now when a user clicks on the placeholder image, the inline <code class="language-plaintext highlighter-rouge">onclick</code> pulls in the correct image in it’s place.</p>
<h3 id="disabled-javascript">Disabled JavaScript</h3>
<p>For users who have JavaScript blocked or disabled we have a decent backup. By including a direct link to the image URL in the <code class="language-plaintext highlighter-rouge">figcaption</code> element, we give the user the ability to still view the image in a separate browser tab.</p>
<p>You could get extra fancy and include some <code class="language-plaintext highlighter-rouge">noscript</code> tags in your project that maybe render a different placeholder image mentioning they have JavaScript disabled etc, but for my needs that would be overkill.</p>
<h2 id="cool---but-why-do-this">Cool - But Why Do This?</h2>
<p>Bandwidth is a limited resource for a lot of users around the world. As designers and developers it’s best to respect this fact and only load in elements as the user <em>requires</em> them. Every little bit helps.</p>
</content>
</entry>
<entry>
<title>89 Blog Posts in a Single HTML File</title>
<link href="https://uglyduck.ca/89-posts-one-file/"/>
<updated>2021-03-22T00:00:00-04:00</updated>
<id>https://uglyduck.ca/89-posts-one-file</id>
<content type="html"><p><br /></p>
<p><em>This is no longer the current setup of my blog</em>. I have switched back to Jekyll for performance reasons. I’ll be leaving this post up as a point of reference though :)</p>
<hr />
<p><br /></p>
<p>This is my personal blog (if that wasn’t already obvious). I currently have 89 blog posts living here. But I have done something magical today:</p>
<p>All of these articles are rendered inside a <em>single HTML file</em>. 🤯</p>
<h2 id="what-sorcery-is-this">What Sorcery is This?!</h2>
<p>No magic here - I’m just using my own personal static site generator called <a href="https://phpetite.org">PHPetite</a>. You can find all the code and more information about PHPetite on the <a href="https://github.com/bradleytaunt/phpetite">official Github repo page</a>. Please note that this project is still very much a WIP, so go easy on me when pointing out bugs or issues!</p>
<h2 id="pingdom-stats">Pingdom Stats</h2>
<p>Before we get into more details on how everything is built, let’s take a quick look at some of my website stats (not that pure numbers are 100% accurate of user experience):</p>
<ul>
<li>99 Performance Grade</li>
<li>951 ms Load Time</li>
<li>231.3 KB Page Size (thank you Netlify compression!)</li>
</ul>
<p><a href="https://tools.pingdom.com/#5e169d6a82000000">See all the Pingdom details for yourself</a></p>
<p>Now the <code class="language-plaintext highlighter-rouge">Page Size</code> will certainly change as more blog posts are added over time, but if the total webpage size is ~230 KB with 89 blog posts, I figure that gives me the ability to add another 200 or so posts before I cross the <a href="https://1mb.club">1MB threshold</a></p>
<p>I’ll deal with how to load in the content more efficiently when that times comes…</p>
<h2 id="what-about-images">What About Images?</h2>
<p>Good question. Since a large portion of my articles tend to be focused on design and CSS, visual examples are somewhat important. So how do I avoid loading in all the individual images on every single post into this single HTML “website”?</p>
<p>Easy - I don’t load any images at all.</p>
<p><del>I set the default <code class="language-plaintext highlighter-rouge">img</code> source to point to <code class="language-plaintext highlighter-rouge">about:blank</code> and rely on the user to click/tap the image to load it in via the <code class="language-plaintext highlighter-rouge">onclick</code> attribute. For users with JavaScript disabled, I also include a link to the “full size image” in the <code class="language-plaintext highlighter-rouge">figcaption</code> below the image. No sweat.</del></p>
<p>I now simply set any images that are not included in the current, visible <code class="language-plaintext highlighter-rouge">section</code> to <code class="language-plaintext highlighter-rouge">display: none</code>. This avoids breaking things with the RSS feed.</p>
<p>Here is an image example of an old Dribbble shot I created years ago:</p>
<figure>
<div class="img-parent">
<img loading="lazy" src="/placeholder-image.webp" onclick="this.src='https://res.cloudinary.com/bradtaunt/image/fetch/q_auto:low/v1570124593/https://uglyduck.ca/public/images/aqua-ui-css-buttons.webp'" alt="Aqua UI buttons" />
</div>
<figcaption><b>Click the placeholder to load in the real image</b><br />Example Dribbble shot for testing. Feel free to click the default image in order to load the correct Dribbble source. <a href="https://res.cloudinary.com/bradtaunt/image/fetch/q_auto:low/v1570124593/https://uglyduck.ca/public/images/aqua-ui-css-buttons.webp">View full size image</a>.</figcaption>
</figure>
<p>I have noticed that Safari / Firefox on iOS renders the imagery as broken image links - I plan to look into this further to avoid that confusion. For now it works well on desktop Chromium and Firefox.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;figure&gt;
&lt;div class="img-parent"&gt;
&lt;img loading="lazy" src="/placeholder-image.webp" onclick="this.src='https://res.cloudinary.com/bradtaunt/image/fetch/q_auto:low/v1570124593/https://uglyduck.ca/public/images/aqua-ui-css-buttons.webp'" alt="Aqua UI buttons"&gt;
&lt;/div&gt;
&lt;figcaption&gt;&lt;b&gt;Click the placeholder to load in the real image&lt;/b&gt;&lt;br&gt;Example Dribbble shot for testing. Feel free to click the default image in order to load the correct Dribbble source. &lt;a href="https://res.cloudinary.com/bradtaunt/image/fetch/q_auto:low/v1570124593/https://uglyduck.ca/public/images/aqua-ui-css-buttons.webp"&gt;View full size image&lt;/a&gt;.&lt;/figcaption&gt;
&lt;/figure&gt;
</code></pre></div></div>
<h2 id="hows-the-seo">How’s the SEO?</h2>
<p>Probably terrible to be honest. One single file for all blog posts is something Google will most likely frown upon. Whatever - it’s my personal blog so I don’t really give a shit.</p>
<h2 id="rss-feed">RSS Feed</h2>
<p>If you recently added my old RSS feed, I’m sorry, but you’ll have to update. The new feed is located at <a href="https://uglyduck.ca/feed.xml">uglyduck.ca/feed.xml</a>. I made this change to avoid spamming the current feed users.</p>
<h2 id="tasty-dog-food">Tasty Dog Food</h2>
<p>At the end of the day, by moving my personal blog over to PHPetite, it makes me more aware of current bugs and restrictions within the project. I feel like this will help me to improve this static site generator even more over time, instead of letting it die a slow death ignored on Github.</p>
<p>I hope you enjoyed this little breakdown and maybe even want to try out PHPetite for yourself? 😉</p>
</content>
</entry>
<entry>
<title>Introducing PageRoast</title>
<link href="https://uglyduck.ca/introducing-pageroast/"/>
<updated>2021-03-11T00:00:00-05:00</updated>
<id>https://uglyduck.ca/introducing-pageroast</id>
<content type="html"><p><em>Following up with my concept of releasing small side projects weekly</em>, I have officially launched <a href="https://pageroast.com">PageRoast</a>. What is PageRoast I hear you ask?</p>
<blockquote>
<p>Receive a detailed report analyzing your landing page with actionable items to improve your conversion rate.</p>
</blockquote>
<p>In simple terms that just means I will <strong>roast your landing page</strong>. Included in a page roast report is:</p>
<ul>
<li>Fully detailed ABCD Framework report</li>
<li>Proposed content &amp; design changes</li>
<li>Page performance audit</li>
</ul>
<p>And you get all of this for just <strong>$100</strong> (currently 50% until April 1st with coupon code PHROAST). Alright, enough with the “sales pitch” - why did I make this side project?</p>
<p>From the main <a href="https://pageroast.com/#2021-03-10-pageroast-has-launched">launch blog post on PageRoast itself</a> I wrote:</p>
<blockquote>
<p>I’ve always been obsessed with developing landing pages that produce high conversion rates. It’s what I’ve done for a large portion of my web development career.<br /><br />So I thought to myself, “Why not launch a small-scale audit service to help indie devs and startups?”</p>
</blockquote>
<p>That’s really all there is to it. If you would like to learn more, check out the following links:</p>
<ul>
<li><a href="https://pageroast.com">PageRoast main website</a></li>
<li><a href="https://pageroast.com/#2021-03-10-pageroast-has-launched">PageRoast official launch blog post</a></li>
</ul>
</content>
</entry>
<entry>
<title>ThriftyName: $5 Brand Names</title>
<link href="https://uglyduck.ca/launching-thriftyname/"/>
<updated>2021-02-25T00:00:00-05:00</updated>
<id>https://uglyduck.ca/launching-thriftyname</id>
<content type="html"><p><em>It’s been a while since I’ve written anything on this blog</em>, but for good reason - <strong>I’ve been working on a handful of side projects</strong>. I plan to drip-feed release these projects over time, but for today I’m announcing <a href="https://thrifty.name">ThriftyName</a>.</p>
<h2 id="what-is-thriftyname">What is ThriftyName?</h2>
<p>In case this post title wasn’t clear enough (or you avoided going to the product site itself) ThriftyName is a place where indie devs, start-ups or entrepreneurs can go to get a custom brand name for <strong>just $5</strong>.</p>
<p>This “service” started out as an almost gimmicky joke in my head, but once I began building out the main website I realized that this could be quite useful for cash-strapped indies. After all, not all developers <em>love</em> to sit around wasting precious time thinking about product names, when they could use that time to <em>build their product</em>.</p>
<h2 id="learn-more">Learn More</h2>
<p>If you’re really interested in the reasoning behind making this project, check out the <a href="https://thrifty.name/#about">official about page on the site itself</a>. I go into a little more depth about my thought process.</p>
<h2 id="more-side-projects-incoming">More Side Projects Incoming…</h2>
<p>Like I said at the beginning of the post, keep a look out for my other side projects that I’ll be rolling out slowing. I’m still not sure of the best “method” to do this (release one every week? every month?) - but I’ll figure it out as I go along.</p>
<p>Thanks for reading!</p>
</content>
</entry>
<entry>
<title>Self-Hosting Fathom Analytics with DigitalOcean</title>
<link href="https://uglyduck.ca/self-hosting-fathom/"/>
<updated>2021-02-02T00:00:00-05:00</updated>
<id>https://uglyduck.ca/self-hosting-fathom</id>
<content type="html"><p><em>Since my previous post walked through the process of setting up</em> <a href="/fathom-analytics-netlify">Fathom PRO on Netlify</a>, I figured it made sense to create a similar tutorial for the “Lite” variation, self-hosted on DigitalOcean.</p>
<p>Please note that while I think the PRO version of <a target="_blank" href="https://usefathom.com/ref/DKHJVX">Fathom Analytics</a> is truly great, for my small, niche blog it seemed overkill compared to self-hosting. Switching over from $14/mo to $5/mo while retaining most of the same functionality was a no-brainer. Choose the option that best suits your needs (or in the case - budget &amp; bandwidth).</p>
<p>With that cleared up - let’s get into it!</p>
<h2 id="prerequisites">Prerequisites</h2>
<ol>
<li>One or more website(s) where you would like to include analytics</li>
<li><a target="_blank" href="https://m.do.co/c/74b3fd11c07a">DigitalOcean account</a> (<strong>this link will give you a $100 credit!</strong>)</li>
<li>Positive attitude and passion for privacy-respecting analytics!</li>
</ol>
<h2 id="create-a-droplet">Create a Droplet</h2>
<p>Once your DigitalOcean account is setup, navigate to the <a target="_blank" href="https://marketplace.digitalocean.com">Marketplace</a> and search for <code class="language-plaintext highlighter-rouge">Fathom Analytics</code>. Then click the <code class="language-plaintext highlighter-rouge">Create Fathom Analytics Droplet</code>.</p>
<p>From here you’ll be brought to a page that allows you to customize the specifications of your new droplet. If you’re a smaller traffic site (personal blog, etc) selecting the <strong>$5/month</strong> Basic Shared CPU option is your best bet.</p>
<figure>
<img src="/placeholder-image.webp" onclick="this.src='/public/images/fathom-create-droplet-details.webp'" alt="Fathom Droplet Details" />
<figcaption><b>Click the placeholder to load in the real image</b><br />Creating the new droplet (<a href="/public/images/fathom-create-droplet-details.webp">direct link to image</a>)</figcaption>
</figure>
<p>Select the data-center region based on where most of your traffic originates from. I would suggest enabling <code class="language-plaintext highlighter-rouge">IPv6</code> and setting up your authentication via SSH instead of a regular password system. Adding backups is entirely at your own discretion.</p>
<p>Once you’re ready, click <strong>Create Droplet</strong>.</p>
<h2 id="enter-the-matrix-not-really">Enter the Matrix (not really)</h2>
<p>Once DigitalOcean finishes spinning up your new droplet, open a terminal and connect to it by entering:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh root@YOUR_DROPLET_IP
</code></pre></div></div>
<p>If you setup your login via SSH everything should work as-is. If you went the password route, you’ll given a prompt to enter it.</p>
<p>Now that you’re connected, Fathom will guide you through a simple configuration setup. It’s fairly straightforward and painless. Once complete, move to the next step.</p>
<h2 id="domains">Domains</h2>
<p>You’ll most likely want to host this instance on your own domain or subdomain - instead of connecting directly via the droplet’s <code class="language-plaintext highlighter-rouge">IP</code>. Head over to your <strong>Networking</strong> page in the sidebar of DigitalOcean and add your custom domain.</p>
<p>Then, click on that newly added domain - we need to add some new records. You’re going to add two new <code class="language-plaintext highlighter-rouge">A</code> records to this domain:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Hostname</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>@</td>
<td>YOUR_DROPLET_IP</td>
</tr>
<tr>
<td>A</td>
<td>www</td>
<td>YOUR_DROPLET_IP</td>
</tr>
</tbody>
</table>
<p>The last thing you need to do is set your nameservers to point to DigitalOcean:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ns1.digitalocean.com
ns2.digitalocean.com
ns3.digitalocean.com
</code></pre></div></div>
<p>Give it some time to propagate and you’ll be in business!</p>
<h2 id="ssl-ftw">SSL FTW</h2>
<p>There is hardly a good reason not to practice security on the web, so setting up your new analytics to be served over <code class="language-plaintext highlighter-rouge">HTTPS</code> is just the smart thing to do. Did I mention that this is completely free as well? See - no excuses.</p>
<p>In order to get a free SSL certificate setup, you’ll need to install <code class="language-plaintext highlighter-rouge">certbot</code>. While connected to your droplet, enter the following:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt-get install python-certbot-nginx
</code></pre></div></div>
<p>Once installed, enter the following to setup SSL (remember to swap out the domain with your own):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>certbot --nginx -d your-cool-domain.com
</code></pre></div></div>
<p>Follow the steps (it’s very quick and easy) and you’ll have <code class="language-plaintext highlighter-rouge">HTTPS</code> setup in a jiffy!</p>
<h2 id="the-final-lap">The Final Lap</h2>
<p>The last thing to do is login to your newly self-hosted Fathom instance, add your site you wish to track, grab the generated tracking code and then slap that badboy on whatever pages you need to track!</p>
<p>Congrats! You’re now officially running your own set of analytics tools. You should be happy about what you’ve accomplished and proud for respecting your users’ privacy!</p>
</content>
</entry>
<entry>
<title>Setting Up Fathom Analytics with Netlify</title>
<link href="https://uglyduck.ca/fathom-analytics-netlify/"/>
<updated>2021-01-19T00:00:00-05:00</updated>
<id>https://uglyduck.ca/fathom-analytics-netlify</id>
<content type="html"><p><em>It’s no secret that I’m passionate about open source software</em>, but I’m also extremely adamant about protecting the privacy of all users across the web. So when I decided to implement analytics on my own personal website, I ended up choosing <a href="https://usefathom.com/ref/DKHJVX">Fathom</a> (<em>get a $10 credit using that link!</em>).</p>
<p>You should research further into the company yourself if you’re interested, but in a nutshell the Fathom platform provides:</p>
<ul>
<li>GDPR, ePrivacy, PECR and CCPA compliance</li>
<li>ad-blocker bypasses (in a good way -&gt; zero impact on users)</li>
<li>anonymous visitor stats</li>
<li>software created by <a href="https://usefathom.com/about">indie developers</a></li>
</ul>
<p>Although this might sound like a “paid” blog post, I can assure you this is completely based on my own opinions and experience with the service. I’m just very pleased with the product :)</p>
<p>Enough chit-chat - let’s breakdown how to setup Fathom on your own site hosted through Netlify!</p>
<h2 id="step-0-assumptions">Step 0: Assumptions</h2>
<p>I’m going into this tutorial with the assumption that you:</p>
<ol>
<li>Have an active <a href="https://usefathom.com/ref/DKHJVX">Fathom account</a> (or are at least trying the 7-day free trial)</li>
<li>Have a website already setup on <a href="https://netlify.com">Netlify (custom domain optional)</a></li>
</ol>
<p>And that’s all you need.</p>
<h2 id="step-1-adding-a-custom-domain-dns-setup">Step 1: Adding a Custom Domain (DNS Setup)</h2>
<p>Even though you have the option to use the <code class="language-plaintext highlighter-rouge">default</code> tracking script URL, I would highly recommend setting it up through your own domain. This helps avoid any browser extensions or firewalls that might block outside URL request made by individual sites.</p>
<ol>
<li>Navigate to the Fathom <strong>Settings</strong> page in the bottom footer (must be logged in)</li>
<li>Select <strong>Domains</strong> from the sidebar</li>
<li>Enter your custom domain in the <code class="language-plaintext highlighter-rouge">Add a new custom domain</code> input</li>
<li>Select <strong>Start Process</strong></li>
<li>Keep note of both the <code class="language-plaintext highlighter-rouge">CNAME</code> and <code class="language-plaintext highlighter-rouge">VALUE</code> (we will copy this in a moment)</li>
</ol>
<p>Open a new tab, and login to your Netlify account:</p>
<ol>
<li>From your <code class="language-plaintext highlighter-rouge">Team Overview</code> page, navigate to the top-level <code class="language-plaintext highlighter-rouge">Domains</code> page</li>
<li>Select the domain you plan to add Fathom to</li>
<li>Under <strong>DNS settings</strong> &gt; <strong>DNS records</strong> select <code class="language-plaintext highlighter-rouge">Add New Record</code></li>
<li>Select <code class="language-plaintext highlighter-rouge">CNAME</code> from the dropdown of available options</li>
<li>Paste the Fathom <code class="language-plaintext highlighter-rouge">CNAME</code> (from the steps above) into the <strong>Name</strong> input</li>
<li>Paste the Fathom <code class="language-plaintext highlighter-rouge">VALUE</code> (from the steps above) into the <strong>Value</strong> input</li>
<li>Click <strong>Save</strong></li>
</ol>
<p>That’s it for DNS setup!</p>
<h2 id="step-2-adding-fathom-your-website">Step 2: Adding Fathom Your Website</h2>
<p>Now that we will be using our own custom domain for the script, it’s time to actually <em>add it</em> to our website. From the same Fathom <strong>Settings</strong> page as before:</p>
<ol>
<li>Navigate to the <strong>Sites</strong> page in the sidebar</li>
<li>Enter your custom domain in the <code class="language-plaintext highlighter-rouge">Add a new site</code> input and click <strong>Get site code</strong></li>
<li>In the modal prompt, select your custom domain from the dropdown list (you’ll see the script code change accordingly)</li>
<li>Copy the tracking code snippet and add it to all the pages you plan to track on your site</li>
<li>When ready, select <strong>Verify site code</strong></li>
<li>If everything went correctly you should be done!</li>
</ol>
<h2 id="step-3-watch-those-live-stats">Step 3: Watch Those Live Stats!</h2>
<p>Open a cold beer (or a bubbly soda if you prefer) and watch your website visitor stats come rolling in on the dashboard! Not only do you now have solid analytics on your website but you have the piece of mind that your users’ privacy isn’t being invaded. Everybody wins!</p>
</content>
</entry>
<entry>
<title>Introducing Notez</title>
<link href="https://uglyduck.ca/introducing-notez/"/>
<updated>2021-01-13T00:00:00-05:00</updated>
<id>https://uglyduck.ca/introducing-notez</id>
<content type="html"><p><em>I have always been a fan of simple note taking applications</em>, since I tend to take a lot of random notes throughout the work day. Sometimes I reach for simple pen and paper, but other times it’s nice to stay focused jotting down notes on the same device I’m working on.</p>
<p>Previously, I just created quick <code class="language-plaintext highlighter-rouge">notes.txt</code> files in my open code editor or fell back on the default OS note apps. These worked perfectly fine but often got in my way (or even worse - lost among everything else).</p>
<p>So I said the hell with it and built <a href="https://getnotez.netlify.app">Notez</a>.</p>
<h2 id="what-makes-notez-special">What makes Notez special?</h2>
<p>Nothing, really. It’s actually a pretty “stupid” app compared to others on the market. But this one is <em>mine</em>. Plus, it was fun to piece it together on a random evening before bed.</p>
<p>For those curious, let’s take a look at the feature list:</p>
<ul>
<li>free and open source (<a href="https://github.com/bradleytaunt/notez">github.com/bradleytaunt/notez</a>)</li>
<li>uses <code class="language-plaintext highlighter-rouge">localStorage</code> to keep changes persistent (helpful for browser crashes, etc)</li>
<li>includes basic export functionality to download content as a simple <code class="language-plaintext highlighter-rouge">output.txt</code> file</li>
</ul>
<p>That’s it. Mind blowing stuff, eh?</p>
<h2 id="making-stuff-is-fun">Making stuff is fun</h2>
<p>My main takeaway when building this tiny, dumb app was to just <em>enjoy building fun stuff</em>. It crossed my mind several times how there are already hundreds of note taking apps across the vast Internet. I also thought about how others might look at the code I wrote and go, “Oh my God - why did he do it that way? What an idiot!”. But I don’t care - I had fun making it.</p>
<p>Hopefully you enjoy using it as well!</p>
</content>
</entry>
<entry>
<title>Animated Radio Tab Toggles</title>
<link href="https://uglyduck.ca/animated-radio-tab-toggles"/>
<updated>2021-01-05T00:00:00-05:00</updated>
<id>https://uglyduck.ca/animated-toggle-tabs</id>
<content type="html"><p><em>In this demo tutorial, we are making the assumption</em> that we need to create a radio slide toggle for our made-up payment options. For this we want to display 3 simple payment choices to the user:</p>
<ul>
<li>One-time payment</li>
<li>Recurring payment</li>
<li>Free tier payment</li>
</ul>
<h2 id="the-final-demo">The Final Demo</h2>
<div class="cp_embed_wrapper"><iframe allowfullscreen="true" allowpaymentrequest="true" allowtransparency="true" class="cp_embed_iframe " name="cp_embed_1" scrolling="no" src="https://codepen.io/bradleytaunt/embed/vYXjpLO?height=382&amp;theme-id=light&amp;default-tab=result&amp;user=bradleytaunt&amp;slug-hash=vYXjpLO&amp;pen-title=Radio%20Tab%20Toggles&amp;name=cp_embed_1" style="width: 100%; overflow:hidden; display:block;" title="Radio Tab Toggles" loading="lazy" id="cp_embed_vYXjpLO" width="100%" height="382" frameborder="0"></iframe></div>
<p>Let’s get started with the base skeleton.</p>
<h2 id="the-html">The HTML</h2>
<p>There isn’t anything special happening here. We just contain all our <code>labels</code> and <code>inputs</code> into a <code>.radio-toggles</code> wrapper, make sure those <code>labels</code> are each properly connected to their corresponding <code>inputs</code>, and then add an empty <code>.slide-item</code> element (more on that later).</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;div class="radio-toggles"&gt;
&lt;input type="radio" id="option-1" name="radio-options"&gt;
&lt;label for="option-1"&gt;One-Time&lt;/label&gt;
&lt;input type="radio" id="option-2" name="radio-options" checked&gt;
&lt;label for="option-2"&gt;Recurring&lt;/label&gt;
&lt;input type="radio" id="option-3" name="radio-options"&gt;
&lt;label for="option-3"&gt;Free&lt;/label&gt;
&lt;div class="slide-item"&gt;&lt;/div&gt;
&lt;/div&gt;
</code></pre></div></div>
<h2 id="the-css">The CSS</h2>
<p>Now for the main event – the CSS. First we want to style the wrapper that holds all of our pieces together. You can tweak this to your liking, but I prefer a simple and clean style:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.radio-toggles {
align-items: center;
background: #eee;
border: 1px solid lightgrey;
border-radius: 9999px;
display: flex;
justify-content: center;
margin: 20px auto;
max-width: 400px;
overflow: hidden;
padding: 4px;
position: relative;
}
</code></pre></div></div>
<p>Next, we “hide” (only visually) the default <code class="language-plaintext highlighter-rouge">radio</code> inputs:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input[type="radio"] {
left: -9999px;
position: absolute;
z-index: -1;
}
</code></pre></div></div>
<p>Then we give the corresponding <code class="language-plaintext highlighter-rouge">label</code> elements a little spacing and breathing room:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>label {
cursor: pointer;
padding: 10px 20px;
text-align: center;
width: 33.33%;
z-index: 2;
}
</code></pre></div></div>
<p>Remember that <code class="language-plaintext highlighter-rouge">.slide-item</code> I referenced earlier? That element will be the visual “slider” that animates between the individual radio options. We style that like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.slide-item {
background: white;
border-radius: 9999px;
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
height: calc(100% - 8px);
left: calc(33.33% + 4px);
position: absolute;
width: calc(33.33% - 8px);
transition: left .4s;
z-index: 0;
}
</code></pre></div></div>
<p>You’ll notice the <code class="language-plaintext highlighter-rouge">left</code>, <code class="language-plaintext highlighter-rouge">height</code>, and <code class="language-plaintext highlighter-rouge">width</code> properties utilize the CSS <code class="language-plaintext highlighter-rouge">calc</code> attributes – this just gives some much needed padding and visual clean-up to the whole tabbed interface.</p>
<p>For the finishing touches, we just need to tell the <code class="language-plaintext highlighter-rouge">.slide-item</code> where to position itself based on which <code class="language-plaintext highlighter-rouge">radio</code> input is currently selected:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input[type="radio"]:nth-of-type(1):checked ~ .slide-item {
left: 4px;
}
input[type="radio"]:nth-of-type(3):checked ~ .slide-item {
left: calc(66.66% + 4px);
}
</code></pre></div></div>
<p>That’s pretty much it! You now have a fully functional, animated toggle slider with just a set of simple <code class="language-plaintext highlighter-rouge">radio</code> inputs and pure CSS.</p>
</content>
</entry>
<entry>
<title>Disabling Comments in WordPress</title>
<link href="https://uglyduck.ca/disabling-comments-in-wordpress/"/>
<updated>2020-12-28T00:00:00-05:00</updated>
<id>https://uglyduck.ca/disabling-comments-in-wordpress</id>
<content type="html"><p><em>I seem to come across a decent amount of clients</em> and users online that have a difficult time knowing how to disable comments for both future <em>and</em> previous blog posts. It isn’t the easiest for both use cases, so let’s break it down.</p>
<h2 id="back-to-the-future">Back to the future</h2>
<p>Disabling comments on all <em>future</em> blog posts is fairly straightforward:</p>
<ol><li>Navigate to your WordPress admin dashboard</li><li>Go to <strong>Settings</strong> &gt; <strong>Discussion</strong></li><li>Uncheck <strong>Allow people to submit comments on new posts</strong></li><li>That’s it!</li></ol>
<figure>
<img loading="lazy" src="https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments.webp" alt="" class="wp-image-281" srcset="https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments.webp 918w, https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments-300x68.webp 300w, https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments-768x174.webp 768w" sizes="(max-width: 918px) 100vw, 918px" width="918" height="208" />
<figcaption><b>Click the placeholder to load in the real image</b><br />You just need to uncheck the last item…</figcaption>
</figure>
<h2 id="but-what-about-old-posts">But what about old posts?</h2>
<p>Have no fear - “Bulk Actions” are here to save the day! In order to retroactively disable comments on older posts, do the following:</p>
<ol><li>Navigate to <strong>Posts</strong></li><li>Select the specific posts you would like to disable comments on (or select all of them)</li><li>Click the <strong>Bulk Actions</strong> drop-down menu and choose <strong>Edit</strong></li><li>Press <strong>Apply</strong></li><li>In the <strong>Bulk Edit</strong> view, look for the <strong>Comments</strong> drop-down </li><li>Select <strong>Do Not Allow</strong> and click <strong>Update</strong></li><li>That’s it!</li><div></div></ol>
<figure><img loading="lazy" src="https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments-2-1024x273.webp" alt="" class="wp-image-283" srcset="https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments-2-1024x273.webp 1024w, https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments-2-300x80.webp 300w, https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments-2-768x205.webp 768w, https://uglyduck.ca/wp-content/uploads/2020/12/disable-comments-2.webp 1252w" sizes="(max-width: 1024px) 100vw, 1024px" width="1024" height="273" /><figcaption><b>Click the placeholder to load in the real image</b><br />Select “Do Not Allow” from the Comments drop-down</figcaption></figure>
<p>I hope this article saves others any headaches in the future. WordPress itself could certainly make this more streamlined, but for now what we have works. Happy (not) commenting!</p>
</content>
</entry>
<entry>
<title>Pure CSS Bar Graphs with Graceful Mobile Fallbacks</title>
<link href="https://uglyduck.ca/flexbox-bar-graphs/"/>
<updated>2020-12-08T00:00:00-05:00</updated>
<id>https://uglyduck.ca/flexbox-bar-graphs</id>
<content type="html"><p><em>I recently published a new open source project</em>, <a target="_blank" href="https://flexbox-bar-graphs.netlify.app/">Flexbox Bar Graphs</a>, and wanted to share a simple breakdown of how it was built. It isn’t anything mind-blowing, but I like the idea of placing bar graphs in a web page with <em>zero</em> Javascript.</p>
<p>So in the end, this is what our bar graphs will look like on desktop:</p>
<figure>
<img src="/placeholder-image.webp" onclick="this.src='/public/images/flexbox-bar-graph-1.webp'" alt="Flexbox Bar Graph" />
<figcaption><b>Click the placeholder to load in the real image</b><br />The flexbox bar graph in desktop view (<a href="/public/images/flexbox-bar-graph-1.webp">direct link to image</a>)</figcaption>
</figure>
<p>And this is how it will look on smaller devices:</p>
<figure>
<img src="/placeholder-image.webp" onclick="this.src='/public/images/flexbox-bar-graph-2.webp'" alt="Flexbox Bar Graph Table" />
<figcaption><b>Click the placeholder to load in the real image</b><br />The flexbox bar graph on mobile devices (<a href="/public/images/flexbox-bar-graph-2.webp">direct link to image</a>)</figcaption>
</figure>
<p>Let’s get into the details!</p>
<h2 id="the-html">The HTML</h2>
<p>The main “secret” of this project is that our graphs are constructed out of HTML <em>tables</em>. Now before you freak out - this is perfectly fine and works in our favor quite well.</p>
<ol>
<li>If the user has JS disabled –&gt; they will still see our graphs</li>
<li>If the user has CSS disabled –&gt; they will see a standard data table set</li>
</ol>
<p>All bases are covered!</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;!-- Using a basic table with our custom data-id --&gt;
&lt;table data-id="flexbox-bar-graph"&gt;
&lt;caption&gt;Web Performance Results&lt;/caption&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Performed&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;Initial Load Time&lt;/th&gt;
&lt;td&gt;
&lt;!--
WTF are these CSS variables?
See the CSS section below
--&gt;
&lt;span style="--data-set:4.7/5;"&gt;&lt;/span&gt;
&lt;p&gt;4.7&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;span style="--data-set:2.7/5;"&gt;&lt;/span&gt;
&lt;p&gt;2.7&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;span style="--data-set:2/5;"&gt;&lt;/span&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</code></pre></div></div>
<p>Nothing crazy is happening here - just your standard HTML table structure. The one main thing to notice is the <code class="language-plaintext highlighter-rouge">--data-set</code> CSS variable placed inline on each data point. This will be important for our CSS to configure the individual bar graphs properly.</p>
<h2 id="the-css">The CSS</h2>
<p>This might look overwhelming if I just dumped the whole CSS file in one big code block, so instead I’m going to break them down into two parts:</p>
<ol>
<li>Baseline styling (mobile)</li>
<li>Desktop styling</li>
</ol>
<h3 id="baseline">Baseline</h3>
<p>Here we target just our table elements with the <code class="language-plaintext highlighter-rouge">data-id</code> of <code class="language-plaintext highlighter-rouge">flexbox-bar-graph</code>. This allows us to avoid worrying about adding classes or IDs and also avoids conflicts with other non-graph styled tables in our projects.</p>
<p>The base <code class="language-plaintext highlighter-rouge">:root</code> element holds all of our bar graph colors. Change these as you see fit!</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* Bar Graph color variables */
:root {
--bar-color-1: #357EC7;
--bar-color-2: #E42217;
--bar-color-3: #4CC417;
--bar-color-4: #7D0541;
--bar-color-5: #FFD801;
}
[data-id="flexbox-bar-graph"] {
border-collapse: collapse;
margin: 4rem 0 6rem;
width: 100%;
}
[data-id="flexbox-bar-graph"] caption {
text-align: left;
}
[data-id="flexbox-bar-graph"] thead th {
text-align: right;
}
[data-id="flexbox-bar-graph"] thead th:nth-child(1),
[data-id="flexbox-bar-graph"] tbody th {
text-align: left;
}
[data-id="flexbox-bar-graph"] tbody th {
font-weight: normal;
font-style: italic;
}
[data-id="flexbox-bar-graph"] tbody td {
text-align: right;
}
[data-id="flexbox-bar-graph"] tbody td p {
margin: 0;
}
</code></pre></div></div>
<h3 id="desktop">Desktop</h3>
<p>Now we set your “visual” bar graphs to show at a set width (in this example it is 1000px and above). That way the “default” styling can target the mobile device screen sizes.</p>
<ul>
<li>The <code class="language-plaintext highlighter-rouge">thead tr th:nth-child(x):before</code> elements create the square “legends” beside each individual data point heading</li>
<li>The <code class="language-plaintext highlighter-rouge">tbody tr td:nth-of-type(x) span</code> elements are the bars themselves</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@media(min-width: 1000px) {
[data-id="flexbox-bar-graph"] {
background: transparent;
display: block;
min-height: 400px;
padding: 0;
position: relative;
width: 100%;
}
[data-id="flexbox-bar-graph"] caption {
display: block;
font-size: 2rem;
text-align: center;
width: 100%;
}
[data-id="flexbox-bar-graph"] thead {
display: block;
margin: 2rem 0 3rem;
width: 100%;
}
[data-id="flexbox-bar-graph"] thead tr {
border-bottom: 1px solid lightgrey;
display: flex;
justify-content: center;
padding-bottom: 1rem;
}
[data-id="flexbox-bar-graph"] thead tr th {
display: inline-block;
margin: 0;
padding: 0;
position: relative;
text-align: right;
}
[data-id="flexbox-bar-graph"] thead tr th:before {
content:'';
display: inline-block;
height: 10px;
margin: 0 0.5rem 0 2rem;
position: relative;
width: 10px;
}
[data-id="flexbox-bar-graph"] thead tr th:nth-child(1),
[data-id="flexbox-bar-graph"] thead tr th:nth-child(1):before {
display: none;
}
[data-id="flexbox-bar-graph"] thead tr th:nth-child(2):before {
background: var(--bar-color-1);
}
[data-id="flexbox-bar-graph"] thead tr th:nth-child(3):before {
background: var(--bar-color-2);
}
[data-id="flexbox-bar-graph"] thead tr th:nth-child(4):before {
background: var(--bar-color-3);
}
[data-id="flexbox-bar-graph"] thead tr th:nth-child(5):before {
background: var(--bar-color-4);
}
[data-id="flexbox-bar-graph"] thead tr th:nth-child(6):before {
background: var(--bar-color-5);
}
[data-id="flexbox-bar-graph"] tbody {
display: flex;
justify-content: space-between;
min-height: 300px;
width: 100%;
}
[data-id="flexbox-bar-graph"] tbody tr {
display: flex;
flex-direction: column-reverse;
flex-wrap: wrap;
justify-content: flex-end;
padding: 0 50px;
position: relative;
width: 100%;
}
[data-id="flexbox-bar-graph"] tbody tr th {
font-size: 90%;
position: absolute;
text-align: center;
top: 100%;
width: calc(100% - 100px);
}
[data-id="flexbox-bar-graph"] tbody tr td {
align-items: center;
display: flex;
flex-direction: column;
height: 95%;
justify-content: flex-end;
}
[data-id="flexbox-bar-graph"] tbody tr td span {
display: block;
height: calc(var(--data-set) * 100%);
width: 20px;
}
[data-id="flexbox-bar-graph"] tbody tr td:nth-of-type(1) span {
background: var(--bar-color-1);
}
[data-id="flexbox-bar-graph"] tbody tr td:nth-of-type(2) span {
background: var(--bar-color-2);
}
[data-id="flexbox-bar-graph"] tbody tr td:nth-of-type(3) span {
background: var(--bar-color-3);
}
[data-id="flexbox-bar-graph"] tbody tr td:nth-of-type(4) span {
background: var(--bar-color-4);
}
[data-id="flexbox-bar-graph"] tbody tr td:nth-of-type(5) span {
background: var(--bar-color-5);
}
[data-id="flexbox-bar-graph"] tbody tr td p {
font-size: 90%;
margin: 0;
text-align: center;
}
}
</code></pre></div></div>
<h2 id="bonus-styling">Bonus Styling</h2>
<p>In the <a target="_blank" href="https://github.com/bradleytaunt/flexbox-bar-graphs">Flexbox Bar Graph</a> repo, I’ve also included the ability to display these bar graphs horizontally, like so:</p>
<figure>
<img src="/placeholder-image.webp" onclick="this.src='/public/images/flexbox-bar-graph-3.webp'" alt="Flexbox Bar Graph Horizontal" />
<figcaption><b>Click the placeholder to load in the real image</b><br />The flexbox bar graph in the horizontal layout (<a href="/public/images/flexbox-bar-graph-3.webp">direct link to image</a>)</figcaption>
</figure>
<p>The change in CSS is actually quite simple to pull this off - you just need to include the <code class="language-plaintext highlighter-rouge">data-layout</code> attribute on the table itself.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[data-layout="horizontal"] tbody {
min-height: auto;
}
[data-layout="horizontal"] tbody tr {
flex-direction: column;
padding: 0 40px;
}
[data-layout="horizontal"] tbody tr th {
width: calc(100% - 80px);
}
[data-layout="horizontal"] tbody tr th {
text-align: left;
top: calc(100% + 20px);
}
[data-layout="horizontal"] tbody tr td {
flex-direction: row;
height: auto;
justify-content: start;
margin: 10px 0;
}
[data-layout="horizontal"] tbody tr td span {
height: 20px;
width: calc(var(--data-set) * 100%);
}
[data-layout="horizontal"] tbody tr td p {
margin-left: 10px;
}
</code></pre></div></div>
<h2 id="thats-all-folks">That’s All Folks!</h2>
<p>That just about sums things up. Feel free to check out the Github repo itself, open any issues you find or fork it for your own!</p>
<ul>
<li>Github: <a href="https://github.com/bradleytaunt/flexbox-bar-graphs">https://github.com/bradleytaunt/flexbox-bar-graphs</a></li>
<li>Live Demo: <a href="https://flexbox-bar-graphs.netlify.app/">https://flexbox-bar-graphs.netlify.app/</a></li>
</ul>
</content>
</entry>
<entry>
<title>Obvious Javascript 'Injection' Fallback</title>
<link href="https://uglyduck.ca/obvious-js-injection-fallback/"/>
<updated>2020-12-04T00:00:00-05:00</updated>
<id>https://uglyduck.ca/obvious-js-injection-fallback</id>
<content type="html"><p><em>Sometimes websites and web apps might require content</em> to be “injected” via Javascript. I should mention that I am strongly against this practice - but often this kind of thing is out of one’s hands. So, the least I can do is setup these “injections” to have proper fallbacks for users who disable JS. You would be surprised how many developers build <em>empty</em> HTML elements with the assumption they will be filled via Javascript.</p>
<h2 id="our-hypothetical-project">Our Hypothetical Project</h2>
<p>Let’s pretend that we have a total tally that pulls in the number of current users using our fake SaaS app. We would do something like this:</p>
<h4 id="html">HTML</h4>
<p>Here we create an empty <code class="language-plaintext highlighter-rouge">h2</code> tag that will update with the current number of users via js:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;main&gt;
&lt;h2 class="total-tally"&gt;&lt;/h2&gt;
&lt;/main&gt;
</code></pre></div></div>
<h4 id="javascript">Javascript</h4>
<p>You’ll have to use your imagination here and assume that the <code class="language-plaintext highlighter-rouge">totalTally</code> variable pulls in the numbers dynamically via API:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>var totalTally = "273,677" /* This would pull something dynamically in prod */
document.getElementsByClassName("total-tally")[0].innerHTML=totalTally;
</code></pre></div></div>
<h2 id="the-problem">The Problem</h2>
<p>The big issue we have now occurs when a user visits this page without JS enabled. The <code class="language-plaintext highlighter-rouge">h2</code> tag will remain empty and they won’t see anything. I know this seems like a very avoidable issue, but you would be surprised how often it actually happens on the web.</p>
<h2 id="the-overly-simple-solution">The (overly simple) Solution</h2>
<p>The easiest way to avoid these types of empty tags - add static content. I know - mind blowing, right?</p>
<h4 id="html-updated">HTML (updated)</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;main&gt;
&lt;h2 class="total-tally"&gt;200,000+&lt;/h2&gt;
&lt;/main&gt;
</code></pre></div></div>
<p>You might be reading this and saying to yourself, “Wow! Thanks Captain Obvious!” and that’s a fair reaction. This is an <em>obvious</em> demo on purpose. If even one single reader learns to avoid leaving empty HTML tags that are solely dependent on Javascript injection, then I’d say this demo was a huge success.</p>
<p>Rule of thumb: don’t make assumption about your users. Play it safe.</p>
</content>
</entry>
<entry>
<title>Bidirectional Scrolling: Why Not Both?</title>
<link href="https://uglyduck.ca/bidirectional-scrolling/"/>
<updated>2020-11-09T00:00:00-05:00</updated>
<id>https://uglyduck.ca/bidirectional-scrolling</id>
<content type="html"><p><em>I recently came across Adam Silver’s post <a href="https://adamsilver.io/articles/bidirectional-scrolling-whats-not-to-like/">about the merits and pitfalls of bidirectional scrolling</a></em> and found myself conflicted with the design arguments put forth in the article. It’s a very good article overall, and I suggest giving it a read before digging deeper into my post here.</p>
<h2 id="the-premise">The Premise</h2>
<p>The original article argues that displaying page content via horizontal scrolling (and therefore slightly hiding interactive content) creates a few major issues:</p>
<ul>
<li>it increases the chance users won’t see it</li>
<li>there’s a greater reliance on digital literacy</li>
<li>it’s generally more labour intensive for users</li>
</ul>
<p>Adam also makes a solid statement here:</p>
<blockquote>
<p>Having to scroll down and across in a zig zag fashion can be tiresome, especially for people with motor impairments.</p>
</blockquote>
<p>But I don’t believe these issues create a need to completely remove the horizontal “scrolling” design altogether. You can still implement the <code class="language-plaintext highlighter-rouge">See All Items</code> category link, while allowing the horizontal content to load in <em>dynamically</em>. Balance is always key.</p>
<h2 id="not-all-at-once-please">Not All At Once, Please!</h2>
<p>So what exactly do I mean by <em>dynamically</em> loading in horizontal content?</p>
<ul>
<li>The user is shown the top 4 items in a given category</li>
<li>From there, the user can use the <code class="language-plaintext highlighter-rouge">See All Items</code> link to jump into a full category page</li>
<li>If they so desire, they can begin scroll horizontally in a given category row
<ul>
<li>Once they reach the end of the row, 4 more items will load in automatically to expand the list</li>
<li>To avoid a never-ending list, it might be best to limit total row items to ~20 items. At this point the UI could prompt the user to <code class="language-plaintext highlighter-rouge">View All Items</code> in that category.</li>
</ul>
</li>
</ul>
<p>By loading the row content in piece-by-piece, initial loads for users will be faster and subsequent list items will load quickly as well (since they would limit to a set default - in this case only 4).</p>
<h2 id="final-improvements">Final Improvements</h2>
<p>Below you can find a quick, static version of this concept. Here you can see the horizontal list items, along with their corresponding <code class="language-plaintext highlighter-rouge">See All Items</code> links. You’ll have to use your imagination for how new items would load once you each the end of a horizontal row. (I’m too lazy to spend extra time building out that functionality for a hypothetical blog post)</p>
<p class="codepen" data-height="844" data-theme-id="dark" data-default-tab="result" data-user="bradleytaunt" data-slug-hash="pobxpXz" style="height: 844px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Bidirectional Scrolling CSS">
<span>See the Pen <a href="https://codepen.io/bradleytaunt/pen/pobxpXz">
Bidirectional Scrolling CSS</a> by Bradley Taunt (<a href="https://codepen.io/bradleytaunt">@bradleytaunt</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>
</content>
</entry>
<entry>
<title>Menu Toggle with Pure CSS</title>
<link href="https://uglyduck.ca/menu-toggle-css/"/>
<updated>2020-10-19T00:00:00-04:00</updated>
<id>https://uglyduck.ca/menu-toggle-css</id>
<content type="html"><p><em>When thinking through navigation designs for mobile devices</em> sometimes the best option is to store away the content behind a toggle button. This button would then display the menu items upon interaction. Let me show you how to create such an element with only CSS - no need for JavaScript today!</p>
<h2 id="before-we-begin">Before we begin</h2>
<p>I would like to point out that the concept of “toggling” the main menu (even for mobile) is not always the best solution. If you’re interested, you can take a look at a previous article I wrote explaining why: <a href="https://uglyduck.ca/hamburger-menu-alternative/">Using Hamburger Menus? Try Sausage Links</a></p>
<p>Now that we have mentioned possible pitfalls of relying so heavily on toggle menus, let’s build one!</p>
<h2 id="our-final-product">Our Final Product</h2>
<p class="codepen" data-height="372" data-theme-id="dark" data-default-tab="result" data-user="bradleytaunt" data-slug-hash="mdEEvEX" style="height: 372px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Menu Toggle with Pure CSS">
<span>See the Pen <a href="https://codepen.io/bradleytaunt/pen/mdEEvEX">
Menu Toggle with Pure CSS</a> by Bradley Taunt (<a href="https://codepen.io/bradleytaunt">@bradleytaunt</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>
<h2 id="the-html">The HTML</h2>
<p>To implement this design you really don’t need much in terms of HTML:</p>
<ul>
<li>A single <code class="language-plaintext highlighter-rouge">checkbox</code> input</li>
<li>A <code class="language-plaintext highlighter-rouge">label</code> that corresponds to the <code class="language-plaintext highlighter-rouge">checkbox</code></li>
<li>A <code class="language-plaintext highlighter-rouge">nav</code> element to house our unordered list items</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;!-- The checkbox input &amp; label partner --&gt;
&lt;input type="checkbox" id="menu-toggle"&gt;
&lt;label for="menu-toggle"&gt;Menu&lt;/label&gt;
&lt;!-- The navigation we wish to toggle --&gt;
&lt;nav&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=""&gt;Home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=""&gt;About&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=""&gt;Articles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=""&gt;Colophon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=""&gt;Contact&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
</code></pre></div></div>
<p>That’s it!</p>
<h2 id="the-css">The CSS</h2>
<p>The first thing we need to do is “hide” the <code class="language-plaintext highlighter-rouge">checkbox</code> input element. It’s important to avoid using <code class="language-plaintext highlighter-rouge">display: none</code> or <code class="language-plaintext highlighter-rouge">visibility: hidden</code> in order to achieve this. Those CSS properties can negatively impact accessibility (specifically screen readers). So we will be relying on the <code class="language-plaintext highlighter-rouge">position</code>, <code class="language-plaintext highlighter-rouge">z-index</code> and <code class="language-plaintext highlighter-rouge">opacity</code> properties to help us out.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* Set the input position to absolute, send it off screen with zero opacity */
input[type="checkbox"] {
left: -9999px;
opacity: 0;
position: absolute;
}
</code></pre></div></div>
<p>Then we give our corresponding <code class="language-plaintext highlighter-rouge">label</code> a minor face-lift to make it appear more button-like:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* Minor visual styling to make the label more button-y */
label {
border: 1px solid currentColor;
border-radius: 4px;
cursor: pointer;
padding: 10px;
}
</code></pre></div></div>
<p>For our main <code class="language-plaintext highlighter-rouge">nav</code> element, we want to set it’s position to <code class="language-plaintext highlighter-rouge">absolute</code> in order to avoid any janky page rendering issues that might occur when toggling the menu:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* Set nav to absolute (avoids odd page rendering space pop-in) */
nav {
opacity: 0;
position: absolute;
z-index: -2;
}
</code></pre></div></div>
<p>The last step is to actually <em>show</em> the menu if the user toggles the <code class="language-plaintext highlighter-rouge">checkbox</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* Show nav when checkbox is checked */
input[type="checkbox"]:checked ~ nav {
opacity: 1;
z-index: 1;
}
</code></pre></div></div>
<p>It might not look like much, but you now have a fully functional menu toggle - <strong>made with pure CSS</strong>!</p>
<h2 id="with-great-power">With Great Power…</h2>
<p>Although this design is very simple to implement, please remember to use these types of menus wisely. Just because you <em>can</em> do something, doesn’t always mean you <em>should</em>.</p>
</content>
</entry>
<entry>
<title>Poor Man's CSS Full-Bleed Layout</title>
<link href="https://uglyduck.ca/poor-mans-full-bleed/"/>
<updated>2020-10-07T00:00:00-04:00</updated>
<id>https://uglyduck.ca/poor-mans-full-bleed</id>
<content type="html"><p><em>I recently came across the very well written and interesting article</em>, <a href="https://joshwcomeau.com/css/full-bleed/">Full-Bleed Layout Using CSS Grid</a>, while browsing my daily designer feeds. I won’t go into the post’s specifics here (I recommend you read the article for yourself) but it details how to render full-bleed element effects utilizing the CSS <code class="language-plaintext highlighter-rouge">grid</code> property.</p>
<p>While the approach in the article works perfectly fine, I thought to myself, “Is there not a simpler, more backwards compatible way to do this?”. Indeed there is.</p>
<p><strong>Don’t care about the tutorial?</strong> <a href="#live-demo">Skip down to the live demo</a></p>
<h2 id="make-the-web-backwards-compatible">Make the Web Backwards Compatible</h2>
<p>I try my best when creating specific element designs or layouts to have everything render consistently across almost <em>all</em> browsers. This tends to include the obvious front-runners: Chrome, Firefox, Safari - but I also try my best not to ignore the oldies: IE11, Edge and older versions of Opera. I believe if most web designers even loosely followed this concept we wouldn’t be stringing together barely implemented CSS properties and hacking together polyfills for all the unsupported browsers. Just my two cents.</p>
<p>What does this have to do with full-bleed exactly? Well, the CSS we will be using in this demo is fully compatible with pretty much <em>any browser</em> that can run the basic version of CSS rendering. If the browser you’re targeting supports <code class="language-plaintext highlighter-rouge">max-width</code> and <code class="language-plaintext highlighter-rouge">margins</code> - then have no fear, full-bleeds are here.</p>
<h2 id="our-html-example">Our HTML Example</h2>
<p>Let’s create a single-column blog layout for this example. We will include a heading, some paragraphs, an image, and a blockquote to keep it simple:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;main&gt;
&lt;article&gt;
&lt;h1&gt;Main Heading&lt;/h1&gt;
&lt;p&gt;Et non consequat eiusmod minim fugiat. Magna duis veniam ex reprehenderit occaecat sit. Nisi ut ex aliquip magna enim.&lt;/p&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Laborum repellat ab earum commodi, consequuntur totam adipisci doloremque asperiores quae at quia non temporibus ipsam voluptate voluptatem ipsa nostrum suscipit aliquid!&lt;/p&gt;
&lt;img src='https://images.unsplash.com/photo-1569083692634-f8db90c093ef?ixlib=rb-1.2.1&amp;q=85&amp;fm=jpg&amp;crop=entropy&amp;cs=srgb&amp;ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt="Some Image"&gt;
&lt;p&gt;Ex excepteur Lorem reprehenderit dolore in consequat voluptate commodo ipsum consequat ea et. Nisi tempor proident anim tempor. Laboris est sunt cillum deserunt culpa proident cillum laborum voluptate. Est exercitation Lorem reprehenderit eu ipsum nisi et.&lt;/p&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus dicta perspiciatis vel ex officiis, nisi optio nihil aspernatur exercitationem sed nobis architecto maxime eaque omnis eos, repellendus necessitatibus provident explicabo?&lt;/p&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid placeat ipsum totam, facere animi tenetur explicabo at veniam, culpa vitae debitis hic modi velit cum perferendis minima quos sit quisquam.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To be, or not to be, that is the question:&lt;br&gt;Whether 'tis nobler in the mind to suffer&lt;br&gt;The slings and arrows of outrageous fortune,&lt;br&gt;Or to take Arms against a Sea of troubles,&lt;br&gt;And by opposing end them: to die, to sleep;&lt;/p&gt;
&lt;cite&gt;- Some guy&lt;/cite&gt;
&lt;/blockquote&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus dicta perspiciatis vel ex officiis, nisi optio nihil aspernatur exercitationem sed nobis architecto maxime eaque omnis eos, repellendus necessitatibus provident explicabo?&lt;/p&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid placeat ipsum totam, facere animi tenetur explicabo at veniam, culpa vitae debitis hic modi velit cum perferendis minima quos sit quisquam.&lt;/p&gt;
&lt;/article&gt;
&lt;/main&gt;
</code></pre></div></div>
<p>This works well as it is, but we will need to “section” off our different areas of content based on whether we want them <code class="language-plaintext highlighter-rouge">full-bleed</code> or not. We do this with - you guessed it - the <code class="language-plaintext highlighter-rouge">section</code> element. In this demo we want the image and blockquote to become <code class="language-plaintext highlighter-rouge">full-bleed</code> when rendered, so we’ll add the <code class="language-plaintext highlighter-rouge">full-bleed</code> class to those sections for now (more on that later):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;main&gt;
&lt;article&gt;
&lt;section&gt;
&lt;h1&gt;Main Heading&lt;/h1&gt;
&lt;p&gt;Et non consequat eiusmod minim fugiat. Magna duis veniam ex reprehenderit occaecat sit. Nisi ut ex aliquip magna enim.&lt;/p&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Laborum repellat ab earum commodi, consequuntur totam adipisci doloremque asperiores quae at quia non temporibus ipsam voluptate voluptatem ipsa nostrum suscipit aliquid!&lt;/p&gt;
&lt;/section&gt;
&lt;section class="full-bleed"&gt;
&lt;img src='https://images.unsplash.com/photo-1569083692634-f8db90c093ef?ixlib=rb-1.2.1&amp;q=85&amp;fm=jpg&amp;crop=entropy&amp;cs=srgb&amp;ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt="Some Image"&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;p&gt;Ex excepteur Lorem reprehenderit dolore in consequat voluptate commodo ipsum consequat ea et. Nisi tempor proident anim tempor. Laboris est sunt cillum deserunt culpa proident cillum laborum voluptate. Est exercitation Lorem reprehenderit eu ipsum nisi et.&lt;/p&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus dicta perspiciatis vel ex officiis, nisi optio nihil aspernatur exercitationem sed nobis architecto maxime eaque omnis eos, repellendus necessitatibus provident explicabo?&lt;/p&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid placeat ipsum totam, facere animi tenetur explicabo at veniam, culpa vitae debitis hic modi velit cum perferendis minima quos sit quisquam.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="full-bleed"&gt;
&lt;blockquote&gt;
&lt;p&gt;To be, or not to be, that is the question:&lt;br&gt;Whether 'tis nobler in the mind to suffer&lt;br&gt;The slings and arrows of outrageous fortune,&lt;br&gt;Or to take Arms against a Sea of troubles,&lt;br&gt;And by opposing end them: to die, to sleep;&lt;/p&gt;
&lt;cite&gt;- Some guy&lt;/cite&gt;
&lt;/blockquote&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus dicta perspiciatis vel ex officiis, nisi optio nihil aspernatur exercitationem sed nobis architecto maxime eaque omnis eos, repellendus necessitatibus provident explicabo?&lt;/p&gt;
&lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid placeat ipsum totam, facere animi tenetur explicabo at veniam, culpa vitae debitis hic modi velit cum perferendis minima quos sit quisquam.&lt;/p&gt;
&lt;/section&gt;
&lt;/article&gt;
&lt;/main&gt;
</code></pre></div></div>
<p>That’s it for the HTML!</p>
<h2 id="the-magic-css-not-really">The Magic CSS (Not Really)</h2>
<p>Now take a deep breath and get ready for some hard CSS work:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>article {
width: 100%;
}
article section {
margin: 0 auto;
max-width: 480px; /* This can be whatever you want */
width: 100%;
}
article section.full-bleed {
max-width: 100%;
}
</code></pre></div></div>
<p>That’s it. <em>Really</em>. Now any element (blockquotes, specific headers, navigations, footers, etc) that you want to layout as “full-bleed”, just wrap it in a <code class="language-plaintext highlighter-rouge">section</code> tag and sick the <code class="language-plaintext highlighter-rouge">full-bleed</code> class on it. Done and done.</p>
<p>Obviously you’ll want to add more styling to clean-up and make your full-bleed layouts more beautiful. This demo was just more of a starting point.</p>
<h3 id="maybe-not-quite-full-bleed">Maybe Not-Quite-Full-Bleed?</h3>
<p>You could also further customize your options by including a class like <code class="language-plaintext highlighter-rouge">half-bleed</code>, which maybe only expands slightly outside the main section <code class="language-plaintext highlighter-rouge">max-width</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>article section.half-bleed {
max-width: 960px;
}
</code></pre></div></div>
<h3 id="sidenote">Sidenote</h3>
<p>For those concerned about accessibility: The <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section">section element</a> is a flow content element, so it can contain almost all HTML elements.</p>
<h2 id="live-demo">Live Demo</h2>
<p class="codepen" data-height="483" data-theme-id="light" data-default-tab="result" data-user="bradleytaunt" data-slug-hash="mdEdjzz" style="height: 483px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Poor Man's CSS Full-Bleed">
<span>See the Pen <a href="https://codepen.io/bradleytaunt/pen/mdEdjzz">
Poor Man's CSS Full-Bleed</a> by Bradley Taunt (<a href="https://codepen.io/bradleytaunt">@bradleytaunt</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>
</content>
</entry>
<entry>
<title>Simple Navigation Setup in Jekyll 3.9.0</title>
<link href="https://uglyduck.ca/simple-jekyll-navigation/"/>
<updated>2020-09-29T00:00:00-04:00</updated>
<id>https://uglyduck.ca/simple-jekyll-navigation</id>
<content type="html"><p><em>I have found that there is a lot of information on the internet</em> in regards to setting up “dynamic” navigation in Jekyll. The problem I’ve noticed is that a good amount of these implementations are overly complex. Here is the simplest way that I tend to use when building out <code class="language-plaintext highlighter-rouge">nav</code> elements in Jekyll (3.9.0 as of this writing).</p>
<h2 id="creating-the-directories--files">Creating the Directories &amp; Files</h2>
<p>In your Jekyll project, at the top level, you need to create a directory called <code class="language-plaintext highlighter-rouge">_data</code>. Inside this folder we will be creating a new file called <code class="language-plaintext highlighter-rouge">navigation.yml</code>. The contents of this file will contain all your navigation links and they are rendered like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- title: Home
url: /
- title: Articles
url: /articles/
- title: About
url: /about/
</code></pre></div></div>
<h2 id="dynamically-rendering-the-navigation">Dynamically Rendering the Navigation</h2>
<p>The next and final step is rendering out the navigation with a simple loop:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% for item in site.data.navigation %}
&lt;li&gt;
&lt;a href="{{ item.url }}"&gt;&lt;span&gt;{{ item.title }}&lt;/span&gt;&lt;/a&gt;
&lt;/li&gt;
{% endfor %}
</code></pre></div></div>
<h2 id="highlight-current-page">Highlight Current Page</h2>
<p>It’s also very easy to extend this method to add a CSS class based on whether a user is on the currently selected page or not:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% for item in site.data.navigation %}
&lt;li&gt;
{% if item.url == page.url %}
&lt;a class="active" href="{{ item.url }}"&gt;&lt;span&gt;{{ item.title }}&lt;/span&gt;&lt;/a&gt;
{% else %}
&lt;a href="{{ item.url }}"&gt;&lt;span&gt;{{ item.title }}&lt;/span&gt;&lt;/a&gt;
{% endif %}
&lt;/li&gt;
{% endfor %}
</code></pre></div></div>
<p><br /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* Custom styling for active class */
li a.active { color: red; }
</code></pre></div></div>
<p>Congrats! You now have fully functional, dynamic navigation on your Jekyll site.</p>
</content>
</entry>
<entry>
<title>Portable Pi: Cheap Raspberry Pi Zero Hacker Terminal</title>
<link href="https://uglyduck.ca/cheap-portable-pi/"/>
<updated>2020-09-16T00:00:00-04:00</updated>
<id>https://uglyduck.ca/cheap-portable-pi</id>
<content type="html"><p><em>I recently came across <a href="https://n-o-d-e.net/zeroterminal3.html">the incredibly cool design</a> for an “ultimate Raspberry Pi computer”</em> by NODE and was inspired to create my own “portable” Pi device. Although, with my concept, I decided to set a few restrictions on the build:</p>
<ul>
<li>I wanted to keep the total cost as low as possible
<ul>
<li>including the Pi Zero <em>and</em> extra hardware</li>
</ul>
</li>
<li>I wanted to avoid any custom casings or need for 3D printed materials</li>
<li>I wanted to make the full design extremely portable / lightweight</li>
<li>I wanted to keep the build as user-friendly as possible (no soldering, odd hacks etc.)</li>
</ul>
<p>With these constraints put in place, I built my very own Raspberry Pi Zero “hacker” terminal. You can see the finished product below:</p>
<p><img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/portable-pi-1.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/portable-pi-1.webp 320w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/portable-pi-1.webp 540w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/portable-pi-1.webp 760w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/portable-pi-1.webp 980w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/portable-pi-1.webp 1200w" sizes="100vw" alt="Portable Pi Front" width="2448" height="3264" crossorigin="anonymous" /></p>
<p><img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/portable-pi-2.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/portable-pi-2.webp 320w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/portable-pi-2.webp 540w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/portable-pi-2.webp 760w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/portable-pi-2.webp 980w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/portable-pi-2.webp 1200w" sizes="100vw" alt="Portable Pi Back" width="2448" height="3264" crossorigin="anonymous" /></p>
<p>I’m pretty happy with the final outcome and best of all - you can easily recreate this yourself with little-to-no effort!</p>
<p>So enough chit-chat - let’s build this bad-boy!</p>
<blockquote>
<p><strong>Note:</strong> Most items in the hardware section are Amazon affiliate links</p>
</blockquote>
<h2 id="table-of-contents">Table of Contents</h2>
<ol>
<li><a href="#expectations">Setting Expectations</a></li>
<li><a href="#hardware">The Hardware</a></li>
<li><a href="#software">The Software</a></li>
<li><a href="#setup">Getting Setup</a></li>
<li><a href="#summary">Final Summary</a></li>
</ol>
<h2 id="expectations">Setting Expectations</h2>
<p><strong>This hardware will not</strong> replace your laptop or any other “high-powered” portable device. Honestly, you’re mobile phone would (most likely) work better in terms of a compact-sized computer. The concept behind this “portable pi” project was to make something <em>for fun</em>.</p>
<p>Please reserve any sassy comments about how buying a cheap $50 laptop off eBay would be more efficient than building out something like I have done here. You’re missing the point entirely.</p>
<p>Moving on for those still interested…</p>
<h2 id="hardware">The Hardware</h2>
<p>I live in Canada, so some of the options are based on available hardware near my location. You’re likely to find similar hardware close to your own country for better pricing / shipping costs.</p>
<blockquote>
<p><strong>Note</strong>: I’ve decided to use a wireless, USB dongle-based keyboard. You can opt to use a bluetooth-based keyboard, but I just find them more trouble than they are worth (keeping a charge, losing connections, interference from other devices)</p>
</blockquote>
<ul>
<li>SBC: <a href="https://amzn.to/3jCVXHG">Raspberry Pi Zero WH</a></li>
<li>Storage / OS: <a href="https://amzn.to/2GHm01R">64GB Micro SD Card</a> (Always have an SD backup!)</li>
<li>Battery HAT: <a href="https://amzn.to/3jGq8xp">Li-ion Battery HAT</a></li>
<li>Battery: <em>This depends on your location. I was able to order <a href="https://www.walmart.ca/en/ip/Brand-New-4-Pcs-2500mAh-TR14500-3-7V-Rechargeable-Li-ion-Battery-for-Flashlight-Torch/PRD3Z73LMZZ3VK9">online through Walmart</a>, but if you live in the USA there are plenty of battery-specific vendors available. Find whatever works best for your needs!</em></li>
<li>Display: <a href="https://www.buyapi.ca/product/3-5-inch-raspberry-pi-tft-display/">KeDei 3.5-inch TFT Display</a></li>
<li>Keyboard: <a href="https://amzn.to/2Gqkwt6">Mini USB Wireless Keyboard</a></li>
<li>Adapter: <a href="https://amzn.to/36I6T3c">Micro USB OTG Cable</a></li>
</ul>
<h4 id="raspberry-pi-zero-wh">Raspberry Pi Zero WH</h4>
<p>You will need a Raspberry Pi Zero W with GPIO header pins already soldered on to the board. (You could also do this yourself manually if you have the tools and skill to do so)</p>
<h4 id="li-ion-battery-hat">Li-Ion Battery HAT</h4>
<p>This HAT holds a single li-ion battery and for our purposes will connect with our Pi Zero via the GPIO header pins. You could also look into double-set battery HATs if that is something you would prefer - for my needs a single rechargeable battery works fine.</p>
<p>Since our main “OS” is purely terminal based, I find you don’t really need a li-ion battery with a massive amount of power storage. 2500mAh will easily last a handful of hours mucking about with emails, files and web browsing. Plus, you can always carry a few extras for swapping out on-the-go.</p>
<h4 id="35-inch-tft-display">3.5-inch TFT Display</h4>
<p>A simple and fairly cheap touchscreen display. We won’t require our display to render videos or pretty graphics (since we will be entirely terminal-based) so this works just fine for our needs. I am using version 6.3 for this build.</p>
<h4 id="micro-usb-otg-adapter">Micro USB OTG Adapter</h4>
<p>As mentioned above, this item is optional if you feel more comfortable with using a pure bluetooth keyboard. I just personally prefer this method.</p>
<h4 id="wireless-keyboard">Wireless Keyboard</h4>
<p>In my product demo image at the beginning of the article, I’m actually using an old Logitech wireless keyboard since this one still hasn’t arrived. I plan to swap them out once I have it - the Logitech is far to large to be deemed “portable”.</p>
<h2 id="software">The Software</h2>
<ul>
<li>Operating System: <a href="https://www.raspberrypi.org/downloads/raspberry-pi-os/">Raspberry Pi OS Lite</a></li>
<li>Display Driver: <a href="http://kedei.net/raspberry/raspberry.html">KeDei 3.5 Driver</a></li>
<li>Email: <a href="https://github.com/LukeSmithxyz/mutt-wizard">Neomutt</a> (via Mutt Wizard)</li>
<li>Web Browser: <a href="https://lynx.browser.org/">Lynx</a></li>
<li>Text Editor: <a href="https://www.nano-editor.org/">Nano</a></li>
<li>Code Editor: <a href="https://www.vim.org/">Vim</a></li>
</ul>
<h2 id="setup">Getting Setup</h2>
<p>I will be setting up this “portable pi” via <a href="https://www.raspberrypi.org/documentation/configuration/wireless/headless.md">headless installation</a> with my main <a href="https://uglyduck.ca/my-pi-desktop/">Raspberry Pi 4 desktop</a>. Included below are some quick instructions on how to setup the Raspberry Pi Zero for <code class="language-plaintext highlighter-rouge">ssh</code> access.</p>
<h4 id="preparing-the-microsd-card">Preparing the microSD card</h4>
<ul>
<li>Use Raspberry Pi Imager to write the Raspberry Pi Lite OS to your microSD card
<ul>
<li>Once complete, remove the microSD storage device and then re-connect it</li>
</ul>
</li>
<li>Navigate to the <code class="language-plaintext highlighter-rouge">boot</code> folder of the microSD device - we will be adding 2 files here
<ul>
<li>First add a completely empty file and name it <code class="language-plaintext highlighter-rouge">ssh</code> (no file extension)</li>
<li>Next add an empty file named <code class="language-plaintext highlighter-rouge">wpa_supplicant.conf</code></li>
</ul>
</li>
<li>Open the <code class="language-plaintext highlighter-rouge">wpa_supplicant.conf</code> file in a code / text editor and paste the following (be sure to change <code class="language-plaintext highlighter-rouge">country</code> to your proper country code, as well as properly setting your network name / password):</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=US
network={
ssid="Your network name/SSID"
psk="Your WPA/WPA2 security key"
key_mgmt=WPA-PSK
}
</code></pre></div></div>
<ul>
<li>Unmount the microSD device, then place the microSD card in your RPi Zero</li>
<li>Assemble the Raspberry Pi Zero with the Battery HAT (li-ion battery attached) connected via GPIO pins, and the 3.5 display connected on-top of the battery HAT’s pins</li>
<li>Turn on the battery HAT switch to power the RPi Zero. Give the device ~90 seconds to perform the initial boot</li>
</ul>
<blockquote>
<p><strong>Note</strong>: the display will turn on but only show a white screen - this is normal</p>
</blockquote>
<h4 id="connecting-to-your-zero-via-ssh">Connecting to your Zero via SSH</h4>
<p>Open the terminal on your Raspberry Pi desktop and enter the following command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh pi@raspberrypi.local
</code></pre></div></div>
<p>You will then be asked if you wish to trust this device (say yes), then prompted for the <code class="language-plaintext highlighter-rouge">pi</code> user’s password - which is <code class="language-plaintext highlighter-rouge">raspberry</code>. After a moment you will be directly connected to your Raspberry Pi Zero. Hooray!</p>
<p>We just want to make a couple small configuration changes before going forward (mostly for security):</p>
<ol>
<li>Run the command <code class="language-plaintext highlighter-rouge">sudo raspi-config</code></li>
<li>Select “Change User Password” from the menu and change the password
<img src="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_1200/https://uglyduck.ca/public/images/raspi-config.webp" srcset="https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_320/https://uglyduck.ca/public/images/raspi-config.webp 320w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_540/https://uglyduck.ca/public/images/raspi-config.webp 540w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_760/https://uglyduck.ca/public/images/raspi-config.webp 760w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_980/https://uglyduck.ca/public/images/raspi-config.webp 980w, https://res.cloudinary.com/bradtaunt/image/fetch/c_limit,f_auto,q_auto,w_984/https://uglyduck.ca/public/images/raspi-config.webp 984w" sizes="100vw" alt="Raspberry Config" width="984" height="321" crossorigin="anonymous" /></li>
<li>After that, select “Network Options” and change your device name
<ul>
<li>This will avoid any conflicts of connecting to newer RPi devices down the line</li>
</ul>
</li>
</ol>
<p>That’s it!</p>
<h4 id="installing-display-driver">Installing display driver</h4>
<p>Next we will need to download and run the drivers needed for our 3.5-inch display to play nicely with our Pi Zero (just a blank white screen doesn’t help us much). While connected to our RPi Zero via SSH, run the following commands:</p>
<p><code class="language-plaintext highlighter-rouge">wget http://kedei.net/raspberry/v6_1/LCD_show_v6_1_3.tar.gz</code></p>
<p>If you have snail-paced rural internet like I do, now is a good time to go and grab a coffee while this download completes.</p>
<p>Once the download has finished, extract the contents and navigate to the new directory and install the driver:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo tar xzf LCD_show_v6_1_3.tar.gz
cd LCD_show_v6_1_3
sudo ./LCD35_v
</code></pre></div></div>
<p>Once completed, the RPi Zero will reboot and everything should work as expected!</p>
<h4 id="installing-software">Installing software</h4>
<p>I’m not going to jump into heavy details about installing terminal-based software for this setup. The links I’ve attached in the software section should provide you with more than enough information to get started. For reference, I’m using a very basic set of applications:</p>
<ul>
<li>Neomutt (emails)</li>
<li>Lynx (quick and simple web browsing)</li>
<li>Nano (writing notes / blog posts in a hurry)</li>
<li>Vim (making any config edits or project work)</li>
</ul>
<h2 id="summary">Final Summary</h2>
<p>There you have it - a terminal-based, portable Raspberry Pi Zero “hacker” device.</p>
<p>Is it practical? Not really. Can it replace any other <em>portable</em> device you might own? Probably not. Is it fun to make and experiment on? You bet! And in the end - isn’t that what matters most with pet Pi projects?</p>
</content>
</entry>
<entry>
<title>Over-Engineering an Oil Tank Gauge</title>
<link href="https://uglyduck.ca/over-engineering-an-oil-tank/"/>
<updated>2020-09-09T00:00:00-04:00</updated>
<id>https://uglyduck.ca/over-engineering-an-oil-tank</id>
<content type="html"><p><em>I almost went down the path of investing a huge amount of time</em> and effort into fixing a stuck oil fuel tank float-gauge in my house. Recently, the float mechanism became stuck and permanently displayed <code class="language-plaintext highlighter-rouge">empty</code> regardless of how much fuel was in the tank - not ideal. It’s a 20 year-old tank, so I wasn’t surprised that the float finally gave out.</p>
<p>Being the wannabe tinkerer that I am, a light bulb went off in my head and I started thinking on how to incorporate some ultrasonic system to display the accurate fuel reading digitally. Obviously my first thought was just to replace the float gauge with a new one and be done with it. That didn’t sound very <em>fun</em> though.</p>
<p>I briefly looked at other similar projects and started brainstorming the best way I would implement this for my own situation. The best option I came across seemed to be this: <a href="https://scottiestech.info/2017/10/24/diy-ultrasonic-fuel-gauge-level-sensor/">DIY Ultrasonic Fuel Gauge / Level Sensor</a></p>
<p>An ultrasonic system with a cool mini display sounded pretty rad - much cooler than just replacing the broken gauge with a new float arm. So it was settled.</p>
<h2 id="my-oil-think-tank">My oil think-tank</h2>
<p>I quickly thought through my options in my head before jumping too far into things (and even included a “poor-mans” temperature strip option):</p>
<table>
<thead>
<tr>
<th>Solution</th>
<th>Cost</th>
<th style="text-align: right">Time</th>
</tr>