AAADAAAMrandom thoughts2025-10-28T00:00:00Zhttps://aaadaaam.com/Adam Stoddard[email protected]Vinyl is dead. Long live vinyl.2016-01-01T00:00:00Zhttps://aaadaaam.com/notes/vinyl/<p>My latest obsession is a very pretty, very shiny Pro-Ject Audio Carbon Debut DC turntable. Listening to music on it has quickly become my favorite way to listen, and not just because I get to use words like “plinth” in casual conversation.</p>
<p>I’m not alone. Amidst the sea of depression that defines the current state of the music business lies an interesting curiosity. While physical and digital album sales have been locked in a downward spiral since 2005, vinyl sales have <em>exploded</em>. In 2014 alone, digital sales declined 9.5% while vinyl sales <em>increased by 52%</em>.</p>
<p>The reason why may not be immediately obvious. After all, if you’ve spent any amount of time around a turntable you’d know exactly why vinyl was largely abandoned in favor of other formats.</p>
<p>Despite claims from many a vinyl purist, it doesn’t sound objectively better. It’s more expensive, less convenient, non-portable, requires more maintenance (both media and turntable), requires fiddly adjustments for best playback, can’t shuffle, repeat, or do much of anything. You can’t buy music for it instantly, etc. And for a minimalist model like mine, the only luxury is a tone arm lifter, which is really more for the benefit of your records instead of a convenience for you.</p>
<p>In other words, it’s not a terribly <em>logical</em> preference.</p>
<p>So then what’s the deal? Mass delusion? Those darn hipsters and their silly nostalgia? It comes down to one word. <strong>Experience.</strong></p>
<p>As convenient and portable as digital music is, it has a singular flaw. It has no physical presence. You can’t see it, touch it, smell it. People who are choosing vinyl tend to be music lovers who are looking for a more emotional and personal connection to the music and artists they love. They also tend to have grown up in a digital only world, which is why experiencing vinyl is roughly equivalent for them to meeting a human in person for the first time when you’re only other contact with humanity has been telephone calls. There’s just no comparison.</p>
<p>But hey, what about CD’s? They’re physical media for sure, and the packaging can do some serious heavy lifting on the experience side of things when done right. But once you slap that disc in, you might as well be playing a digital file. It’s just another dead black box, and <em>nothing</em> like playing a record.</p>
<p>The satisfying thunk of the power switch as the record slowly spins up to speed. The few seconds of suspense as the tone arm slowly descends towards the platter. The thump that pumps from your speakers when it finally does. The few seconds of silence punctuated only by the occasional crackle or pop before the music starts. The fact that I have to get up and flip sides. The fact that I <em>can’t</em> just skip songs. The Rube Goldberg-esqueness of it all. And yeah, nostalgia is in there too.</p>
<p>On top of that, artists have recognized this new wave of vinyl enthusiasts and are pressing custom color vinyl to double down on the experience front. You know what’s better than a modern and minimalist turntable? A modern and minimalist turntable with a transparent red record spinning on it.</p>
<h2>The Rider and The Elephant</h2>
<p>The resurgence in vinyl despite the shortcomings of the format is handily explained by a behavioral psychology mental model called “The Rider and The Elephant”. The model was developed by Jonathan Haidt and is incredibly useful to explain how and why humans make decisions.</p>
<p>The rider represents your <strong>rational</strong> side, and the elephant represents your <strong>emotional</strong> side. The rider is in control, but that control is precarious. If the elephant decides to go in a different direction than the rider wants, the rider is powerless to do anything about it.</p>
<p>In the case of vinyl, that’s exactly what’s happening. Your rational mind may say “inconvenient!”, “expensive!”, “definitely do not buy this!”, but your (well, my) emotions say <em>“honey badger don’t care”</em>.</p>
<p>If you’re attempting to persuade someone to do something, you’re chance of success is much, much higher if you can convince <strong>both</strong> the rider and elephant.</p>
<p>When you don’t is when you hear statements like <em>“I know it’s good for me (rider), but it looks and tastes gross (elephant)”</em>, or <em>“it fits my needs (rider), but I just don’t love it (elephant).”</em></p>
<p>When you do is when you hear statements like <em>“shut up and take my money!”</em></p>
<p>Various industries tends towards trying to persuade the rider or elephant to differing degrees. The alcohol industry for example almost exclusively appeals to the elephant. People will want to sex you!” is the baseline pitch of 99% of alcohol advertising.</p>
<p>The automotive industry tends to do both. They make cars and marketing that appeal to your emotions, but then back it up with MPG, 0-60 times, safety test results, etc.</p>
<p>And then there’s the software industry. For most of its history the software industry has barely noticed the existence of the elephant. It’s been all rider, all the time. Apple’s rise to dominance was the event that finally had the industry stand up and say “OK, I guess experience really does matter”, but the lesson hasn’t fully sunk in yet. That’s why you still see site after site focusing on features instead of benefits. That’s why anything that’s not a bullet point feature tends to end life as an inglorious sacrifice on the altar of MVP.</p>
<p>But there’s an upside here. If you can identify and speak to the emotional drivers your customers have, you can compete handily against entrenched players in your space. Even if they have bigger budgets and more robust offerings. So acknowledge the elephant in the room. You’ll be better off for it.</p>
Are you sure?2017-03-01T00:00:00Zhttps://aaadaaam.com/notes/are-you-sure/<p>I was Twittering away the other day, and <a href="https://prettier.io/">Prettier</a> came across my feed. Prettier turns the conceptual moorings of linting on its head. Instead of “Well, actually”-ing you when you make a commit and forcing you to fix whatever pedantic issues it’s complaining about, it just… <em>fixes them</em>.</p>
<p>As you can guess, I have an uneasy relationship with lint. I value what linting brings to the table, but I don’t like my tools saying, <em>“You need to do x before I’ll do what you want.”</em> Who’s serving who here? <strong>You’re a hammer. <em>Swing</em>.</strong></p>
<p>This is a niche example of software behaving inconsiderately. A common example takes the form of three words guaranteed to upend the user / tool power dynamic with every invocation: <em>“are you sure?”</em></p>
<p>It seems like an innocuous enough question to ask before you complete an action on their behalf, but think about how it would play out in real life.</p>
<p><strong>Person A:</strong> <em>“I’ve got my hands full with all this stuff. Mind opening the door?”</em></p>
<p><strong>Person B:</strong> <em>“Are you sure you want to open the door?”</em></p>
<p><strong>Person A:</strong> <em>“Uh, yeah. I wouldn’t have asked if I wasn’t.”</em></p>
<p><strong>Person B:</strong> <em>“OK, I’ll open the door.”</em></p>
<p><strong>Person A:</strong> <em>“Great, can you close it after me? I don’t want the dog to get out.”</em></p>
<p><strong>Person B:</strong> <em>“Are you sure you want me to close the door?”</em></p>
<p><strong>Person A:</strong> <em>“I really fucking hate you right now.”</em></p>
<p>You’d never do this in real life, but it happens all the time in software. Alan Cooper nails exactly what’s wrong with this question in his seminal book, About Face:</p>
<blockquote>
<p>“Interactive products should stand by their convictions. If we tell the computer to discard a file, it shouldn’t ask, “Are you sure?” Of course we’re sure; otherwise, we wouldn’t have asked. It shouldn’t second-guess us or itself.</p>
<p>On the other hand, if the computer has any suspicion that we might be wrong (which is always), it should anticipate our changing our minds by being prepared to undelete the file upon our request.</p>
<p>How often have you clicked the Print button and then gone to get a cup of coffee, only to return to find a fearful dialog box quivering in the middle of the screen, asking, “Are you sure you want to print?” This insecurity is infuriating and the antithesis of considerate human behavior.”</p>
<p>—Alan Cooper</p>
</blockquote>
<p>Self-confidence is one dimension of Cooper’s core design principle: <strong>Software should behave like a considerate human being.</strong></p>
<p>What are some of the other dimensions that make a human (and software) considerate? They use common sense, don’t ask a bunch of unnecessary questions, keep you informed, help you avoid awkward mistakes, fail gracefully, anticipate the needs of others, and are preceptive and conscientious.</p>
<p>If you’re trying to make considerate software, don’t ask your user if they’re sure. Assume they’re competent humans who mean what they intend. If the action is potentially destructive, give them a way to gracefully recover if they change their mind (undo/redo, archive states, auto-save drafts, etc).</p>
<p>If you’ve fallen into the “Are you sure?” trap, well, welcome to the club. I certainly have, and so have a bunch of others. Sometimes you don’t realize you’re doing something inconsiderate, and sometimes time and money dictate other choices.</p>
<p>But next time you find yourself reaching for that question, take a step back and think about what you’re trying to accomplish. Do you really need to ask the user that? Is there another approach you could take that would be more considerate? If so, take it.</p>
There’s such a thing as “too helpful”2017-03-22T00:00:00Zhttps://aaadaaam.com/notes/too-helpful/<p>Not too long ago Jason Fried wrote about his <a href="https://m.signalvnoise.com/best-buy-vs-the-apple-store-abb16cf342c0">experience shopping at Apple Stores</a>, in which he described the common occurrence of walking into a store and being immediately set upon by a too helpful Apple rep (the “bouncer”).</p>
<p>In trying to be as attentive, helpful, and friendly AS HUMANLY POSSIBLE, this bouncer ended up being too helpful. <strong>Aggressively helpful.</strong></p>
<p>What’s it mean to be aggressively helpful? It means that in your attempt to be as helpful as possible, you’ve prioritized that goal <strong>over the customer’s own desire</strong>. They don’t want your help right now, but by Grabthar’s Hammer, you’re going to give it to them.</p>
<p>If you’ve ever set foot on a car lot, you know <em>exactly</em> what aggressively helpful looks like. It’s personified by the ill-suited, slick-haired vultures with a plastic smile who swoop in the <em>instant</em> your toe touches pavement. Just wanted to look around? TOO BAD! You’re getting help!</p>
<p>Once largely confined to used car salesmen, aggressive helpfulness is making fast inroads among startups. Paradoxically, startups that <em>genuinely care</em> about customer experience (or at least purport to) seem to be the most susceptible to it.</p>
<p>Visited the website for a startup recently? If so, this will probably sound familiar. You land on the site and BLAMMO! Up pops a chat notification, with whomever-in-charge-of-customer-razzmatazz offering enthusiastically!!! if they can help you with anything?</p>
<p>In an effort to engage customers and serve them better, they’re often accomplishing the opposite. Just like it doesn’t feel great to be accosted the moment you step onto a car lot or into an Apple Store, it doesn’t feel great to be “engaged with” the moment you land on a website.</p>
<p><strong>Rule #1 of being genuinely helpful: Respect the wishes of the person you’re trying to help.</strong></p>
<p>It’s an innocent mistake to make. If you think of helpfulness and un-helpfulness as two ends of a spectrum, you can only do more good by adding more helpfulness, right?</p>
<p>But that’s not how it works. In reality, those two ends aren’t ends at all. Trying to be more helpful by violating rule #1 pushes you right back towards being unhelpful.</p>
<p>So what do you do if you’ve veered towards too helpful? Instead of unleashing an unrelenting fury of helpfulness when someone walks in your virtual door, <em>chill for a minute</em>.</p>
<p>Let the customer poke around, kick some tires, form some thoughts.</p>
<p>Be there, waiting. Ready to spring into action at a moment’s notice. Make getting your attention easy and obvious. Maybe offer some help if someone’s wandering around with a scrunched look on their face (or the digital equivalent thereof).</p>
<p>Just don’t let your desire to be as helpful as possible override what your customer actually wants.</p>
Everything I know about marketing I learned from the 1990 movie “Crazy People.”2018-01-11T00:00:00Zhttps://aaadaaam.com/notes/crazy-people/<p>For many many many people, the term “marketing” is interchangeable with words like “spin,” “flim-flam,” “malarkey,” and of course, “bullshit.”</p>
<p>You can’t <em>really</em> blame people who think this way. A huge amount of marketing <em>is</em> reality-ignoring bullshit/spin/hokum/snake oil.</p>
<p>This fact — that marketing is mostly poppycock — is the foundation that the movie <em>Crazy People</em> builds on. For anyone unfamiliar with the movie, here’s the quick synopsis (I swear this is relevant):</p>
<p>In <em>Crazy People</em>, Dudley Moore’s character is an advertising executive who does something absolutely <em>unthinkable</em> — he creates a series of ads that tell the truth. Here’s a few examples:</p>
<blockquote>
<p>“Volvo — they’re boxy but they’re good.”</p>
</blockquote>
<blockquote>
<p>“Metamucil — it makes you go to the toilet.”</p>
</blockquote>
<p>His agency rejects the ads and checks poor Dudley into a sanitarium thinking he’s taken leave of his senses. A mistake leads to the ads running in newspapers across the country anyway, at which point the unexpected happens. The ads work. <em>Really well</em>.</p>
<p>It’s tempting to write this off as nonsense movie hijinks, and it <em>mostly</em> is. But there’s a pearl of wisdom in there. As it happens, <strong>marketing and advertising that tells the truth can be incredibly powerful</strong>.</p>
<h2>Truth-based marketing in the wild</h2>
<p>Here are a few real-world examples of truth-based marketing:</p>
<p><a href="https://www.youtube.com/watch?v=a11wlngpuSY">This video</a> from Saddleback Leather is one of my favorite examples. All he’s doing here is describing in detail how to knock off one of their bags. But the message is crystal clear: they put a ton of time and attention into making a bag that lasts. If you’re tired of bags that fall apart, Saddleback is for you. So good.</p>
<p>Apple may be well know for their “reality distortion field,” but Steve Jobs provided a fantastic example of truth-based marketing when he introduced the original iPod:</p>
<blockquote>
<p>“The coolest thing about iPod is that your entire music library fits in your pocket. OK? You can take your whole music library with you, right in your pocket. Never before possible. So that’s iPod.”</p>
</blockquote>
<p>They could have marketed the iPod a hundred different ways. But the product was so groundbreaking, they knew that that the best thing they could do was get out of the way and state this simple fact. 10/10.</p>
<p>Finally, <a href="https://www.pinterest.com/pin/14003448828805789/">a classic from Fiat</a>. It would have been laughable to claim that a Fiat was just as good as a Ferrari, so they didn’t. Instead they call out the fact that a person with well established taste in cars (Mr. Ferrari himself!) chooses Fiat as his practical car of choice. Genius.</p>
<p>As you can see, you don’t have to sling baloney or shovel hooey to do marketing right.</p>
<h2>How to make truth-based marketing work.</h2>
<p>If you’d like to give truth-based marketing a try, you’re in luck. It’s remarkably straightforward! Here’s how it works:</p>
<ol>
<li>Find a unique, desirable outcome that your product or company delivers to customers.</li>
<li>Talk about that thing.</li>
</ol>
<p>People tend to not believe it’s this simple, so let’s run through some common questions:</p>
<p><em>“What if my product doesn’t solve any problems or have any real value?”</em></p>
<p>No marketing your way out of this one. Just ask Juicero.</p>
<p>Instead, you need to do the sounds-simple-but-is-incredibly-hard thing and make a better product or service. And while you do that, focus your marketing on a truth that’s appealing, but possibly not unique. Do you have great customer service? Talk about that.</p>
<p><em>“What if we don’t know why people are buying our product?”</em></p>
<p>This is a good place to be, but you’ve got some work to do. And plenty of customers to talk to. Just keep in mind that the reasons people tell you aren’t necessarily the <em>real</em> reasons they buy. Frameworks like <a href="http://jobstobedone.org/">Jobs to Be Done</a> can help you figure it out.</p>
<p>Any other questions? No? Good.</p>
<p>I heartily recommend giving truth-based marketing a try. Not only will your company reap the benefits, but you get to wake up in the morning energized by the fact that you’re not adding to the ever-increasing volume of bunk, hogwash, and hot air in the world. It feels nice!</p>
Death to process machines!2018-04-04T00:00:00Zhttps://aaadaaam.com/notes/death-to-process-machines/<p>You hear a ton of different advice on the right and wrong way to go about designing an app or website.</p>
<p><em>“You should be using Figma.”</em><br />
<em>“Design Systems or GTFO.”</em><br />
<em>“Real Designers design 100% in code.”</em><br />
<em>“Wireframes are a waste of time.”</em><br />
<em>“If you’re not making prototypes, you’re not doing it right.”</em><br />
<em>“You need to start on paper.”</em></p>
<p>You’d think there’s no agreement whatsoever about the right way to design, but there’s one point that’s largely free of controversy — <em>that your process should be linear</em>.</p>
<p>The classic linear approach looks something like this:
<strong>research → sketch → wireframe → static comps → prototype → code</strong></p>
<p>It’s kind of like those Rube Goldberg-esque manufacturing machines they use to make Doritos and Ding-Dongs. Drop an idea into the process machine, and after getting mashed and molded into shape as it winds through the steps a finished product pops out the other side! Predictable! Efficient!</p>
<p>Kind of.</p>
<p>Process machines work, but only when they <em>work</em>. They don’t adapt, and that makes them <em>fragile</em>. All it takes is one little <a href="https://en.wikipedia.org/wiki/Sabot_%28shoe%29">Sabot</a> to grind your process machine to a halt.</p>
<h2>Hank, a.k.a. “the Sabot”</h2>
<p>I’ve been watching <em>Finding Dory</em> with my kid lately, and part of the “making of” footage keeps jumping out at me.</p>
<p>In the movie, there’s this octopus named Hank. <em>Septopus</em>, technically. His character model was so onerous to work with, they lopped off a tentacle to make animating him <em>doable</em>. Still, with 4,000 separate controls he was incredibly challenging to work with.</p>
<p>At this point in the process, they’re well past sketches and renderings and animatics — those lower fidelity stages that help you vet a bunch of ideas quickly and cheaply. They already <strong>Got Real</strong> too. The character rig was built, technical details worked out, fundamental questions answered.</p>
<p>They’re in the final animation stage — 3D models in 3D environments. They could have soldiered on at the expense of the production schedule and budget. Instead, they did something <em>really</em> interesting — they went back to sketching.</p>
<p>By sketching out the complex movement of Hank’s tentacles on paper, they could nail down the perfect, fluid animation they were looking for in a <em>fraction</em> of the time. Once they were happy with the sequence, they’d animate in 3D to match. <strong>They got a better product in less time because they chose to value <em>process principles</em> instead of a <em>process prescription</em>.</strong></p>
<h2>The cure for a prescriptive process</h2>
<p>The Finding Dory team made a better product faster by making decisions that prioritized <em>speed and quality</em> instead of sticking to a fixed process.</p>
<p>You might choose other things to value, but if you’re working in a commercial setting, focusing on the sweet spot between speed and quality should be at the top of your list. Turning around great work quickly is <em>kind of a big deal</em> for professional designers and artists after all.</p>
<p>If you value speed, start a project by figuring out what the biggest, most fundamental questions are. In <a href="https://basecamp.com/books/getting-real">Getting Real</a>, this is called “epicenter design”:</p>
<blockquote>
<p><strong>Start from the core of the page and build outward</strong></p>
<p>Epicenter design focuses on the true essence of the page — the epicenter — and then builds outward. This means that, at the start, you ignore the extremities: the navigation/tabs, footer, colors, sidebar, logo, etc. Instead, you start at the epicenter and design the most important piece of content first.</p>
<p>Whatever the page absolutely can’t live without is the epicenter. For example, if you’re designing a page that displays a blog post, the blog post itself is the epicenter. Not the categories in the sidebar, not the header at the top, not the comment form at the bottom, but the actual blog post unit. Without the blog post unit, the page isn’t a blog post.</p>
<p>Only when that unit is complete would you begin to think about the second most critical element on the page. Then after the second most critical element, you’d move on to the third, and so on. That’s epicenter design.</p>
<p>Epicenter design eschews the tradtional “let’s build the frame then drop the content in” model. In that process, the page shape is built, then the nav is included, then the marketing “stuff” is inserted, and then, finally, the core functionality, the actual purpose of the page, is poured in to whatever space remains. It’s a backwards process that takes what should be the top priority and saves it for the end.</p>
</blockquote>
<p>Here’s an example of why this is <em>crucial</em>. I was working on a little side project iOS app that used a unique, possibly unworkable audio interface. If I <em>didn’t</em> value speed, I could have spent <em>countless</em> hours designing the myriad details that rested on the foundation of this one oddball idea. Design comes before code in the classic linear process, after all.</p>
<p>Instead, I started in code to figure out whether or not this idea was viable. It wasn’t! So I adjusted my plans, and saved myself an enormous amount of time and energy.</p>
<p>Once you know the questions that need answers first, ask yourself:
<strong><em>“Which medium gives me the clearest answer to my questions in the least amount of time?”
A venn diagram of ‘clearest answers’ and ‘fastest to make’.</em></strong></p>
<p>In the case of my side project, the answer was code. For a page on <a href="https://basecamp.com/">Basecamp.com</a>, the answer is often text or a rough sketch. For you, it might be something else entirely.</p>
<h2>Knowing when to change gears</h2>
<p>That gives you a place to start, but how do you know when it’s time to switch to a different medium? <strong>When you hit resistance.</strong></p>
<p>Think about driving a car. You’re cruising down the highway — engine purring away like a contented kitten. But then you start driving up a hill. The gear you’re in was great for cruising, but not for hill climbing. In order to keep up your speed, you shift into a new gear.</p>
<p>Same thing here. But unlike cars, there’s no rock solid indicator that you’ve hit too much resistance in your medium of choice. Luckily, most designers and artists have a solid handle on when you need to switch to a medium that offers more fidelity. This is the part that lines up with the classic low fidelity → high fidelity linear process after all. <em>You know you’re ready to move on from sketching when sketching stops giving you useful insight.</em></p>
<p>Once you’ve hit this point, figure out the next most important set of questions and ask yourself again: “which medium gives me the clearest answer to my questions in the least amount of time?”</p>
<p>The second case — shifting back to a lower fidelity — is tougher. Both because people are less practiced at it, and also because it’s <em>tricky</em>. Take working in code. You’re working at 100% fidelity, so there’s no limit to the medium’s ability to answer questions. But there <em>is</em> a limit to its ability to answer questions <em>fast</em>.</p>
<p>When you feel yourself not pursuing paths because it feels like too much work, that’s a really good sign that you need to back out. When things feel like they just aren’t clicking like they should, it’s time to reassess. Be mindful, and you’ll start to develop a feel for it.</p>
<h2>Using a medium to your advantage</h2>
<p>There’s a third case for switching to — or sticking with — a medium. This one doesn’t care about resistance, it only cares about a fundamental truth; <strong>process influences outcome</strong>. Just like drawing something with a pencil is going to look different than drawing it with a marker, designing in browser is going to produce an different outcome than designing in Sketch.</p>
<p>The more you understand how a medium affects your work — the kind of tool marks it leaves — the more you can use it to your advantage. Want your design to be expressive? Probably better to work with a visual tool like Sketch, Illustrator, or even <em>gasp</em> Photoshop. Want a minimal, lightweight design? Stick to designing in code.</p>
<hr />
<p>Hopefully this gives you some insight into how you can use principles to guide your process on case-by-case without feeling like you’re constantly reinventing the wheel.</p>
<p>Think about your process and the kind of work that you do. Define the principles that are important to you, focus on the big stuff first, and keep questioning if the medium you’re working in is the right one for the moment.</p>
Generalists, specifically.2020-11-05T00:00:00Zhttps://aaadaaam.com/notes/generalists-specifically/<p>A generalist designer, axiomatically, is someone who doesn’t specialize in any particular area of design. They’re not UX designers, UI designers, content strategists, illustrators, product designers, copywriters, graphic designers, front-end developers, or animators, even though their work may touch every single one of those roles. The proverbial “jack of all trades, master of none”.</p>
<p>That’s part of the reason why the industry mantra for decades has been “SPECIALIZE!, SPECIALIZE!, SPECIALIZE! THE MORE SPECIALIZED THE BETTER! YOUR CAREER IS DOOMED OTHERWISE!”</p>
<p>I’ve spent <em>years</em> hand-wringing over this. Worrying that I was spreading myself too thin. Worrying that my love for all forms of designing and building and making and creating would make me irrelevant in an industry that’s charging headlong towards specialization.</p>
<p>But then I realized something; the industry… is wrong.</p>
<p>Well, two things actually; the industry is wrong, and generalists are, paradoxically, a <em>kind</em> of specialist. It just so happens that the specialty isn’t <strong>categorical</strong>, it’s <strong>situational</strong>. Generalists <em>excel</em> at helping smaller or newer companies leverage design faster, cheaper, and easier than an equivalent team of specialists. How?</p>
<ol>
<li>
<p>Generalists live and breathe the Pareto principle (80% of the benefit comes from 20% of the effort). By focusing on 80-90% good, generalists can deliver work that that moves business goals forward in a tiny fraction of the time that it might otherwise take. Sometimes you actually need to get to 100% and hey, great time to call in a specialist. That last 20% is quite literally their expertise.</p>
</li>
<li>
<p>Generalists cut through organizational bloat like <em>butter</em>. Teams of specialists have to coordinate, confer, write specs, and/or have meetings at each handoff. That takes time. A <em>lot</em> of time. Oh, and let’s not forget the extra layers of people management you need to wrangle a larger staff. Generalists eliminate <em>all of that</em> because they’re doing it all themselves. It’s an order of magnitude more efficient.</p>
</li>
<li>
<p>The work of a generalist is seamless, by default. The product of one mind, one vision. With a team of specialists, everyone is focused on their own domain and it’s easy for the end product to feel like what it is; a bunch of disjointed parts glued together. High-functioning teams are great at avoiding this, but it takes work to really pull it off (and usually another specialist).</p>
</li>
<li>
<p>Generalists view work from a multitude of perspectives, where specialists tend to fall victim to Maslow’s hammer (if all you have is a hammer, everything looks like a nail). A specialist brand designer probably isn’t considering how their design explorations might or might not translate to efficient web assets, for instance. Generalists see the entire forest, and the work is better for it.</p>
</li>
</ol>
<p>That’s all to say; most companies that aren’t Google or Apple-scale would be far better served by hiring generalists instead of specialists. If you’re starting a company, generalists are your best friend. If you’re trying to get more done without working longer hours, generalists are your jam. If you’re trying to stay lean, generalists are what’s cookin’.</p>
<p>The best part? Working as a generalist is <em>satisfying</em>. Variety is the spice of life, and working as a generalist is very, very spicy. The idea of a job that’s nothing but drawing boxes in Figma interspersed with meetings about the boxes you draw in Figma <em>fills me with dread</em>. Humans are hardwired for novelty. We wither when we emulate robots. Working as a generalist is the more <em>human</em> way to work.</p>
<p>Viva generalists.</p>
<p>P.S. I’m not the only one who’s realized all of this:</p>
<ul>
<li><a href="https://davidepstein.com/the-range/">Range, Why Generalists Triumph in a Specialized World</a></li>
<li><a href="https://www.campaignlive.com/article/generalists-beat-specialists/1706618">Generalists Beat Specialists</a></li>
<li><a href="https://www.designerfund.com/blog/a-founders-guide-to-hiring-your-first-designer/">A Founders’ Guide to Hiring your First Designer</a></li>
</ul>
On brand2022-03-09T00:00:00Zhttps://aaadaaam.com/notes/on-brand/<p>Across the clients and companies I’ve worked with, more often than not I’ve had to sell the value of hard to measure stuff like branding and designing for emotion. Chances are good I’ll have to keep selling folks on this subject, so I thought it’d be handy to have a link I can send folks that outlines my thoughts. This link, specifically.</p>
<p>Let’s start at the very beginning and review the purpose branding plays in the context of a tech startup. Does it even matter? Can’t you just use off-the-shelf design systems or copy what everyone else is doing or do nothing at all? The short answer; <em>sometimes, but not all the time, and usually not forever</em>.</p>
<blockquote>
<p>“The only problem with Microsoft is they just have no taste”<br />
— Steve Jobs</p>
</blockquote>
<p>You can look at early Microsoft or Google for examples of when brand identity (and more broadly “good design”) isn’t a vital strategic pillar. When you have a product that is category defining or wholly unique in the value it delivers, you can build an incredibly successful business without investing in branding or experience. At the time, the hardware-independent model offered by the Windows ecosystem was impossible to beat. It didn’t matter that Microsoft had no taste. They eventually invested in branding of course. But what made them start?</p>
<p>As novel technologies become commodity or you directly enter a competitive market, branding and experience design become increasingly valuable strategic levers. Slack is a classic example of a company that saw massive success in large part due to branding and experience design. Slack wasn’t a notable standout feature-wise compared to contemporaries like HipChat or Campfire when it entered the market, but it had a cool name, a fresh brand identity, a <em>beautifully</em> designed app, and an amazing onboarding experience. My team was using Hipchat when it launched, and we couldn’t switch fast enough. It felt like a breath of fresh air.</p>
<p>When you get branding right, it’s a multiplier on everything you do. It’s the salt that intensifies flavor, a wind at your back. Hiring is easier, customers are more satisfied, prospects more enthusiastic.</p>
<p>As to <em>why</em> that’s true, you have to remember that humans make decisions through a combination of conscious logical reasons and unconscious emotional reasons. Features, benefits, and value appeal to the logical self, whereas branding and “experience” focuses on the emotional self. As I described with Microsoft and Slack, you can compete on one <em>or</em> the other, but when you get both right, <em>magic happens</em>. <a href="https://aaadaaam.com/notes/vinyl/">For a deeper dive on this phenomenon, read on</a>.</p>
<h2>What constitutes a “good” brand identity?</h2>
<p>Good brand identities are inherently contextual. Good for one company could be bad for another. For instance, the identity for Monster works for an energy drink, but it’d be <em>disastrous</em> for a mortuary. There are a few common “good” characteristics that apply to most businesses though:</p>
<ul>
<li><strong>A good brand identity inspires confidence.</strong> At minimum, a brand identity must immediately identify you as legitimate. A good brand identity takes that several steps further, instilling confidence that you can solve the problem potential customers have.</li>
<li><strong>A good brand identity stands out.</strong> In 2022, it’s basically impossible to wholly unique, but it’s vitally important to not be blandly forgettable. If a customer is competitive shopping, do you want to be that one Zebra standing in the middle of a pack of identical zebras? Or do you want to be that one Zebra with pink stripes? <em>Always</em> the latter. This is where most tech companies go wrong. They think they have to “look like a tech company” and don the homogeneous “tech look”.</li>
<li><strong>A good brand identity shines in the primary medium.</strong> For a tech company, that usually means websites and apps.</li>
<li><strong>A good brand identity is consistent, but not monotonous.</strong> Evolutionarily speaking, humans are wired to respond to novelty. If you spot that tiger in the bushes, you live. If not, tiger snack. In music terms, you want a song with rise and fall, not a monotonous single tone.</li>
<li><strong>A good brand identity should make you feel… something.</strong> Ideally a positive something.</li>
</ul>
<p>A closing thought; consider that on any typical day there is no functional difference between a $20,000 Kia Rio and a $100,000 luxury car. They both get you from A to B safely. They both crawl through traffic at the same rate. What is it that people are paying 5x more for?</p>
Fundamentals2022-11-08T00:00:00Zhttps://aaadaaam.com/notes/fundamentals/<p>The pendulum swinging back from “recreate everything in [insert SPA framework]” to “multi-page apps are pretty good, actually” is a timely reminder that focusing your energy on mastering the fundamentals is usually a smart bet.</p>
<p>Early in my career I was more of a Flash person. In terms of creative expression, it felt like a breath of fresh air compared to compared to sliced up images and table hell. But then CSS Zen Garden came along and opened my eyes to a radically different way. Not just a different way of <em>building</em> websites via semantic markup and external stylesheets, but a different way of <em>thinking</em> about them. A unique, living medium with its own inherent character and beauty. I was hooked, and left Flash in the rearview mirror.</p>
<p>Roughly 20 years later, I can say with confidence that focusing on the fundamentals — markup, CSS, and vanilla javascript — was the right choice. All that Flash-specific knowledge that I spent countless hours accumulating? Useless. Meanwhile, my web standards knowledge has stayed consistently relevant, useful, and in-demand. <strong>Tools change, fundamentals don’t.</strong></p>
Extending 11ty’s page variable2022-11-20T00:00:00Zhttps://aaadaaam.com/notes/extending-11ty-page-variable/<p>I’ve had my eye on Astro, so I thought I’d take it for a spin by re-building this here website. I stuck with 11ty in the end for a variety of reasons, but the experience inspired me to see if I could replicate a few Astro niceties in 11ty. Turns out yes, thanks to 11ty’s page variable.</p>
<h2>single file templates, kind of</h2>
<p>11ty’s new <a href="https://github.com/11ty/webc">WebC format</a> uses single file templates out of the box, but Liquid and Nunjucks require a little more creativity. My use case for single file templates is more about having an easy place to drop little bits of page-specific CSS vs. a full-blown asset pipeline. Totally doable by extending 11ty’s <code>page</code> variable.</p>
<p>Step one is creating a paired shortcode to the 11ty config. I like to keep my filters and shortcodes in separate files to keep things tidy:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> postcss <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'postcss'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> nesting <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'postcss-nesting'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> csso <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'postcss-csso'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token function">style</span><span class="token punctuation">(</span><span class="token parameter">content</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">postcss</span><span class="token punctuation">(</span><span class="token punctuation">[</span>
nesting<span class="token punctuation">,</span>
csso<span class="token punctuation">,</span>
<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">process</span><span class="token punctuation">(</span>content<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">from</span><span class="token operator">:</span> <span class="token string">'undefined'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">result</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>page<span class="token punctuation">.</span>style <span class="token operator">=</span> result<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token string">''</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>I’m using PostCSS for nesting syntax and minification, but you can skip that and return plain CSS if you want. Now we need to tell 11ty about our new shortcode:</p>
<pre class="language-js"><code class="language-js">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> style <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./_source/_utilities/style.js"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addPairedShortcode</span><span class="token punctuation">(</span><span class="token string">'style'</span><span class="token punctuation">,</span> style<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>Finally, add little conditional code in your layout file’s <code><head></code> element:</p>
<pre class="language-liquid"><code class="language-liquid"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> <span class="token object">page</span><span class="token punctuation">.</span><span class="token keyword">style</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css">
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">page</span><span class="token punctuation">.</span><span class="token keyword">style</span> <span class="token delimiter punctuation">}}</span></span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span></code></pre>
<p>That’s it for plumbing. Here’s how you use it in a Liquid template:</p>
<pre class="language-liquid"><code class="language-liquid"><span class="token comment"><!-- front matter and template content --></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">style</span> <span class="token delimiter punctuation">%}</span></span>
.foo {
position: absolute;
top: 0;
}
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endstyle</span> <span class="token delimiter punctuation">%}</span></span></code></pre>
<p>Any CSS you write in the shortcode is added to a style tag in the head of the page. It’s a nice and simple way to add page-specific styles without bloating you main CSS bundle.</p>
<h2>multiple slots</h2>
<p>Another nice thing about Astro is the concept of multiple slots. Out of the box, 11ty only supports one <code>content</code> slot, which can made more complex layout needs hard to pull off without making markup concessions.</p>
<p>For instance, on this site I needed a way to include some markup outside of the <code>main</code> element where I’m rendering content. What to do? Once again, extending the 11ty supplied <code>page</code> variable with a paired shortcode is the answer:</p>
<pre class="language-js"><code class="language-js">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token function">setVar</span><span class="token punctuation">(</span><span class="token parameter">content<span class="token punctuation">,</span> name</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>page<span class="token punctuation">[</span>name<span class="token punctuation">]</span> <span class="token operator">=</span> content<span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token string">''</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>Unlike our style shortcode, this variation allows for setting a variable name in order to support additional slots. Like with the style shortcode, we need to hook up our slot by adding a conditional where it’s going to be used:</p>
<pre class="language-html"><code class="language-html">{% if page.pattern %}
{{ page.pattern }}
{% endif %}</code></pre>
<p>I’m using <code>pattern</code> as the name since that’s what I’m using it for, but you can use whatever names make sense to you. Here’s how you use it in a Liquid template:</p>
<pre class="language-liquid"><code class="language-liquid"><span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> setVar <span class="token string">'pattern'</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>random-pattern</span> <span class="token attr-name">quantity</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>10<span class="token punctuation">"</span></span> <span class="token attr-name">jitter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>150<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>random-pattern</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> endsetVar <span class="token delimiter punctuation">%}</span></span></code></pre>
<p>Worth noting here that Liquid.js does have a concept of <a href="https://liquidjs.com/tags/layout.html#Multiple-Blocks">multiple blocks</a> but I couldn’t get it to work properly in my testing. Plus, this technique has the benefit of not wholesale changing how you do layout in your project.</p>
Revisiting PJAX2022-11-27T00:00:00Zhttps://aaadaaam.com/notes/revisiting-pjax/<p>Personal sites are great venues for exploring those half-baked and high-contrast ideas that are illuminating, but impractical for commercial work. No funnels, no customers; just a blank canvas and the freedom to explore it.</p>
<p>Lately, the tension between functional attributes (performance, accessibility, DX) and experiential attributes has been the focus of my own exploration. On one side of the spectrum, you have <a href="https://motherfuckingwebsite.com/">bare HTML</a>. Pure functionality (debatable!), a big zero experience-wise. Like a car stripped down to bare frame, this is “too far”. On the other side, you have pure js apps and sites. Rich, expressive, and capable, but complex and comparatively slow. Also “too far”. If these are both “too far”, what does “just right” look like? How far <em>can</em> you push on one without sacrificing the other? Is it even really a tradeoff? Can you have it all?</p>
<p>Most of my energy has been focused on pushing towards performance and simplicity, but this time around I thought I’d switch things up and focus on a marquee experiential features of SPA frameworks; app-like, animated page transitions. Frameworks and libraries are out. Too big, too complex, or both. So what’s left? Do-it-yourself PJAX.</p>
<p>PJAX as a technique isn’t remotely new, but as it turns out, modern javascript offerings like custom elements make it <strong>a heck of a lot easier to implement without help from a library.</strong> For example, one of the problems you have to solve with PJAX is that page events only fire on the first page visit, which causes all kinds of problems for scripts you might be running on various window events. Custom elements sidestep the problem completely thanks to baked-in lifecycle events like <code>connectedCallback</code>. They Just Work with the PJAX pattern, no extra code required.</p>
<p>Custom elements are a biggie, but the fetch API and host of other improvements all contribute to then end result of <em>a lot less code</em>. Here’s the final PJAX custom element I cooked up, that’s running on this website right now:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">PageTransition</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
<span class="token keyword">async</span> <span class="token function">getPage</span><span class="token punctuation">(</span><span class="token parameter">url</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> responseText <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> parser <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">DOMParser</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> doc <span class="token operator">=</span> parser<span class="token punctuation">.</span><span class="token function">parseFromString</span><span class="token punctuation">(</span>responseText<span class="token punctuation">,</span> <span class="token string">'text/html'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span>history<span class="token punctuation">.</span><span class="token function">pushState</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">,</span> url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">transitionPage</span><span class="token punctuation">(</span>doc<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>href <span class="token operator">=</span> url<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token function">transitionPage</span><span class="token punctuation">(</span><span class="token parameter">doc</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> head <span class="token punctuation">}</span> <span class="token operator">=</span> document<span class="token punctuation">;</span>
<span class="token keyword">const</span> newElements <span class="token operator">=</span> doc<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'head > [data-swap]'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> oldElements <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'head > [data-swap]'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> newContent <span class="token operator">=</span> doc<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'page-transition'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerHTML<span class="token punctuation">;</span>
<span class="token keyword">const</span> containerStyles <span class="token operator">=</span> <span class="token function">getComputedStyle</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> delay <span class="token operator">=</span> <span class="token function">parseFloat</span><span class="token punctuation">(</span>containerStyles<span class="token punctuation">.</span>transitionDuration<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// transition out</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'transitioning'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token comment">// replace page elements</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> newContent<span class="token punctuation">;</span>
oldElements<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> element<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
newElements<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> head<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>element<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// transition in</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">transitionable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token function">scrollTo</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token string">'transitioning'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> delay <span class="token operator">*</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">transitionable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// only act on relative urls</span>
<span class="token keyword">const</span> links <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'a[href^="/"]'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
links<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">link</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>link<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'listener'</span><span class="token punctuation">)</span> <span class="token operator">!==</span> <span class="token string">'true'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
link<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'listener'</span><span class="token punctuation">,</span> <span class="token string">'true'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
link<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>e<span class="token punctuation">.</span>metaKey<span class="token punctuation">)</span> <span class="token punctuation">{</span>
e<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getPage</span><span class="token punctuation">(</span>link<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'href'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">transitionable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// reload the page when history controls are used</span>
window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'popstate'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
window<span class="token punctuation">.</span>location<span class="token punctuation">.</span><span class="token function">reload</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
window<span class="token punctuation">.</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'page-transition'</span><span class="token punctuation">,</span> PageTransition<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>56 lines of straightforward javascript that covers the basics and then some. It handles swapping all the <code><head></code> content that needs swapping and the exit portion of page transitions. The intro portion is handled by a separate, pre-existing custom element that handles scroll triggered animations of all types. This approach allows for more flexibility, and lets me skip handling back/forward controls without losing the entire transition.</p>
<p>Definitely a worthwhile experiment, and one that I’ll keep running for a while, but my big takeaway is that it’ll be a good day if and when the <a href="https://developer.chrome.com/docs/web-platform/view-transitions/">View Transition API</a> makes all of this unnecessary.</p>
Complexity creeps2022-12-07T00:00:00Zhttps://aaadaaam.com/notes/2022-12-7-complexity-creeps/<p>The thing about complexity is that the cost rarely seems obvious upfront. It usually starts as a reasonable desire, with a reasonable set of initial tradeoffs that make you think the benefit of taking on more complexity is worth the cost.</p>
<p>But then complexity does what complexity does, and creates <em>entirely new problems</em> you didn’t anticipate. So what do you do? Add <em>even more complexity</em> to solve that problem. And so on and so forth, each individual decision seeming perfectly reasonable and uncontroversial.</p>
<p><a href="https://www.youtube.com/watch?v=AbSehcT19u0">Hal’s attempt to change a lightbulb</a> perfectly encapsulates how complexity creeps in. It happens so slowly and methodically that there’s no “before” anymore. However long things take, however much friction or maintenance or education something requires, that’s just <em>normal</em>. Meanwhile, you’re taking twice as long to get half as much done.</p>
<p>It’s so pernicious that the only sustainable solution is vigilance. Complexity is sus. Complexity is an iceberg. Complexity is a monkey’s paw.</p>
Consistent with what?2022-12-16T00:00:00Zhttps://aaadaaam.com/notes/consistent-with-what/<p>“Consistency” is a word you hear <em>a lot</em> in tech. Designers talk about it (do not play the “drink when you hear this word” game with this word at a design systems conference), developers talk about it, everyone! But when it comes to consistency, it’s important to recognize that <strong>consistency comes in different flavors</strong>.</p>
<p>Case in point; I was working on a piece of UI for a new feature, and after iterating on a few different expressions in both light and dark mode, I couldn’t reconcile the fact that the card looked “right” with a subtle drop shadow in light mode, but looked “right” with a subtle edge highlight in dark mode. My logic brain was screaming. “Inconsistent!” “Bad!”</p>
<p>But then it hit me; my faithful and trusty gut brain was focused on the fact that this inconsistent implementation is actually <em>entirely consistent with the laws of physics</em>, and how light behaves in the real world. If you shine a light down on a glossy white object that’s sitting on a white surface, you’re going to see shadow. Highlights are there, but they’re blown out because everything is white on white. If you use a black object on black surface, the shadow is very difficult to see but now the highlights stand out. Inconsistent from an implementation perspective, perfectly consistent from an “adheres to the laws of nature” perspective.</p>
<p>Another case where this is true is “mathematical centering” vs. “optical centering”. Logical-brain <em>loves</em> mathematical centering, and derides things like “magic numbers”. But take a look at this humble play button comparison, with the triangle mathematically centered on the left, and optically centered on the right:</p>
<p><svg style="max-width: 600px;" viewBox="0 0 600 220" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="m108.19 2c58.55 0 106.19 47.64 106.19 106.19s-47.64 106.19-106.19 106.19-106.19-47.64-106.19-106.19 47.64-106.19 106.19-106.19m0-2c-59.75 0-108.19 48.44-108.19 108.19s48.44 108.19 108.19 108.19 108.19-48.44 108.19-108.19-48.44-108.19-108.19-108.19z"></path><path d="m55.26 48.23 103.86 59.96-103.86 59.96zm-2-3.46v126.85l109.86-63.42z"></path><path d="m413.83 2c58.55 0 106.19 47.64 106.19 106.19s-47.64 106.19-106.19 106.19-106.19-47.64-106.19-106.19 47.64-106.19 106.19-106.19m0-2c-59.75 0-108.19 48.44-108.19 108.19s48.44 108.19 108.19 108.19 108.19-48.44 108.19-108.19-48.44-108.19-108.19-108.19z"></path><path d="m376.67 48.12 103.86 59.96-103.86 59.96zm-2-3.46v126.85l109.86-63.42-109.86-63.42z"></path></g></svg></p>
<p>You might be thinking to yourself right now, “there is no way that the option on the left is centered”, and you would be wrong. There is exactly as much space on the left side of the triangle as the right. But there’s also no question that the “perceptually consistent” version on the right looks “correct” and the “mathematically consistent” version doesn’t. Consistency is all well and good. Just make sure you’re picking the right kind.</p>
Step into the light (DOM)2022-12-19T00:00:00Zhttps://aaadaaam.com/notes/step-into-the-light-dom/<p>For a long time I didn’t know what to do with web components. There wasn’t a lot of writing around them outside of the spec, all the stuff around shadow DOM and templates made it feel inaccessible, and I just didn’t see a compelling reason to bother with any of it (<em>something something PWAs</em>).</p>
<p>That changed the day <a href="https://mastodon.social/@javan">@javan</a> clued me in to the fact that it is totally OK, and often ideal, to just use the custom element part of the spec. No shadow DOM, no templates, just the regular old DOM, which we now get to call the much cooler sounding “light DOM”.</p>
<p>Why would you want to do such a thing?</p>
<ol>
<li><strong>Custom elements have the very cool ability manage themselves via <code>connectedCallback</code> and <code>disconnectedCallback</code>.</strong> These are the unsung heroes of the spec; the things that make web components the can-do workhorses that they are. The second you step out of a strictly MPA scenario, these are very, very handy.</li>
<li><strong>Progressive enhancement is straightforward.</strong> Any markup you add inside your custom element is just… there, javascript or no. Wrap standard interactive elements like <code>details</code>, <code>textarea</code>, <code>input</code>, etc in custom elements that enhances them, and you’ve got yourself a solid progressive enhancement story.</li>
<li><strong>Custom elements enforce good javascript patterns.</strong> This one took me a while to spot, but it’s the thing that’s made custom elements the default way I write vanilla javascript these days (take a peek under the hood for examples). Vanilla javascript always felt spaghetti-ish in my clumsy hands, but there’s only one way to write custom elements. There’s no making a custom element without writing it as a class. For me, it’s meant an overall improvement in code quality.</li>
</ol>
<p>Here’s an example of a progressively enhanced <code>details</code> element called <code>small-details</code> that makes a details element expand into what looks like an unordered list with header once the viewport is wide enough:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/* ----------------------------------------------------------------------------
details element that defaults to closed @small and open @medium+
---------------------------------------------------------------------------- */</span>
<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">SmallDetails</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
<span class="token function">listen</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>mediaQuery<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'change'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>matches<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">update</span><span class="token punctuation">(</span><span class="token parameter">matches</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>matches<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>details<span class="token punctuation">.</span>open <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>summary<span class="token punctuation">.</span>tabIndex <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>details<span class="token punctuation">.</span>open <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>summary<span class="token punctuation">.</span>tabIndex <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>details <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'details'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>summary <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'summary'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>mediaQuery <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">'(max-width: 49.999em)'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>mediaQuery<span class="token punctuation">.</span>matches<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">listen</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
window<span class="token punctuation">.</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'small-details'</span><span class="token punctuation">,</span> SmallDetails<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And here’s how it gets used:</p>
<pre><code><small-details>
<details>
<summary>Only interactive @small</summary>
<ul>
<li><a href="/link-to-page/">a page</a></li>
<li><a href="/another-page/">another page</a></li>
</ul>
</details>
</small-details>
</code></pre>
<p>Nothing fancy or complicated. Just clear, simple, and durable code, and a great progressive enhancement story. Custom elements are <em>good</em>.</p>
“Craft at scale” is a white whale2023-03-29T00:00:00Zhttps://aaadaaam.com/notes/craft-at-scale/<p>Designers and product-folk like to think of ourselves as craftspeople. We <em>care</em> about making things <em>great</em>. Is it possible to do it at scale? Shot and chaser:</p>
<ul>
<li><a href="https://gk3fyi.substack.com/p/the-cost-of-craft">The Cost of Craft</a> - Longtime Meta designer George Kedenburg III did us all a favor and exhaustively documented the challenges of delivering “great” at scale. It’s well worth your time.</li>
<li><a href="https://www.businessinsider.com/tech-companies-ruining-apps-websites-internet-worse-google-facebook-amazon-2023-3">Big Tech’s big downgrade</a> - in this Business Insider piece, Ed Zitron details how and why the largest tech companies — like Meta — are actively making their products worse for users.</li>
</ul>
<p>George nails the problems, and in credit to his fortitude and optimism, leaves you with the idea that craft at scale is <em>possible</em>. I’m less optimistic. If you look at the available evidence, <strong>“craft at scale” is mutually exclusive with the kind of rapid and unending growth that’s the baseline expectation for traditional startups and public tech companies.</strong></p>
<p>We’d say that’s <em>obvious</em> in other industries. Budweiser isn’t craft beer. IKEA isn’t heirloom-quality furniture. But we tend to treat software as immune to the typical relationship between quality and quantity. It’s software! It can infinitely replicate!</p>
<p>There’s some truth to that idea. Moving from making 100 sandwiches a day to 100,000 requires a fundamental shift in tools and process that inevitably leads to quality loss. Meanwhile, an indie dev can ship the same software to 100 or 100,000 people without doing anything different. As George points out, the tension between craft and scale in tech has much more to do with the <em>size</em> of the product you’re building and the <em>speed</em> at which you’re trying to build it.</p>
<p>Funded startups and public tech companies need to keep the high-growth engine running, which inevitably leads progressively larger and more complex product offerings that attempt to attract more and more users. No single team can deliver on that, which leads directly to disjointed and/or competing teams, and a steady erosion of vision that’s backfilled by process. This is the inflection point where you’re now “manufacturing” software.</p>
<p>Growing the surface area of the product only takes you so far though. You eventually hit some kind of saturation point, and what follows is the kind of shitification Ed outlines. The focus shifts from quality to “value extraction”, which leads to higher prices, worse customer service, manipulative or deceptive products, monopoly lock-in, etc.</p>
<p>If you’re a designer at a large tech company, you cannot and will not overcome this. If you look at Apple — arguably one of the few companies that’s been able deliver craft at scale for any amount of time — they did so because Steve Jobs was a true believer who did more that espouse belief; he hired, fired, inspired and cajoled his way to an org built around delivering on it. It <em>has</em> to come from the very top for craft at scale to be a remote possibility.</p>
<p>Jesper Kouthoofd of Teenage Engineering recognizes that <a href="https://scandinavianmind.com/feature/human-touch-interview-jesper-kouthoofd-teenage-engineering">craft and rapid, unending growth are fundamentally at odds</a>:</p>
<blockquote>
<p>We only want to make great products and when you don’t focus only on making money and have reached a certain level, everything becomes about quality. Right now, there is a certain cultural fascination with fast growth, IPOs and so on, but I want to go slow, really slow and think long-term. It takes time to do good things. You see, this cultural phenomenon of speed and growth at all costs is displayed in every startup, they all look the same, it’s like fast food: it looks good, its taste it’s consistent but then you feel horrible afterwards.</p>
</blockquote>
<p>If pursuit of craft is what drives you, you need to find smaller, slower waters.</p>
<hr />
<div class="text --size-s">
<p>My framework for defining if something is “well crafted” or “a product of craft”:</p>
<ol>
<li><strong>Well made.</strong> Craft is the product of high quality ingredients, approach, and process.</li>
<li><strong>Well considered.</strong> When something is imbued with craft, it’s obvious to the end user that every detail was thought through. The care and attention shines through.</li>
<li><strong>Beneficial.</strong> Well crafted software/tools/food/clothing/buildings give more than they take. They solve problems, meet needs, grant abilities, delight senses and satisfy emotions. Time and energy invested is rewarded in multiples.</li>
<li><strong>Opinionated.</strong> Craft has perspective. It thinks things should be <em>just so</em>. It’s heavily influenced by a vision that exists outside the tactical pursuit of mass appeal.</li>
</ol>
</div>Building vs. optimizing2023-05-19T00:00:00Zhttps://aaadaaam.com/notes/building-vs-optimizing/<p>Software — and software companies — break down into two distinct modes:</p>
<ol>
<li><strong>Build mode</strong> - you’re in blue sky territory, creating something where nothing existing before, trying to find product market fit.</li>
<li><strong>Optimize mode</strong> - you have an existing product, and you’re iterating on it to grow your user base and increase revenue.</li>
</ol>
<p>It’s an <em>incredibly important</em> distinction to recognize, for a few reasons. One is professional. In my experience, people strongly prefer working in one mode or the other. If you know which and pick jobs accordingly, your career will be better for it. Me? Build mode. The times in my career when I’ve been in optimization roles correspond 1:1 with professional low points.</p>
<p>The other reason is that <strong>techniques and practices aren’t one-size-fits-all</strong>. The trouble is, discourse is dominated by people working at larger companies that operate in the optimization mode. That’s led to the general idea that the best practices for optimization mode are the <em>One True Way to Do Things</em>.</p>
<p>Why is that a problem? Bringing optimization mode approaches to build mode is like bringing a knife to a gun fight; it puts you at a fundamental disadvantage, and the the likely outcomes range from <em>bad</em> to <em>worse</em>.</p>
<p>One example is the bit of conventional wisdom that dictates that you should have a measurable KPI for every project, every piece of design, every feature that you ship. Side-stepping the fact that <a href="https://en.wikipedia.org/wiki/Blind_men_and_an_elephant">you can’t measure most design outcomes holistically</a>, this idea makes sense when you’re optimizing, since it mostly ensures that you don’t move backwards. It trades speed for reduced risk; pretty good calculus for mature companies.</p>
<p>The same approach applied to build mode leads to rudderless products, feature factories, bad decisions made on bad data, and a major blow to your ability to move <em>quickly</em>. Companies at Facebook can run A/B tests and have statistically relevant results the next day. At an early startup that day turns into <em>weeks and months</em>. That’s a kiss of death in a competitive space. <strong>In build mode, there’s simply no substitute for taste, vision, deep knowledge of the problem space, and direct user feedback.</strong></p>
<p>Design system are another example. A strong focus on creating and maintaining a comprehensive design system makes <em>perfect sense</em> for a large, mature company. Design system are <em>inherently a scale concern</em> after all. But if your first move as a founding designer at a new startup is to establish a comprehensive design system, <em>you’ve chosen unwisely</em>. Priority one at an early startup is finding product-market fit. If you don’t have that, nothing else matters.</p>
<p>Pragmatically, you also don’t know what you don’t know. Building first lets you see where patterns form, then extract those patterns into a focused, just-enough design system. The end result is business that finds its footing faster, and an app that doesn’t feel like a bunch of LEGO parts stuck together.</p>
<p>Staying mindful of the mode you like and the mode you’re in makes work better.</p>
View transitions + Quicklink2023-05-28T00:00:00Zhttps://aaadaaam.com/notes/view-transitions-quicklink/<p>A great thing about the multi-page view transitions spec (<a href="https://daverupert.com/2023/05/getting-started-view-transitions/">read this post from Dave Rupert for an intro</a>) is that it’s pure progressive enhancement. If a browser doesn’t support it, things work the way they’ve worked since Mosaic.</p>
<p>One caveat is that they don’t <em>always</em> feel instant. It makes sense; you have to render what’s on the other side of a link if you want to smoothly transition between old and new. The lag between clicking and the transition starting is the time it takes your server to load the subsequent page. If you want to fix that, you’ve got two options:</p>
<ol>
<li><strong>Make your site or app faster.</strong> There’s a long list of reasons why fast is good. View transitions are another.</li>
<li><strong>Use <a href="https://getquick.link/">Quicklink</a>.</strong> It pre-fetches pages based on links that are in your viewport, network conditions permitting.</li>
</ol>
<p>These aren’t mutually exclusive options. This here website is lightweight and statically rendered (fast), and adding Quicklink turned fast into <em>instant</em>.</p>
<p>The great thing about Quicklink is that it’s <em>also</em> progressive enhancement. It’s about 1KB of non-blocking javascript that builds on top of how the platform works, instead of attempting to wholesale replace it like client-side routing alternatives. There’s no high-stakes failure mode where users can’t click a link. There’s no strange behavior with history or scroll position. Along with view transitions, you get most of the upside, none of the downside.</p>
<p>The “enhance instead of replace” approach pays in other ways too. Quicklink predates view transitions by <em>years</em>. It didn’t have to be updated to support the new spec, because building for the platform is building for the future.</p>
Taming Tailwind2023-09-19T00:00:00Zhttps://aaadaaam.com/notes/taming-tailwind/<p>I wasn’t a Tailwind fan. Between the heavy-handed approach, the needlessly antagonistic marketing, and a community prone to dog-piling valid criticism, I didn’t see much to be a fan of. But life comes at you fast, and I found myself starting a job at a company that was wrapping up a migration to – you guessed it – Tailwind.</p>
<p>So I set my hangups and pre-conceived notions aside, and did things the Tailwind way. For a solid year, I used it almost daily to design and build UI in a production Rails app. I <em>know</em> Tailwind, well and truly. Still not a fan.</p>
<p>Don’t get me wrong, there <em>absolutely are</em> bright spots. The structure and syntax of bread-and-butter utilities like margin, padding, and flex are clear, concise, and easy to work with, the color opacity system is emulation-worthy, and the documentation is excellent (praise be the type-CSS-to-get-Tailwind-CSS box).</p>
<p>Those bright spots just aren’t bright enough to distract from a weakly-held prior turned full-blown conviction; <strong>utility-only CSS doesn’t work</strong>.</p>
<p>The reality of utility CSS is that like 10% of properties – margin, padding, type sizing, color, showing / hiding things in different contexts – are doing 90% of the work. What that means in practice; <strong>a component CSS architecture with a narrow set of utilities to cover that 10% delivers <em>almost all</em> of the value that utility CSS can possibly deliver</strong>.</p>
<p>And the value <strong>is real</strong>. Utilities keep CSS small, components flexible, and architecture simple. But in a very cautionary tale kind of way, Tailwind proves that there <em>really can</em> be too much of a good thing. In order to grab that last 10% of value that utility CSS can potentially deliver, Tailwind has to do three things:</p>
<ol>
<li>Create utilities to cover <em>all</em> CSS properties and their infinite permutations.</li>
<li>Create new, stripped down mechanisms to replace CSS behavior that’s too complex to be modeled as utilities.</li>
<li>Create escape hatches to vanilla CSS, because #1 is impossible and #2 means there are things CSS can do that Tailwind can’t.</li>
</ol>
<p>The end result is a framework that makes the easy things easier, the hard things harder, and <a href="https://github.com/tailwindlabs/tailwindcss/issues?page=2&q=is%3Aissue+is%3Aclosed">invents problems</a> where none need exist.</p>
<p>Anything with multiple states (read; anything interactive) is a soup of unstructured, repetitious state prefixes. Support dark mode? Get used to typing <code>dark:</code>, because you’re going to write it in front <em>every</em> color declaration in <em>every</em> part of <em>every</em> component.</p>
<p>Paradigm-shifting custom properties? Nope. Calc()? Only in arbitrary values, which <em>really is</em> equivalent to writing inline styles. Grid? Nerfed. Complex relationship selectors? Sorry, you get the <s>pain stick</s> <code>group</code> system instead.</p>
<p>It was this last one that broke me. I was working on some goal UI updates to goals in our app, which uses <code><details></code> elements that can nest into moderately complex parent/child structures. After an hour of struggling to get custom open/closed markers to toggle in accordance with their containing parent – a basic direct child selector – I was struck by a revelation.</p>
<p><em>I could just write CSS.</em></p>
<p>There’s no award for writing “pure Tailwind”. There are no Tailwind Cops that will come to your house and arrest you for holding Tailwind the way its authors didn’t intend. It’s open source, babe. And luckily, door #3 – the escape hatch – is surprisingly pleasant.</p>
<h2>How to make CSS and Tailwind work together</h2>
<p>The gist; use Tailwind’s <a href="https://tailwindcss.com/docs/functions-and-directives#theme">theme function</a> to map values from Tailwind to component-level custom properties. Here’s a simple, contrived example:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.button</span> <span class="token punctuation">{</span>
--padding <span class="token function">theme</span><span class="token punctuation">(</span>padding.3<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color</span><span class="token punctuation">:</span> <span class="token function">theme</span><span class="token punctuation">(</span>colors.gray.900<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--background-color</span><span class="token punctuation">:</span> <span class="token function">theme</span><span class="token punctuation">(</span>colors.white<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token selector">[data-theme="dark"] &</span> <span class="token punctuation">{</span>
<span class="token property">--color</span><span class="token punctuation">:</span> <span class="token function">theme</span><span class="token punctuation">(</span>colors.white<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--background-color</span><span class="token punctuation">:</span> <span class="token function">theme</span><span class="token punctuation">(</span>colors.gray.800 / 30%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token property">padding</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--padding<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">background-color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--background-color<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>In essence, you’re reducing Tailwind’s job down to “token manager”, and unlocking the full power of modern CSS by doing so. It means you can use Tailwind utilities for the simple stuff, component CSS for the more complex stuff, and have it all work off of the same set of values. It reduces Tailwind lock-in too. Your spiciest CSS starts 99% migrated; point at :root properties instead of Tailwind values and call it a day.</p>
<p>Values you type within the theme function tab complete, which coupled with the more intuitive naming structure, makes for a <em>downright pleasant</em> authoring experience. I wish my root custom props could auto-complete like that.</p>
<p><em>“What about @apply?”</em> you might be asking. @apply manages to give you all the cons of component CSS and and all the cons of Tailwind in one package. <a href="https://twitter.com/adamwathan/status/1226511611592085504?lang=en">Even the author doesn’t want you to use @apply</a>.</p>
<h3>Media queries</h3>
<p>If you want to use Tailwind’s breakpoints in CSS, <a href="https://tailwindcss.com/docs/functions-and-directives#screen">there’s a function for that too</a>. It goes a little something like this:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.button</span> <span class="token punctuation">{</span>
<span class="token property">--font-size</span><span class="token punctuation">:</span> <span class="token function">theme</span><span class="token punctuation">(</span>fontSize.sm<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token atrule"><span class="token rule">@media</span> <span class="token function">screen</span><span class="token punctuation">(</span>lg<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
<span class="token property">--font-size</span><span class="token punctuation">:</span> <span class="token function">theme</span><span class="token punctuation">(</span>fontSize.md<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--font-size<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h3>Color</h3>
<p>I’ve spent a decent amount of time thinking about and implementing approaches for making multi-theme websites as simple as single color websites (see: theme picker on this very website). Initial values defined as custom properties are key, lest you consign yourself to the aforementioned type-dark-before-everything dungeon.</p>
<p>Tailwind doesn’t work that way out of the box, but it’s straightforward to make it so. Start by adding a root css file, and defining custom properties for your colors. You want to make sure that you only provide the raw values here; opacity modifiers won’t work otherwise.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span>
<span class="token property">--color-asphalt-50</span><span class="token punctuation">:</span> 230deg 10% 98%<span class="token punctuation">;</span>
<span class="token property">--color-asphalt-100</span><span class="token punctuation">:</span> 230deg 10% 96%<span class="token punctuation">;</span>
<span class="token comment">/* more colors... */</span>
<span class="token punctuation">}</span></code></pre>
<p>Next, <a href="https://tailwindcss.com/docs/customizing-colors#using-css-variables">customize your Tailwind theme colors</a>, and map your custom properties to new Tailwind colors.</p>
<pre class="language-js"><code class="language-js">module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">theme</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">colors</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">neutral</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token number">50</span><span class="token operator">:</span> <span class="token string">"hsl(var(--color-asphalt-50) / <alpha-value>)"</span><span class="token punctuation">,</span>
<span class="token number">100</span><span class="token operator">:</span> <span class="token string">"hsl(var(--color-asphalt-100) / <alpha-value>)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span></code></pre>
<p>You can work with colors using the normal Tailwind color syntax, and you can do “cool color stuff” with custom props in your component CSS. Win-win.</p>
Behavior wrappers2023-11-28T00:00:00Zhttps://aaadaaam.com/notes/behavior-wrappers/<p>Something that’s been on my mind lately is the value of <em>not</em> encapsulating html, css, and javascript into singular components. Sometimes – if you value keeping things simple – teasing apart behavior and presentation into composable pieces makes more sense.</p>
<p>Enter custom elements. As it happens, they’re the perfect vehicle for creating encapsulated behavior that you can use in all sorts of different contexts, and have it Just Work™, with progressive enhancement as the default behavior.</p>
<p>Here’s a relevant example from a project I’m working on. This is a custom element called <code><jitter-bug></code>. It’s job? Make a mess. Here’s how you use it:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>jitter-bug</span> <span class="token attr-name">jitter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>20<span class="token punctuation">"</span></span> <span class="token attr-name">scale</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>10<span class="token punctuation">"</span></span> <span class="token attr-name">rotation</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>15<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>this is<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>completely arbitrary<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>markup<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>jitter-bug</span><span class="token punctuation">></span></span></code></pre>
<p>Stick some markup inside, and twiddle with a few knobs to tune the effect. It’s not concerned <em>at all</em> with what’s inside in terms of markup or styling. It’s only job is to <em>mess things up</em>. This’d be hard/weird as a kitchen-sink component, but sticking to behavior-only keeps things simple:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">JitterBug</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
<span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">observedAttributes</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token string">'scale'</span><span class="token punctuation">,</span> <span class="token string">'jitter'</span><span class="token punctuation">,</span> <span class="token string">'rotation'</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">get</span> <span class="token function">scale</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">hasAttribute</span><span class="token punctuation">(</span><span class="token string">'scale'</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'scale'</span><span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token string">'0'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">get</span> <span class="token function">jitter</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">hasAttribute</span><span class="token punctuation">(</span><span class="token string">'jitter'</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'jitter'</span><span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token string">'0'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">get</span> <span class="token function">rotation</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">hasAttribute</span><span class="token punctuation">(</span><span class="token string">'rotation'</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'rotation'</span><span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token string">'0'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">static</span> <span class="token function">random</span><span class="token punctuation">(</span><span class="token parameter">min<span class="token punctuation">,</span> max</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> minimum <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">ceil</span><span class="token punctuation">(</span>min<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> maximum <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span>max<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token punctuation">(</span>maximum <span class="token operator">-</span> minimum <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">+</span> minimum<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">makeItMessy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>targetElements<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> el <span class="token operator">=</span> element<span class="token punctuation">;</span>
<span class="token keyword">const</span> rS <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>constructor<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>scale <span class="token operator">/</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>scale <span class="token operator">*</span> <span class="token number">1.5</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> rX <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>constructor<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>jitter <span class="token operator">*</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>jitter<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> rY <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>constructor<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>jitter <span class="token operator">/</span> <span class="token number">2</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>jitter <span class="token operator">/</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> rR <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>constructor<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>rotation <span class="token operator">*</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>rotation<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>jitter <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
el<span class="token punctuation">.</span>style<span class="token punctuation">.</span>translate <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>rX<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">rem </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>rY<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">rem</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>scale <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
el<span class="token punctuation">.</span>style<span class="token punctuation">.</span>scale <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>rS<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>rotation <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
el<span class="token punctuation">.</span>style<span class="token punctuation">.</span>rotate <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>rR<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">deg</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>targetElements <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">':scope > *'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">makeItMessy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
window<span class="token punctuation">.</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'jitter-bug'</span><span class="token punctuation">,</span> JitterBug<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>I use this same basic approach for all sorts of things. <code><details></code> augmentations, content overflows, animation triggers, etc. Little behavior wrappers to use any place, any time.</p>
“AI Inside”2024-02-04T00:00:00Zhttps://aaadaaam.com/notes/ai-inside/<p>Tech companies are scrambling to stake their claims in the AI gold rush, and — on top of the legal and ethical woes they’re leaving in their wake — <em>a lot of them</em> are shipping <em>surprisingly bad product</em>. The tell? If the name focuses on “✨AI”, <em>brace yourself</em>.</p>
<p>Remember Intel’s iconic “Intel Inside” campaign? Effectiveness aside, it was a marketing campaign disconnected from solving user problems. Having “Intel inside” didn’t make it easier to use a computer or unlock new use cases. Instead, the campaign rested on the premise that “Intel Inside” was <em>something consumers found inherently desirable</em>. It’s a similar story with “✨ (product-name-here) AI” and “✨ AI co-pilot” products. They think consumers want “AI” because it helps them do…. <em>stuff</em>, and so they ship software that let them do… <em>that stuff</em>. Raw technology brimming with whatever use case you can think of! (<a href="https://www.nbcnews.com/tech/tech-news/explicit-ai-generated-taylor-swift-images-continue-proliferate-x-insta-rcna136193">1</a>)(<a href="https://www.threads.net/@luokai/post/C1tURZlO3fD">2</a>)(<a href="https://www.newyorker.com/culture/infinite-scroll/is-ai-art-stealing-from-artists">3</a>)(<a href="https://arstechnica.com/information-technology/2024/01/rhyming-ai-powered-clock-sometimes-lies-about-the-time-makes-up-words/">4</a>)</p>
<p>The hope is — sticking with the gold rush metaphor — that if they send enough miners (users) down every mine shaft, they’ll eventually, <em>inevitably</em>, discover gold in them there hills. To some degree, <em>that’s tech</em>. There’s always a discovery phase with new technology, purpose-driven or otherwise. What makes this era of exploration unique?</p>
<ol>
<li>This stuff is weapons-grade plutonium, and instead of running long careful betas and designing around potential harms, companies are yeeting AI features into production without a second thought.</li>
<li>I really mean <em>“without a second thought”</em>. Companies that ought to know better are shipping product that is gimmicky, half-functional, and/or <em>outright destructive</em> to the core product and then loudly, <em>aggressively</em> promoting it in-app and everywhere else.</li>
</ol>
<p>Examples in the wild are myriad, but Notion and Adobe will do. I use products from both companies daily, and conveniently for me they offer examples of the bad way <em>and</em> some better ways to integrate large language models into commercial applications.</p>
<h2>Bad</h2>
<p>Notion’s value proposition for groups is “external hive brain”. Instead of knowledge being partitioned away in the heads of various mortal humans, Notion puts all that information into a single organized space where everyone has access to all that knowledge.</p>
<p>It follows then, that you’d want your external hive brain to be filled with high quality content. And here’s where we run into trouble. As of February 2024, Notion wants AI to be a major — perhaps default — way content gets created and edited.</p>
<ol>
<li>When you create a new document, there are two options; “blank page”, and “start writing with AI…”.</li>
<li>If you pick “blank page”, you’re met with a “press ‘space’ for AI…” prompt. This same message appears whenever you place your cursor on a blank line.</li>
<li>When you select text, the first button in the floating toolbar is “✨ Ask AI”.</li>
<li>At one point last week, I had two different in-app pop-ups for two different AI features, one of which couldn’t be dismissed without viewing the underlying promo.</li>
</ol>
<p>Notion <em>really</em> wants you to use AI.</p>
<p>But <em>why</em> would any company want an external hive brain filled with content they can’t trust? That gives you no audit trail to determine reliability? That has extra, value-free filler intentionally stuffed in? None of this is exaggeration; Notion prefaces generative AI with a “AI responses can be inaccurate or misleading” disclaimer, includes a “make it longer” tool, offers no sourcing for text it generates, and no UI for indicating whether or not a document includes generated content. These trust & quality undermining features combined with heavy emphasis in the interface work together to directly undermine Notion’s core value proposition. <em>It’s bad product.</em></p>
<p>The lesson here; general purpose AI isn’t one-size-fits-all. Notion has <em>strong</em> uses cases in tone change, summarization, and translation. The problem stems from generative tools.</p>
<p>Adobe Illustrator is a more straightforward example of clumsy execution and marketing exuberance. When you select multiple elements, <a href="https://mastodon.social/@aaadaaam/111568539175428608">two out of three primary actions in the contextual toolbar are for AI features</a>; one for “✨Generate”, and one for “Recolor”. Recolor is a useful feature (note again the correlation between a name that reflects a defined use case and quality / utility), and Generate… is not. To be fair, this isn’t a case of “add (beta) to the end to cover our asses”, <em>it really is beta software</em>. The prompting is shallow, the results aren’t predictable, and the output is akin to what you’d get from a raster-to-vector conversion (read; not great).</p>
<p>Normally that’d be fine, but turning a contextual action bar — something that should include the most common actions — into an advertising surface for tools that aren’t fully cooked and aren’t ready for professional use isn’t. It undermines Illustrator utility, and Adobe’s credibility as a reliable vendor of professional tools. <em>It’s bad product.</em></p>
<h2>Better</h2>
<p><a href="https://www.notion.so/blog/introducing-q-and-a">Notion Q&A</a> is their second attempt at integrating AI, and it shows. One again, the name tells the story. Where before we had general purpose “do AI… stuff”, now we have a specific use case; <em>ask questions and get answers from across all of your content</em>. The critical difference here is that any generation is backed by links to the content it was generated from. No crossing fingers that the summary is accurate. Just dig in.</p>
<p>And unlike their general purpose AI, Q&A improves Notion’s value proposition for larger accounts by turning “the more you add, the harder it is to find things” into “the more you add, the richer your dataset is”. <em>It’s good product.</em></p>
<p>A subset of Photoshop’s Generative Fill, <a href="https://helpx.adobe.com/photoshop/using/generative-expand.html">Generative Expand</a> strikes a similar note. In contrast to general purpose pure-prompt image generators, Generative expand solves a specific, painful problem; <em>you don’t have enough image for what you’re trying to do</em>. It’s baked directly into the crop tool; expand the crop larger than your current image, note what the extended scene should include, press enter. Simple. Style is determined by the source photo, which makes for more consistent output, and a tool that’s useful in a professional workflow. <em>It’s good product.</em></p>
<h2>Best</h2>
<p>What makes for an “ideal” AI product? Where do things go from here? Notion Q&A and Generative Expand hint at the answer:</p>
<ol>
<li>They focus on augmenting, improving, and/or manipulating original human input, <em>not replacing it</em>. First wave products like ChatGPT and Midjourney are androids; the best tools will make <em>cyborgs</em>.</li>
<li>They move past pure prompt interfaces and solve concrete problems.</li>
</ol>
<p>In other words, the arc of AI tools will probably follow a similar arc as operating systems; from command line to GUI-centric. You can already see this happening with products like Perplexity, that solve a lot of the problems inherent in ChatGPT’s approach by wrapping a lot more UI around the inputs and outputs.</p>
<p>Consider the case of image generation, where first generation tools suffer from a fatal flaw; they pack style <em>and</em> content into the same interface (a single prompt). That leads to a large amount of unpredictability, which makes them unsuitable for most professional use cases (assuming you can get past the “is this legally safe to use?” issue in the first place).</p>
<p>What makes Generative Expand more useful is that it separates out content and presentation. Imagine defining a set of “brand styles” that include a high degree of control and specificity, that you can then apply at will to to a multi-modal input that describes the content (text, reference image, doodle, all of the above). Massively more predictable, massively more powerful.</p>
<p><a href="https://www.threads.net/@carlosbannon/post/C1Eq8nUrVcT">This demo of generating architectural renderings in realtime from a GUI</a> give a hint at how much unexplored opportunity there is. Intentional interfaces, solving real problems, with “✨AI” fading back to an implementation detail.</p>
The inhuman computer2024-02-25T00:00:00Zhttps://aaadaaam.com/notes/the-inhuman-computer/<p>The Vision Pro has arrived. With it, came a <a href="https://www.vanityfair.com/news/tim-cook-apple-vision-pro">splashy Vanity Fair piece</a> featuring Tim Cook reclining at his desk, stoically indulging in some <em>spatial computing</em>. The moment I saw the shot, I was reminded of the iconic photo of early Apple designer Susan Kare, also reclining at a desk, with a smile on her face and a Mac perched behind her. <a href="https://mastodon.social/@aaadaaam/111857201909872932">I posted about it</a>, because I thought it was an interesting juxtaposition of two points in Apple’s history.</p>
<p>The more I read about the Vision Pro, the more my mind kept coming back to those two photos. <a href="https://www.youtube.com/watch?v=UvkgmyfMPks">Casey Niestat’s <em>brilliant</em> review</a> illustrates why. Here he was, sitting in the middle of New York City in the middle of the day, surrounded by people but somehow also alone; wrapped in a personal bubble of technology.</p>
<p>Now, you might take Casey’s video as tongue in cheek. Apple clearly doesn’t intend this generation of the Vision Pro to be used as a walking around device. But if you look at any of their marketing videos, their vision for how it should be used isn’t fundamentally different. People playing with their kids, talking with friends, living their lives, all funneled through the Vision Pro. Apple’s vision for the future of computing, at a time when people feel more disconnected then ever, is to disconnect people <em>even more</em> by turing real life into a digital, Apple controlled experience. It’s a vision that is fundamentally inhuman.</p>
<p>It’s not just the vision; thw same philosophy seems to have saturated into every pore of the device. All the marketing videos share an intentionally bland art direction, with “homes” ripped out of Dwell and characters that resemble department store mannequins more so than living, breathing people. And then there are <a href="https://www.theverge.com/2023/6/5/23750096/apple-vision-pro-headset-persona-facetime">the avatars</a>. My immediate reaction on seeing them was “Steve Jobs would NEVER”, quickly followed by a recall of Hayao Miyazaki’s infamous reaction to a demo of an AI-created character:</p>
<blockquote>
<p>I am utterly disgusted… I strongly feel that this is an insult to life itself.</p>
</blockquote>
<p>At every turn, the Vision Pro seeks to remind you that it wasn’t designed with <em>actual humans</em> in mind. <a href="https://www.wheresyoured.at/the-apple-vision-pro-a-review/">Ed Zitron’s scathing review</a> offers an exhaustive list of user hostilities, from a flawed interface model, to major bugs affecting fundamental UI tasks, to a design that requires an individually sized “light seal” to function properly. Even video viewing – one of the more successful use cases according to most reviews – suffers from chronic inhumanity. It’s not uncommon for movies to bring viewers to tears, but <em>crying is simply something you can’t do while using the Vision Pro</em>.</p>
<p>Which brings me back to those two photos. I don’t see the same scene captured decades apart, I see two fundamentally different philosophies, and two fundamentally different versions of Apple.</p>
<p>The Mac was infused with creative spirit from the start; both in expression and intent. It was a tool to empower creativity, empower <em>people</em> in their personal and professional lives. The photo of Susan Kare tells that story. Carefree, feet on desk, a wall of book and notes behind her, and yes, a Mac.</p>
<p>The Vision Pro however represents an inversion of the Mac’s vision of a computer as a tool that empowers and enhances human expression. Instead, like a cell turned cancerous, the technology is now the point, with humans serving to give it life (money) and purpose (more money). The photo of Tim Cook tells that story too. Drab, lifeless, art unhung, Vision Pro at center stage.</p>
<p>Apple isn’t alone here. This tail-wags-dog approach underpins the AI space at large, like it did with “web 3” and blockchain before it. If anything, it’s <em>the</em> defining characteristic of modern big tech. These are the richest companies on the planet, but they want <em>more</em>, and they’re desperate to find or force the next big thing in order to make it happen.</p>
<hr />
<ul>
<li>
<p>In the spirit of the <a href="https://en.wikipedia.org/wiki/Bechdel_test">Bechdel test</a>, I’d like to propose the “Can you cry while using it?” test as a means for assessing whether or not flesh and blood humans were centered in the making of a given piece of technology.</p>
</li>
<li>
<p>On the hardware itself; I’m a longtime owner of an Occulus Rift, which uses a large, power hungry computer and multiple hardwired sensors placed around the room to track you. The fact that Apple has packed all of that into a headset – with major improvements to things like screen resolution – is a technical feat.</p>
</li>
<li>
<p>You can’t cry in any other current-gen “goggle” headsets either, but no other system is seriously marketed as anything other than game-playing devices.</p>
</li>
<li>
<p>The <a href="https://brilliant.xyz/products/frame">Frame glasses from Brilliant Labs</a> seem much more Apple-like than the Vision Pro, and much more human-centric in approach. It’s a mystery to me why Apple – given their experience with Earpods, Watch, and Siri – decided not to pursue this direction.</p>
</li>
</ul>
Chasing color2024-10-01T00:00:00Zhttps://aaadaaam.com/notes/chasing-color/<p>I’m a fan of side-quests; professional tangents that fuel your interest, sharpen your craft, and pay long-term dividends towards whatever your main quest is. I tend to have a few running at any point; some might last a few weeks, some off and on for years.</p>
<p>One of the latter is <em>designing CSS color systems</em>. It’s stuck around for a few reasons. Color is a <em>powerful</em> tool in the design toolkit, and designing a maintainable, flexible, malleable, adaptable color systems is <em>hard</em> (read; “fun to try and solve”). I also like like to keep things grounded in practical application, which means I’ve mostly pursued this side-quest in the context of websites I’m actively designing and building. In this case, that means my <a href="https://aaadaaam.com/">personal site</a>, the <a href="https://steady.space/">website for Steady</a>, and the latest iteration of <a href="https://grease.aaadaaam.com/">my website starter, Grease</a>. We’ll get to those in a moment. First, definitions.</p>
<h2>Defining the ideal</h2>
<p>What does an ideal CSS color system look like? My answer to this question has evolved along the way, but here’s what I think, today:</p>
<ul>
<li><strong>Composable, cascading themes</strong> - You should be able to set different themes at the page-level, section-level, and component-level, and have them cascade down until a different theme is applied.</li>
<li><strong>Light & dark mode for all themes</strong> - Color themes shouldn’t be <em>in addition to</em> light and dark mode, every theme <em>includes</em> light and dark mode support.</li>
<li><strong>Expressive</strong> - It should be easy to change opacity, use tints and shades, etc. within the context of your system. No having to go off system just because you need to add transparency.</li>
<li><strong>Micromanagement-free</strong> - You should never even have to <em>think</em> about working with specific hues at the component level. In other words, adding a new theme shouldn’t mean touching every component.</li>
<li><strong>Small set of properties</strong> - You should be able to worth with a small set of properties that you can further modify vs. maintaining an extensive list of semantic colors. Easier to work with, and easier to make new themes.</li>
<li><strong>Good DX</strong> - It should feel good to work with. Flexible, straightforward, reads intuitively, forgiving, consistent, etc.</li>
</ul>
<h2>Adventure 1: “Can you theme grungy graphics?”</h2>
<p>The design for the current iteration of this website was <em>heavily</em> influenced by a desire juxtapose modern minimalism against 90’s era grungy textures, and a desire to make color completely mutable.</p>
<p>For the graphics side of things, I used an approach of applying different SVG noise filters to text and CSS shapes. That meant I could grunge up anything and animate it without having to bake a bunch of different assets. And of course it meant <em>everything</em> could have color applied by CSS.</p>
<p>The color system was simple. Map your theme colors to a narrow set of semantic variables, and apply those variables to elements and components.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span>
<span class="token comment">/* colors */</span>
<span class="token property">--color-white</span><span class="token punctuation">:</span> 0deg 0% 85%<span class="token punctuation">;</span>
<span class="token property">--color-black</span><span class="token punctuation">:</span> 0deg 0% 10%<span class="token punctuation">;</span>
<span class="token property">--color-yellow</span><span class="token punctuation">:</span> 58deg 100% 50%<span class="token punctuation">;</span>
<span class="token property">--color-gold</span><span class="token punctuation">:</span> 51deg 100% 50%<span class="token punctuation">;</span>
<span class="token comment">/* semantic colors */</span>
<span class="token property">--color-text</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-black<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-sheet</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-white<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-link</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-text<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-accent</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-yellow<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">a</span> <span class="token punctuation">{</span>
<span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">hsl</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--color-text<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token selector">&:hover</span> <span class="token punctuation">{</span>
<span class="token property">text-decoration-color</span><span class="token punctuation">:</span> <span class="token function">hsl</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--color-accent<span class="token punctuation">)</span> / 75%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Light and dark mode are separate themes, along with a core set of designed themes and a “random” option, all keyed off a data attribute applied to the HTML element. Here’s the dark theme:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">:root[theme="dark"]</span> <span class="token punctuation">{</span>
<span class="token property">--font-weight</span><span class="token punctuation">:</span> 300<span class="token punctuation">;</span>
<span class="token property">--color-text</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-white<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-sheet</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-black<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-link</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-gold<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-accent</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-gold<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>A little bit of javascript early in the page head works to apply themes, and prevent the dreaded FART (Flash of inAccurate coloR Theme):</p>
<pre class="language-js"><code class="language-js"><span class="token operator"><</span>script<span class="token operator">></span>
<span class="token keyword">const</span> theme <span class="token operator">=</span> localStorage<span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token string">'theme'</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token string">'system'</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>documentElement<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'theme'</span><span class="token punctuation">,</span> theme<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>theme <span class="token operator">===</span> <span class="token string">"random"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
document<span class="token punctuation">.</span>documentElement<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span><span class="token string">'--color-random-text'</span><span class="token punctuation">,</span> localStorage<span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token string">'color-text'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>documentElement<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span><span class="token string">'--color-random-sheet'</span><span class="token punctuation">,</span> localStorage<span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token string">'color-sheet'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token operator"><</span><span class="token operator">/</span>script<span class="token operator">></span></code></pre>
<h3>Pros of this approach</h3>
<ul>
<li>It’s dead simple. There’s only a few semantic colors, so it’s easy to work with at the component-level, and trivially easy to add new themes.</li>
</ul>
<h3>Cons of this approach</h3>
<ul>
<li><em>It’s dead simple.</em> 3 colors, with no tints or shades works with the design direction of the site, but it’s too restrictive for general use.</li>
<li>Separate light and dark themes. Light and dark are independent themes, just like every color theme. In other words, you lose light/dark control once you switch to a color theme.</li>
<li>Incomplete color definitions. This system uses a pretty common approach of only assigning the values of a color to a variable, and using a complete color definition in components so that you can adjust opacity if need be. In practice I’ve found that decomposing like this introduces more cognitive friction than I’d like. “Can I use the variable here, or do I need to wrap it?” is a question I keep having to ask myself.</li>
</ul>
<h2>Adventure 2: “Can you make color composable?”</h2>
<p>For <a href="https://steady.space/">steady.space</a>, I wanted to see if I could address the “too restrictive” problem while keeping the good parts. I had a secondary goal here; I wanted to make standing up and designing new pages <em>very fast and very easy</em>.</p>
<p>So I took the core of the last approach, and broke down the “one theme per page” model. Now, every page would be composed of “micro-themes” that cascade down until a new theme is applied. To meet my secondary goal, I married it to a set of color utilities that let me quickly and easily compose expressive color on every page without touching CSS.</p>
<p>Themes are set up pretty much the same as before:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.theme-dark</span> <span class="token punctuation">{</span>
<span class="token property">--color-text</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-asphalt-0<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-accent</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-ultramarine-400<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-sheet</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-asphalt-900<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-pattern</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-asphalt-800<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.theme-grape-aqua</span> <span class="token punctuation">{</span>
<span class="token property">--color-text</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-grape-600<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-accent</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-grape-600<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-pattern</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-aqua-400<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-sheet</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-aqua-300<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>But instead of a single theme applied to the HTML element, you use them like so:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>main</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>theme-dark<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>color --text --use-accent<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>alpha<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>bravo<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>theme-light<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card theme-ultra<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>charlie<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card theme-rhubarb<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>delta<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>main</span><span class="token punctuation">></span></span></code></pre>
<p>If you’re wondering what’s happening with that h2, it’s a little theme re-mapper:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.color</span> <span class="token punctuation">{</span>
<span class="token selector">&.--text</span> <span class="token punctuation">{</span> <span class="token property">--color-text</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--property<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token selector">&.--sheet</span> <span class="token punctuation">{</span> <span class="token property">--color-sheet</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--property<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token selector">&.--accent</span> <span class="token punctuation">{</span> <span class="token property">--color-accent</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--property<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token selector">&.--pattern</span> <span class="token punctuation">{</span> <span class="token property">--color-pattern</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--property<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token selector">&.--use-sheet</span> <span class="token punctuation">{</span> <span class="token property">--property</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-sheet<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token selector">&.--use-text</span> <span class="token punctuation">{</span> <span class="token property">--property</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-text<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token selector">&.--use-accent</span> <span class="token punctuation">{</span> <span class="token property">--property</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-accent<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token selector">&.--use-pattern</span> <span class="token punctuation">{</span> <span class="token property">--property</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-pattern<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>It gives you one more level of flexibility without having to dip into CSS, and splitting it up into two properties means I didn’t have to make a utility class for every possible combination.</p>
<h3>Pros of this approach</h3>
<ul>
<li>It’s still very simple.</li>
<li>It allows for very expressive per-page color and ultra-fast composition.</li>
<li>It’s easy to work with, and super low maintenance. You can have 1000 unique color compositions across pages without adding a single extra line of CSS.</li>
</ul>
<h3>Cons of this approach</h3>
<ul>
<li>There’s no light/dark mode support at all.</li>
<li>There’s still that slightly annoying “decomposed color” problem.</li>
<li>It’s still somewhat restrictive within individual themes. You can alter opacity, but that’s a poor proxy for actual tints and shades.</li>
</ul>
<h2>Adventure 3: “Can new CSS make it better?”</h2>
<p>When I started working on a new version of <a href="https://grease.aaadaaam.com/">Grease</a>, I knew one of things I wanted to focus on was a solid, modern, broadly useful color system. And more specifically, I was keen to see if new tools like <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark">light-dark()</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_colors/Relative_colors">relative color syntax</a> could make it better. Spolier: yes.</p>
<p>Once again, I wanted to keep the good parts, and whittle away at the cons. The changes start at the top with some new primitives; source colors, and tint & shade steps:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span>
<span class="token property">color-scheme</span><span class="token punctuation">:</span> light dark<span class="token punctuation">;</span>
<span class="token comment">/* source colors */</span>
<span class="token property">--neutral</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>80% 0.01 210<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--primary</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>30% 0.16 210<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--secondary</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>70% 0.11 20<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">/* tints & shades */</span>
<span class="token property">--0</span><span class="token punctuation">:</span> 100% 0 h<span class="token punctuation">;</span>
<span class="token property">--50</span><span class="token punctuation">:</span> 99% <span class="token function">calc</span><span class="token punctuation">(</span>c/16<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--100</span><span class="token punctuation">:</span> 94% <span class="token function">calc</span><span class="token punctuation">(</span>c/4<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--200</span><span class="token punctuation">:</span> 88% <span class="token function">calc</span><span class="token punctuation">(</span>c/2<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--300</span><span class="token punctuation">:</span> 70% c h<span class="token punctuation">;</span>
<span class="token property">--300</span><span class="token punctuation">:</span> 70% c h<span class="token punctuation">;</span>
<span class="token property">--400</span><span class="token punctuation">:</span> 60% c h<span class="token punctuation">;</span>
<span class="token property">--500</span><span class="token punctuation">:</span> 50% c h<span class="token punctuation">;</span>
<span class="token property">--600</span><span class="token punctuation">:</span> 40% c h<span class="token punctuation">;</span>
<span class="token property">--700</span><span class="token punctuation">:</span> 30% c h<span class="token punctuation">;</span>
<span class="token property">--800</span><span class="token punctuation">:</span> 20% <span class="token function">calc</span><span class="token punctuation">(</span>c/1.5<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--900</span><span class="token punctuation">:</span> 15% <span class="token function">calc</span><span class="token punctuation">(</span>c/2<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>This approach makes it very straightforward to develop a complete, consistent color palette without having to maintain an exhaustive list of properties. A <a href="https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl">perceptually uniform color space like OKLCH</a> is <em>crucial</em> to making generated color systems like this work well.</p>
<p>Those values get baked into preset colors, using relative color syntax to combine source color with tints & shades, and light-dark() to bake in theme-level light/dark mode support.</p>
<pre class="language-css"><code class="language-css"> <span class="token comment">/* preset colors */</span>
<span class="token property">--color-text</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
<span class="token function">var</span><span class="token punctuation">(</span>--primary<span class="token punctuation">)</span><span class="token punctuation">,</span>
white
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-bg</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
white<span class="token punctuation">,</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--primary<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--800<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-border</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
<span class="token function">var</span><span class="token punctuation">(</span>--neutral<span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--primary<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--600<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-subtle</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--50<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--primary<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--900<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-accent</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--secondary<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-shadow</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span></code></pre>
<p>Elements and components can and should use the preset colors as much as possible, but it’s easy to carve out on-theme exceptions. Just drop back down to the primitives:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">a:hover</span> <span class="token punctuation">{</span>
<span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--secondary<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--600<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--secondary<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--200<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>You create additional themes by redefining your source colors and presets:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.high-contrast</span> <span class="token punctuation">{</span>
<span class="token property">--neutral</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>30% 0 210<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--primary</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>30% 0.36 210<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--secondary</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>85% 0.36 20<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-text</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>black<span class="token punctuation">,</span> white<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-bg</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>white<span class="token punctuation">,</span> black<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-border</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>black<span class="token punctuation">,</span> white<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-accent</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--secondary<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-subtle</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--100<span class="token punctuation">)</span> / 50%<span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--800<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Composition mostly works the same. Themes are automatically light/dark aware, but thanks to light-dark(), a one-line utility lets you force one mode or the other:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>main</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>theme-primary<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>alpha<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>bravo<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>theme-secondary dark<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card theme-primary light<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>charlie<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card theme-primary light<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>delta<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>main</span><span class="token punctuation">></span></span></code></pre>
<h3>Pros of this approach</h3>
<ul>
<li>Super expressive, super flexible. In the example above I’m sticking to tint & shade presents, but nothing is stopping you from having fun with relative color <em>while never going fully off-theme</em>.</li>
<li>Straightforward, with a nice, tidy set of properties to work with. You get a full set of tints and shades for every color in every theme without having to manually define them.</li>
<li>You’re always working with fully formed colors.</li>
<li>No choosing between color themes and light/dark support. Every theme bakes it in.</li>
</ul>
<h3>Cons of this approach</h3>
<ul>
<li>These are very new properties. Relative color syntax in particular <a href="https://caniuse.com/css-relative-colors">isn’t fully baked yet</a>, with – at time of publish – no broad support for <code>currentcolor</code> or using a color defined with <code>light-dark()</code> as a source. Grease is always a bit forward looking (it’s purpose is to be the starting point for my next website, and I don’t start websites that often), but YMMV.</li>
<li>I’m sticking to the parts of RCS that are broadly available, so this approach is a little lacking on the <em>relative</em> part of relative color. There’s probably more simplicity to be had here when RCS is implemented in full and broadly available.</li>
</ul>
<h2>The quest continues</h2>
<p>I’m happy with the progress I’ve made here, but as you can see by <em>the fact that I’m still listing cons</em>, I’m not 100% satisfied. I just can’t help but feel that I haven’t quite cracked it yet. So onto the back burner it goes where it’ll sit and marinate. And once I’ve had time to live with this approach, and relative color syntax has time to mature, I’ll try again to see if I can finally call this side-quest “done”.</p>
House Fan2024-11-03T00:00:00Zhttps://aaadaaam.com/notes/house-fan/<p>Our house has one of those old, giant whole-house fans that looks like it was ripped out of an airplane and grafted into the ceiling. We love to run it on cool nights when we can leverage <a href="https://www.sacbee.com/news/bee-curious/article261322247.html">Sacramento’s fabled Delta breeze</a> to maximum effect. The steady current of cool, citrus and jasmine-scented air it produces is <em>deeply</em> satisfying after a sun-scorched day.</p>
<p>It’s also the <em>perfect</em> noise machine, which as it happens, turned out to be a bit of a problem. Our son got so hooked on its soothing sounds that we’d wake up in the middle of 30F winter nights to the sound of our house fan <em>on full blast</em>. We tried subbing in various white noise machines, but he was <em>dead set</em> on the house fan.</p>
<p>I get it. I’m a can’t-sleep-without-noise person, and can attest to the fact that the sound our house fan makes is <em>top-tier</em>. The magic is two overlapping tones; brown noise created by the rush of air moving through the fan, and a deep, resonant <em>thrum</em> created by the fan vibrating the structure of our house like a tuning fork. <em>Our house literally sings us to sleep.</em></p>
<p>So instead of playing a round of <em>immovable object vs unstoppable force</em>, I decided to figure out an <em>accommodation</em>. “Accommodation” is a word you hear frequently in special education and disability circles (our son is on the spectrum), and it’s a concept we work into day-to-day life. Briefly, an accommodation is a modification or adjustment to the status quo for a person with a given disability, with a goal of increasing access or equity. An accommodation might be more time to take a test, video off on a Zoom call, a ramp for someone using a wheelchair or walker, etc.</p>
<p>The accommodation in this case? A house fan emulator.</p>
<p>I sampled our house fan’s sound, built a <a href="https://housefan.aaadaaam.com/">simple offline PWA to play it</a>, loaded it on an old iPhone, and hooked it up to a bass-friendly <a href="https://www.ikea.com/us/en/p/vappeby-bluetooth-speaker-gen-3-white-black-s29571396/">Ikea bluetooth speaker</a>. To make things extra immersive, I added a “rumble pack” on his bed frame (an aquarium air pump).</p>
<p>The overall effect was surprisingly realistic. Like, can’t tell the difference in a blind test realistic. Sufficed to say, the accommodation was accepted. Happy kid, happy dad.</p>
<p>Our son uses it whenever weather doesn’t permit the real deal, and I use it (sans rumble pack) whenever I travel. You can use it too. It’ll work on your phone, offline, forever. No ads, no tracking, just <em>house fan</em>.</p>
<p>Check it out at <a href="https://housefan.aaadaaam.com/">housefan.aaadaaam.com</a>.</p>
<h2>Building House Fan</h2>
<p>In theory I could have just sampled a long recording of our house fan, but it would have been harder to use, and let’s be honest, <em>it was a reason to build something fun</em>.</p>
<p>I’ve built just-for-me iOS apps before, but I didn’t feel like dealing with app store nonsense, so I went the PWA route. Plus, I wanted to see what kind of shape PWA background audio on iOS was in (spoiler: not great).</p>
<p>This is a tiny app that I don’t want to maintain, so it’s just vanilla markup, CSS, and JS that’ll work until the heat death of the internet. Here’s what the markup looks like:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>layout<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>sound-player</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>figure</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>fan<span class="token punctuation">"</span></span> <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>a pixel art house fan<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>frame<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>louvers<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>louver<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>louver<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>louver<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>louver<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>louver<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>louver<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>blades<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>figure</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>audio</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/sounds/house-fan-60.mp3<span class="token punctuation">"</span></span> <span class="token attr-name">loop</span> <span class="token attr-name">preload</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>auto<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>audio</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>play<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button<span class="token punctuation">"</span></span> <span class="token attr-name">aria-pressed</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>false<span class="token punctuation">"</span></span> <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>play<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">aria-hidden</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">aria-hidden</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>sound-player</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>A graphic of a house fan broken up into parts for animation purposes, an audio element that points to the recording, and a button. The <code>audio</code> element is important here. If you want background audio to work on iOS – critical for this specific use case – you <em>must</em> to use an audio element. I spent too long working with fancy looping audio libraries before I figured this out.</p>
<p>All the functionality is handled by the <code>sound-player</code> custom element:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">SoundPlayer</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
<span class="token function">playable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>toggle<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>fan<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">update</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> pressed <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>toggle<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'aria-pressed'</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token string">'true'</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>toggle<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'aria-pressed'</span><span class="token punctuation">,</span> <span class="token function">String</span><span class="token punctuation">(</span><span class="token operator">!</span>pressed<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>toggle<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">toggle</span><span class="token punctuation">(</span><span class="token string">'--active'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>audio<span class="token punctuation">.</span>paused<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>audio<span class="token punctuation">.</span><span class="token function">play</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>toggle<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'aria-label'</span><span class="token punctuation">,</span> <span class="token string">'pause'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>audio<span class="token punctuation">.</span><span class="token function">pause</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>toggle<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'aria-label'</span><span class="token punctuation">,</span> <span class="token string">'play'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token function">loop</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>audio<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"timeupdate"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>audio<span class="token punctuation">.</span>currentTime <span class="token operator">></span> <span class="token keyword">this</span><span class="token punctuation">.</span>audio<span class="token punctuation">.</span>duration <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>audio<span class="token punctuation">.</span>currentTime <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>audio<span class="token punctuation">.</span><span class="token function">play</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>fan <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'figure'</span><span class="token punctuation">)</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>blades <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'.blades'</span><span class="token punctuation">)</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>toggle <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'button'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>audio <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'audio'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">playable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">loop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'sound-player'</span><span class="token punctuation">,</span> SoundPlayer<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>It does 3 things:</p>
<ol>
<li>Adds a CSS hook for when the sound is playing for style/animation purposes.</li>
<li>Adds event listeners to the fan and button to play/pause the audio and update the UI.</li>
<li>Adds an event listener to the audio that sets the time back to the beginning when there is 1s of play time left.</li>
</ol>
<p>CSS-wise, there’s a few things worth pointing out when it comes to working with small-scale pixel images like this. Here’s the styling for the fan blades, which covers them all:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.fan</span> <span class="token punctuation">{</span>
<span class="token selector">...
.blades</span> <span class="token punctuation">{</span>
<span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span>
<span class="token property">inset</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token property">z-index</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span>
<span class="token property">aspect-ratio</span><span class="token punctuation">:</span> 1/1<span class="token punctuation">;</span>
<span class="token property">background-image</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">"/assets/blades.gif"</span><span class="token punctuation">)</span></span><span class="token punctuation">;</span>
<span class="token property">background-size</span><span class="token punctuation">:</span> 200% 100%<span class="token punctuation">;</span>
<span class="token property">background-position</span><span class="token punctuation">:</span> 0 0<span class="token punctuation">;</span>
<span class="token property">background-repeat</span><span class="token punctuation">:</span> no-repeat<span class="token punctuation">;</span>
<span class="token property">image-rendering</span><span class="token punctuation">:</span> pixelated<span class="token punctuation">;</span>
<span class="token selector">&.--active</span> <span class="token punctuation">{</span>
<span class="token property">animation</span><span class="token punctuation">:</span> fan-spin 0.1s step-start infinite<span class="token punctuation">;</span>
<span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span>prefers-reduced-motion<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
<span class="token property">animation</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>It uses a common image sprite + <code>background-position</code> approach to animate through frames. Using a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#stepsinteger_step-position">steps animation timing function</a> is important here, since it gives you that hard cut you’d expect for frame-based animation. <code>image-rendering: pixelated;</code> is also critical here, otherwise the tiny sprites (some as small as 16x20 px) would be a blurry mess.</p>
<p>Making it work as an offline progressive web app took a few additions. Here’s the relevant portion of the <code>head</code> tag:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1, viewport-fit=cover<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>apple-mobile-web-app-capable<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>yes<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>apple-mobile-web-app-status-bar-style<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>default<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>theme-color<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#999<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(prefers-color-scheme: dark)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>theme-color<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#ddd<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(prefers-color-scheme: light)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>manifest<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/app.webmanifest<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>apple-touch-icon<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/assets/icons/favicon-196x196.png<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span></code></pre>
<p>The meta tags are mostly about getting a chromeless, full screen canvas, which makes for a more “app-like” experience. A manifest is a requirement for a site to be detectable as a PWA in some browsers, and includes PWA-specific metadata like the name of the app, additional icons, display mode, etc.</p>
<p>The final piece is a service worker, which is where the “offline” part happens. Here’s what it looks like:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> secondsSinceEpoch <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span>Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">/</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">PRECACHE</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">precache-v</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>secondsSinceEpoch<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">RUNTIME</span> <span class="token operator">=</span> <span class="token string">'runtime'</span><span class="token punctuation">;</span>
<span class="token comment">// A list of local resources we always want to be cached.</span>
<span class="token keyword">const</span> <span class="token constant">PRECACHE_URLS</span> <span class="token operator">=</span> <span class="token punctuation">[</span>
<span class="token string">'/'</span><span class="token punctuation">,</span>
<span class="token string">'/index.html'</span><span class="token punctuation">,</span>
<span class="token string">'/assets/main.css'</span><span class="token punctuation">,</span>
<span class="token string">'/app.js'</span><span class="token punctuation">,</span>
<span class="token string">'/sounds/house-fan-60.mp3'</span><span class="token punctuation">,</span>
<span class="token string">'/assets/frame.gif'</span><span class="token punctuation">,</span>
<span class="token string">'/assets/louvers.gif'</span><span class="token punctuation">,</span>
<span class="token string">'/assets/blades.gif'</span><span class="token punctuation">,</span>
<span class="token string">'/assets/toggle.gif'</span><span class="token punctuation">,</span>
<span class="token string">'/assets/off.gif'</span><span class="token punctuation">,</span>
<span class="token string">'/assets/on.gif'</span>
<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token comment">// The install handler takes care of precaching the resources we always need.</span>
self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'install'</span><span class="token punctuation">,</span> <span class="token parameter">event</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span>
caches<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span><span class="token constant">PRECACHE</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">cache</span> <span class="token operator">=></span> cache<span class="token punctuation">.</span><span class="token function">addAll</span><span class="token punctuation">(</span><span class="token constant">PRECACHE_URLS</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span><span class="token function">skipWaiting</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// The activate handler takes care of cleaning up old caches.</span>
self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'activate'</span><span class="token punctuation">,</span> <span class="token parameter">event</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> currentCaches <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token constant">PRECACHE</span><span class="token punctuation">,</span> <span class="token constant">RUNTIME</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span>
caches<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">cacheNames</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> cacheNames<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">cacheName</span> <span class="token operator">=></span> <span class="token operator">!</span>currentCaches<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>cacheName<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">cachesToDelete</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> Promise<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span>cachesToDelete<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">cacheToDelete</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> caches<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span>cacheToDelete<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> self<span class="token punctuation">.</span>clients<span class="token punctuation">.</span><span class="token function">claim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// The fetch handler serves responses for same-origin resources from a cache.</span>
<span class="token comment">// If no response is found, it populates the runtime cache with the response</span>
<span class="token comment">// from the network before returning it to the page.</span>
self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'fetch'</span><span class="token punctuation">,</span> <span class="token parameter">event</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token comment">// Skip cross-origin requests, like those for Google Analytics.</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>request<span class="token punctuation">.</span>url<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>location<span class="token punctuation">.</span>origin<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
event<span class="token punctuation">.</span><span class="token function">respondWith</span><span class="token punctuation">(</span>
caches<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>request<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">cachedResponse</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>cachedResponse<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> cachedResponse<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> caches<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span><span class="token constant">RUNTIME</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">cache</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token function">fetch</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>request<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token comment">// Put a copy of the response in the runtime cache.</span>
<span class="token keyword">return</span> cache<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>request<span class="token punctuation">,</span> response<span class="token punctuation">.</span><span class="token function">clone</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> response<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>This is 99% boilerplate that I found… somewhere? The part worth mentioning is the <code>PRECACHE_URLS</code> array. Every resource that you want to be available offline needs to be represented there.</p>
<p>Put all the pieces together, and you get a nice little phone experience. App icon, app-ish feel, and it works 100% offline, forever. Makes me want to build a little library of single purpose, offline PWAs.</p>
Oops, I built a headless frontend with 11ty2024-11-10T00:00:00Zhttps://aaadaaam.com/notes/headless-frontend-11ty/<p>My journey with <a href="https://www.11ty.dev/docs/data-global/">11ty’s global data</a> went something like this:</p>
<ol>
<li>“This is where my little text strings go”</li>
<li>“Neat, I can use javascript to set dynamic values”</li>
<li>“Literally any service with an API is my CMS now”</li>
</ol>
<p>That last phase is key to understanding how our recent help site migration went off-script. We’d been using Intercom for support, but at the end of the day we just weren’t satisfied with the experience for customers, or ourselves. We ended up switching to a combination of <a href="https://www.helpscout.com/">HelpScout</a> for a help desk, and <a href="https://loops.so/">Loops</a> for marketing and onboarding sequences (now email-based).</p>
<p>I was all set to do some quick styling on the Helpscout’s hosted help pages, and started digging into their docs when I saw a line of text that <em>instantly</em> set the project off course:</p>
<blockquote>
<p>The full Help Scout Docs API documentation can be found here: https://developer.helpscout.com/docs-api/.</p>
</blockquote>
<p><em><strong>“Literally any service with an API is my CMS now”</strong></em></p>
<p>Memory is hazy about what happened next; all I know is that several days later I looked up my keyboard and I’d designed/built <a href="https://steady.space/docs/">a headless front-end for our docs site</a> on our 11ty-powered website. Here’s how it works.</p>
<h2>Authentication</h2>
<p>You need <a href="https://developer.helpscout.com/docs-api/">an API key</a> to get data from your Docs site; I’m using a standard-issue dotenv setup. The steps in brief:</p>
<ol>
<li>Install <a href="https://github.com/motdotla/dotenv">dotenv</a> in your 11ty project.</li>
<li>Create a .env file and add it to your .gitignore file.</li>
<li>Add your API key to the .env file and give it a name, like <code>HELPSCOUT_DOCS=your-key-here</code></li>
<li>import dotenv at the top of your eleventy config file, like so: <code>import 'dotenv/config';</code></li>
<li>Also add your key to Cloudflare Pages / Netlify / whatever.</li>
</ol>
<h2>Talkin’ to APIs</h2>
<p>The authenticate-and-fetch part is handled by a small helper called <code>fetchHelpScoutData.js</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> EleventyFetch <span class="token keyword">from</span> <span class="token string">'@11ty/eleventy-fetch'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> environment <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../_data/env.js'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> apiKey <span class="token operator">=</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">HELPSCOUT_DOCS</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token parameter">endpoint<span class="token punctuation">,</span> apiKey</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://docsapi.helpscout.net/v1/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>endpoint<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">?status=published</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">const</span> key <span class="token operator">=</span> apiKey <span class="token operator">||</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">HELPSCOUT_DOCS</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">duration</span><span class="token operator">:</span> environment <span class="token operator">===</span> <span class="token string">'development'</span> <span class="token operator">?</span> <span class="token string">'1d'</span> <span class="token operator">:</span> <span class="token string">'5m'</span><span class="token punctuation">,</span>
<span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span><span class="token punctuation">,</span>
<span class="token literal-property property">fetchOptions</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token string-property property">'Authorization'</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Basic </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>Buffer<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>key <span class="token operator">+</span> <span class="token string">':X'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token string">'base64'</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
<span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'application/json'</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token function">EleventyFetch</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error fetching </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>endpoint<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>This helper does two important things:</p>
<ol>
<li>It keeps from having to repeat a bunch of boilerplate for every endpoint we want to grab data from. We have to iterate over multiple endpoints to get the data we need, so a clean abstraction pays dividends here.</li>
<li>It caches the data for a period of time using <a href="https://www.11ty.dev/docs/plugins/fetch/">11ty fetch</a> so we don’t hit the API every time someone runs a build in local dev.</li>
</ol>
<h2>Building global data</h2>
<p>From there, we construct global data files that get used in templates. There are three; one that returns what amounts to a sitemap (nested titles and links for collections, categories, and articles), one that returns categories and the articles within, and one that returns the full content of help articles.</p>
<p>Starting with <code>_data/help-map.js</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> fetchHelpScoutData <span class="token keyword">from</span> <span class="token string">'../_utilities/fetchHelpScoutData.js'</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">nestedAlt</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> map <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token string">'collections'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> collection <span class="token keyword">of</span> data<span class="token punctuation">.</span>collections<span class="token punctuation">.</span>items<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> collectionObject <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>collection<span class="token punctuation">,</span> <span class="token literal-property property">categories</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> categoriesData <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">collections/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>collection<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/categories</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> category <span class="token keyword">of</span> categoriesData<span class="token punctuation">.</span>categories<span class="token punctuation">.</span>items<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> categoryObject <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>category<span class="token punctuation">,</span> <span class="token literal-property property">entries</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> articlesData <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">categories/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>category<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/articles</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
categoryObject<span class="token punctuation">.</span>entries <span class="token operator">=</span> articlesData<span class="token punctuation">.</span>articles<span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>categoryObject<span class="token punctuation">.</span>slug <span class="token operator">!=</span> <span class="token string">'uncategorized'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
collectionObject<span class="token punctuation">.</span>categories<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>categoryObject<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
map<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>collectionObject<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> map<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>This demonstrates the general approach of iterating over multiple endpoints to build up a data structure that’s easy to work with. The end result here is an array of collection objects that include arrays of category objects, which contain arrays of article objects.</p>
<p>Here’s <code>_data/help-categories.js</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> fetchHelpScoutData <span class="token keyword">from</span> <span class="token string">'../_utilities/fetchHelpScoutData.js'</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">collections</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> categories <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token string">'collections'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> collection <span class="token keyword">of</span> data<span class="token punctuation">.</span>collections<span class="token punctuation">.</span>items<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> categoriesData <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">collections/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>collection<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/categories</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> category <span class="token keyword">of</span> categoriesData<span class="token punctuation">.</span>categories<span class="token punctuation">.</span>items<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> categoryObject <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>category<span class="token punctuation">,</span> <span class="token literal-property property">entries</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> articlesData <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">categories/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>category<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/articles</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
categoryObject<span class="token punctuation">.</span>entries <span class="token operator">=</span> articlesData<span class="token punctuation">.</span>articles<span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>categoryObject<span class="token punctuation">.</span>slug <span class="token operator">!=</span> <span class="token string">'uncategorized'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
categories<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>categoryObject<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> categories<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>And finally our articles, via <code>_data/help-articles.js</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> fetchHelpScoutData <span class="token keyword">from</span> <span class="token string">'../_utilities/fetchHelpScoutData.js'</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">articles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> articles <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token string">'collections'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> collection <span class="token keyword">of</span> data<span class="token punctuation">.</span>collections<span class="token punctuation">.</span>items<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> categoriesData <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">collections/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>collection<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/categories</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> category <span class="token keyword">of</span> categoriesData<span class="token punctuation">.</span>categories<span class="token punctuation">.</span>items<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> articlesData <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">categories/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>category<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/articles</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> article <span class="token keyword">of</span> articlesData<span class="token punctuation">.</span>articles<span class="token punctuation">.</span>items<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> articleData <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">articles/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>article<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>articleData<span class="token punctuation">.</span>article<span class="token punctuation">.</span>related<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> relatedArticles <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> relatedArticle <span class="token keyword">of</span> articleData<span class="token punctuation">.</span>article<span class="token punctuation">.</span>related<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> relatedArticleData <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchHelpScoutData</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">articles/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>relatedArticle<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> name<span class="token punctuation">,</span> slug<span class="token punctuation">,</span> number <span class="token punctuation">}</span> <span class="token operator">=</span> relatedArticleData<span class="token punctuation">.</span>article<span class="token punctuation">;</span>
relatedArticles<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">,</span> slug<span class="token punctuation">,</span> number <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
articleData<span class="token punctuation">.</span>article<span class="token punctuation">.</span>relatedArticles <span class="token operator">=</span> relatedArticles<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
articles<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>articleData<span class="token punctuation">.</span>article<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> articles<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>This one gets more complex in order to pull in the related entry data for each article, which we’re using for “next page” links within guides.</p>
<h2>Building views</h2>
<p>From here on out, the process is no different than if you were working with local global data file. All of the pages are handled by one layout ( <code>_layouts/docs.liquid</code>), a partial for the sidebar (<code>_includes/docs/sidebar.liquid</code>), and three templates; <code>docs/index.liquid</code> for the landing page, <code>docs/category.liquid</code> for the category pages, and <code>docs/article.liquid</code> for the individual article pages.</p>
<p>Let’s start with the sidebar:</p>
<pre class="language-liquid"><code class="language-liquid"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>nav</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-sidebar<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">for</span> <span class="token object">collection</span> <span class="token keyword">in</span> help<span class="token operator">-</span>map <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>small-details</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>details</span> <span class="token attr-name">open</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>summary</span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">collection</span><span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>summary</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">for</span> category <span class="token keyword">in</span> <span class="token object">collection</span><span class="token punctuation">.</span>categories <span class="token delimiter punctuation">%}</span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> id <span class="token delimiter punctuation">%}</span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">assign</span> matchingPages <span class="token operator">=</span> category<span class="token punctuation">.</span>entries<span class="token punctuation">.</span>items <span class="token operator">|</span> <span class="token function filter">where</span><span class="token operator">:</span> <span class="token string">"id"</span><span class="token punctuation">,</span> id <span class="token delimiter punctuation">%}</span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>details</span> <span class="token attr-name"><span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> matchingPages<span class="token punctuation">.</span><span class="token function">size</span> <span class="token operator">></span> <span class="token number">0</span> <span class="token delimiter punctuation">%}</span></span>open<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>categories<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>summary</span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>summary</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">for</span> entry <span class="token keyword">in</span> category<span class="token punctuation">.</span>entries<span class="token punctuation">.</span>items <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name"><span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> entry<span class="token punctuation">.</span>id <span class="token operator">==</span> id <span class="token delimiter punctuation">%}</span></span>aria-current</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>page<span class="token punctuation">"</span></span><span class="token attr-name"><span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span></span> <span class="token attr-name"><span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> entry<span class="token punctuation">.</span>name<span class="token punctuation">.</span><span class="token function">size</span> <span class="token operator">></span> <span class="token number">30</span> <span class="token delimiter punctuation">%}</span></span>title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> entry<span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token punctuation">"</span></span><span class="token attr-name"><span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/docs/article/<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> entry<span class="token punctuation">.</span>number <span class="token delimiter punctuation">}}</span></span>-<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> entry<span class="token punctuation">.</span>slug <span class="token delimiter punctuation">}}</span></span>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> entry<span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endfor</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>details</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endfor</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>details</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>small-details</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endfor</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>nav</span><span class="token punctuation">></span></span></code></pre>
<p>The prep work on data structure pays off here, where I can easily iterate over the entries within <code>help-map</code>. A couple of points worth noting:</p>
<ul>
<li>This uses an exclusive accordion approach, which is now trivially simple in CSS. Give > 1 details element the same <code>name</code> value and you get an exclusive accordion.</li>
<li>I wanted a given category <code>details</code> element to be open when you’re viewing an article and have the article selected. So I’m checking for a matching ID and using it to selectively set the <code>open</code> attribute on the matching category <code>details</code> and <code>aria-current="page"</code> on the article itself.</li>
</ul>
<p>The <a href="https://steady.space/docs/">landing page for the docs</a> displays the two collections and their categories:</p>
<pre class="language-liquid"><code class="language-liquid">
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">for</span> <span class="token object">collection</span> <span class="token keyword">in</span> help<span class="token operator">-</span>map <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">collection</span><span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-grid<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">for</span> category <span class="token keyword">in</span> <span class="token object">collection</span><span class="token punctuation">.</span>categories <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-card shadow-lg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-card__hero<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/docs/category/<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>number <span class="token delimiter punctuation">}}</span></span>-<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>slug <span class="token delimiter punctuation">}}</span></span>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-card__content<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h3</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-card__title<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h3</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> category<span class="token punctuation">.</span>description <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-card__description<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>description <span class="token delimiter punctuation">}}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endfor</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endfor</span> <span class="token delimiter punctuation">%}</span></span>
</code></pre>
<p>The category and article templates are where things get a little more interesting because they are used to generate pages from our HelpScout data. That’s where <a href="https://www.11ty.dev/docs/pages-from-data/">11ty’s pagination feature</a> comes into play. Here’s the template for category pages:</p>
<pre class="language-liquid"><code class="language-liquid">---
layout: docs.html
pagination:
data: help-categories
size: 1
alias: category
permalink: "docs/category/<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>number <span class="token delimiter punctuation">}}</span></span>-<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>slug <span class="token delimiter punctuation">}}</span></span>/"
eleventyComputed:
title: "<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span> – <span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> site<span class="token punctuation">.</span>title <span class="token delimiter punctuation">}}</span></span>"
description: "<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>description <span class="token operator">|</span> <span class="token function filter">strip_html</span> <span class="token operator">|</span> <span class="token function filter">truncate</span><span class="token operator">:</span> <span class="token number">160</span> <span class="token delimiter punctuation">}}</span></span>"
---
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">render</span> <span class="token string">'docs/sidebar.html'</span><span class="token punctuation">,</span> site<span class="token operator">:</span> site<span class="token punctuation">,</span> help<span class="token operator">-</span>map<span class="token operator">:</span> help<span class="token operator">-</span>map <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-primary<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-column<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>nav</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-crumbs<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/docs/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>docs<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>nav</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mt-0 mb-md leading-flush size-xxxl<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-category<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">for</span> <span class="token object">article</span> <span class="token keyword">in</span> category<span class="token punctuation">.</span>entries<span class="token punctuation">.</span>items <span class="token delimiter punctuation">%}</span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> <span class="token object">article</span><span class="token punctuation">.</span>name <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/docs/article/<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>number <span class="token delimiter punctuation">}}</span></span>-<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>slug <span class="token delimiter punctuation">}}</span></span>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>arrow<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endfor</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
</code></pre>
<p>What that frontmatter <code>pagination</code> block translates to is “generate one page for every entry in help-categories” and give help-categories an alias of ‘categories’ that I can use in my template.</p>
<p>The article template is more of the same:</p>
<pre class="language-liquid"><code class="language-liquid">---
layout: docs.html
pagination:
data: help-articles
size: 1
alias: article
permalink: "docs/article/<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>number <span class="token delimiter punctuation">}}</span></span>-<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>slug <span class="token delimiter punctuation">}}</span></span>/"
eleventyComputed:
title: "<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span> – <span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> site<span class="token punctuation">.</span>title <span class="token delimiter punctuation">}}</span></span>"
description: "<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>text <span class="token operator">|</span> <span class="token function filter">strip_html</span> <span class="token operator">|</span> <span class="token function filter">truncate</span><span class="token operator">:</span> <span class="token number">160</span> <span class="token delimiter punctuation">}}</span></span>"
---
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">assign</span> category <span class="token operator">=</span> help<span class="token operator">-</span>categories <span class="token operator">|</span> <span class="token function filter">where</span><span class="token operator">:</span> <span class="token string">"id"</span><span class="token punctuation">,</span> <span class="token object">article</span><span class="token punctuation">.</span>categories<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">|</span> <span class="token function filter">first</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">render</span> <span class="token string">'docs/sidebar.html'</span><span class="token punctuation">,</span> site<span class="token operator">:</span> site<span class="token punctuation">,</span> help<span class="token operator">-</span>map<span class="token operator">:</span> help<span class="token operator">-</span>map<span class="token punctuation">,</span> id<span class="token operator">:</span> <span class="token object">article</span><span class="token punctuation">.</span>id <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-primary<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-content<span class="token punctuation">"</span></span> <span class="token attr-name">data-pagefind-body</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-formatted<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>nav</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-crumbs<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/docs/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>docs<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span> / <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/docs/category/<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>number <span class="token delimiter punctuation">}}</span></span>-<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>slug <span class="token delimiter punctuation">}}</span></span>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> category<span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span> /<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>nav</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mt-0 mb-md leading-flush size-xxxl<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">article</span><span class="token punctuation">.</span>text <span class="token delimiter punctuation">}}</span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> <span class="token object">article</span><span class="token punctuation">.</span>relatedArticles <span class="token delimiter punctuation">%}</span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">assign</span> next <span class="token operator">=</span> <span class="token object">article</span><span class="token punctuation">.</span>relatedArticles<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-related<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>Next:<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/docs/article/<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> next<span class="token punctuation">.</span>number <span class="token delimiter punctuation">}}</span></span>-<span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> next<span class="token punctuation">.</span>slug <span class="token delimiter punctuation">}}</span></span>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> next<span class="token punctuation">.</span>name <span class="token delimiter punctuation">}}</span></span><span class="token entity named-entity" title=" ">&nbsp;</span>→<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>table-of-contents</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>table-of-contents</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
</code></pre>
<p>One of the nice things about HelpScout is that their docs data is stored as HTML, so we don’t need to do any processing or translation on the content.</p>
<p>That <code>table-of-contents</code> tag is a custom element that includes clickable links of all of the major headings within an article, that auto-updates the active heading as you scroll. The heading ID’s are auto-generated by the new <a href="https://www.11ty.dev/docs/plugins/id-attribute/">ID attribute plugin in 11ty 3.0</a>. Here’s what the custom element looks like:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">TableOfContents</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
<span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">observeHeadings</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> headings <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'h2'</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">heading</span> <span class="token operator">=></span> heading<span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>headings<span class="token punctuation">.</span>length <span class="token operator"><</span> <span class="token number">2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>style<span class="token punctuation">.</span>display <span class="token operator">=</span> <span class="token string">'none'</span><span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> toc <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">generateTOC</span><span class="token punctuation">(</span>headings<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
<h3>On this page</h3>
<div></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>toc<span class="token punctuation">.</span>outerHTML<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"></div>
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">generateTOC</span><span class="token punctuation">(</span><span class="token parameter">headings</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> toc <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'ul'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> stack <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">{</span> <span class="token literal-property property">level</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token literal-property property">element</span><span class="token operator">:</span> toc <span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
headings<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">heading</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> level <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>heading<span class="token punctuation">.</span>tagName<span class="token punctuation">.</span><span class="token function">charAt</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> listItem <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'li'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> link <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
link<span class="token punctuation">.</span>textContent <span class="token operator">=</span> heading<span class="token punctuation">.</span>textContent<span class="token punctuation">;</span>
link<span class="token punctuation">.</span>href <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">#</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>heading<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
listItem<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span>level <span class="token operator"><=</span> stack<span class="token punctuation">[</span>stack<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>level<span class="token punctuation">)</span> <span class="token punctuation">{</span>
stack<span class="token punctuation">.</span><span class="token function">pop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>level <span class="token operator">></span> stack<span class="token punctuation">[</span>stack<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>level<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> newList <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'ul'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
stack<span class="token punctuation">[</span>stack<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>element<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>newList<span class="token punctuation">)</span><span class="token punctuation">;</span>
stack<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span> level<span class="token punctuation">,</span> <span class="token literal-property property">element</span><span class="token operator">:</span> newList <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
stack<span class="token punctuation">[</span>stack<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>element<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>listItem<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> toc<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">observeHeadings</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> headings <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'h2'</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">heading</span> <span class="token operator">=></span> heading<span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> isScrolling <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> clickedLink <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> observer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">IntersectionObserver</span><span class="token punctuation">(</span>
<span class="token punctuation">(</span><span class="token parameter">entries</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>isScrolling <span class="token operator">&&</span> <span class="token operator">!</span>clickedLink<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> visibleHeadings <span class="token operator">=</span> entries<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">entry</span> <span class="token operator">=></span> entry<span class="token punctuation">.</span>isIntersecting<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>visibleHeadings<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> firstVisibleHeading <span class="token operator">=</span> visibleHeadings<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>target<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">highlightTOCItem</span><span class="token punctuation">(</span>firstVisibleHeading<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span> <span class="token literal-property property">rootMargin</span><span class="token operator">:</span> <span class="token string">'0px 0px 0px 0px'</span> <span class="token punctuation">}</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
headings<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">heading</span> <span class="token operator">=></span> observer<span class="token punctuation">.</span><span class="token function">observe</span><span class="token punctuation">(</span>heading<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> tocLinks <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
tocLinks<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">link</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
link<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
event<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> targetId <span class="token operator">=</span> link<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'href'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> targetElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span>targetId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>targetElement<span class="token punctuation">)</span> <span class="token punctuation">{</span>
clickedLink <span class="token operator">=</span> link<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">highlightTOCItem</span><span class="token punctuation">(</span>targetElement<span class="token punctuation">)</span><span class="token punctuation">;</span>
targetElement<span class="token punctuation">.</span><span class="token function">scrollIntoView</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">behavior</span><span class="token operator">:</span> <span class="token string">'smooth'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'scroll'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>clickedLink<span class="token punctuation">)</span> <span class="token punctuation">{</span>
isScrolling <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
<span class="token function">clearTimeout</span><span class="token punctuation">(</span>window<span class="token punctuation">.</span>scrollTimeout<span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span>scrollTimeout <span class="token operator">=</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
isScrolling <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
clickedLink <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">passive</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">highlightTOCItem</span><span class="token punctuation">(</span><span class="token parameter">heading</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> links <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
links<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">link</span> <span class="token operator">=></span> link<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token string">'active'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> headingId <span class="token operator">=</span> heading<span class="token punctuation">.</span>id<span class="token punctuation">;</span>
<span class="token keyword">const</span> correspondingLink <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">a[href="#</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>headingId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"]</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>correspondingLink<span class="token punctuation">)</span> <span class="token punctuation">{</span>
correspondingLink<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'active'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'table-of-contents'</span><span class="token punctuation">,</span> TableOfContents<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>There’s a lot going on here, but generally we are:</p>
<ul>
<li>Finding all the h2 elements on the page and using them to build our TOC.</li>
<li>Observing which heading is within the viewport and updating the active TOC element as you scroll.</li>
<li>Updating the active TOC element when you click on it directly.</li>
</ul>
<p>At this point, we’ve got all of our pages in place, and some solid patterns for fast and fluid navigation. Just one thing missing; search.</p>
<h2>Adding search with Pagefind</h2>
<p>In my experience, adding search to a static site is painful, clunky, or both. Which is why I was <em>delighted</em> to discover <a href="https://pagefind.app/">Pagefind</a>. You need to do four things; set up your search input, tell Pagefind what to index, include the Pagefind CSS and JS, and trigger indexing as part of your build.</p>
<p>The search input is set up jump menu style, courtesy of a js-free popover:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>dialog</span> <span class="token attr-name">popover</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>search<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>modal shadow-lg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>searchResults<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'DOMContentLoaded'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">new</span> <span class="token class-name">PagefindUI</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">element</span><span class="token operator">:</span> <span class="token string">"#searchResults"</span><span class="token punctuation">,</span>
<span class="token literal-property property">showSubResults</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token literal-property property">showImages</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token literal-property property">resetStyles</span><span class="token operator">:</span> <span class="token boolean">false</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>dialog</span><span class="token punctuation">></span></span></code></pre>
<p>Telling Pagefind what to index is as simple as adding a data attribute to the context you want to index. In this case, I added it to the element that wraps article content:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>docs-content<span class="token punctuation">"</span></span> <span class="token attr-name">data-pagefind-body</span><span class="token punctuation">></span></span>
...
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span></code></pre>
<p>I set up the PageFind CSS and JS behind a <code>pagefind</code> front matter key so that any page or layout can opt-in as need be.</p>
<pre class="language-liquid"><code class="language-liquid"><span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">if</span> pagefind <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/pagefind/pagefind-ui.css<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/pagefind/pagefind-ui.js<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> <span class="token keyword">endif</span> <span class="token delimiter punctuation">%}</span></span></code></pre>
<p>To build the index, I created a new <code>index</code> script in the project’s <code>package.json</code> file, and added it to the existing scripts for staging and production build (for local dev I run the script as needed only):</p>
<pre class="language-json"><code class="language-json"><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"ELEVENTY_ENV=development eleventy --serve --incremental --quiet"</span><span class="token punctuation">,</span>
<span class="token property">"publish:stage"</span><span class="token operator">:</span> <span class="token string">"ELEVENTY_ENV=stage eleventy && npm run index"</span><span class="token punctuation">,</span>
<span class="token property">"publish:prod"</span><span class="token operator">:</span> <span class="token string">"ELEVENTY_ENV=prod eleventy && npm run index"</span><span class="token punctuation">,</span>
<span class="token property">"index"</span><span class="token operator">:</span> <span class="token string">"npx pagefind --site _public"</span>
<span class="token punctuation">}</span></code></pre>
<p>That’s all there is to it. Well done Pagefind.</p>
<h2>Wrapping up</h2>
<p>For a couple days of work to design and build, the results were well worth it. We get all the benefits of keeping help content in HelpScout (CMS for easy management, being able to include help page content in responses to customers, etc), and all of the benefits from a completely bespoke frontend (better experience, tighter brand integration, better SEO from not having help content on a subdomain). The project didn’t go as planned, but I wouldn’t change a thing.</p>
Dusting cobwebs2024-12-17T00:00:00Zhttps://aaadaaam.com/notes/dusting-cobwebs/<p>I haven’t touched the design of this website in a few years, and I’ve mostly been content to let it collect a patina of dust and cobwebs. One of my explicit goals with this iteration of my website was to make it <em>re-design resistant</em> after all.</p>
<p>The key word there is <em>mostly</em>. I’d designed the <a href="https://aaadaaam.com/work/">overview of my work</a> around the job of, well, <em>getting a job</em>. But I’ve been gainfully, happily employed for years now. The job changed, so the page needed to change too. And like with real dust and cobwebs, cleaning one spot tends to put everything left untouched in stark relief. Sufficed to say, <em>one thing led to another</em>. Not to a full re-design, but definitely a refresh. A deep <s>spring</s> winter cleaning.</p>
<h2>re-designing /work</h2>
<p>The original design for <a href="https://aaadaaam.com/work/">/work</a> was focused around a collection of case studies. A reasonable approach when you’re job hunting, but it created two problems:</p>
<ol>
<li>All of my case studies were password protected, which made for an unsatisfying experience for all but the small number of people who I gave the password to.</li>
<li>Focusing on depth inherently means you’re <em>not</em> focusing on breadth. Cherry-picking from a long career looks more or less the same as showing the entirety of a short career.</li>
</ol>
<p>This time around, I oriented the design around <em>breadth</em> and <em>range</em>. I talk about how I’m a generalist designer on my front door; this page needs to pay it off. And while it’s doing that, make it clear that I’m operating from a depth of experience.</p>
<p>So instead of running with the typical “only show the work for the job you want” advice that’s perfectly fine when you’re on the hunt, I took the opposite approach. No edits, no omissions. A comprehensive timeline of my career in tech, but from a 30,000ft view.</p>
<p>All in all, it’s a much more enjoyable page for a visitor, and one that gives you a much better sense of my approach and body of work.</p>
<h2>Incorporating anchor positioning</h2>
<p>I’m a fan of side-quests, so I decided early on that I’d also use this /work redesign as an excuse to play with <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning">anchor positioning</a>. And given it’s still early days and I didn’t want to polyfill, the usage couldn’t be <em>core</em>.</p>
<p>Since I was already running with a timeline, I used dialogs and anchor position to make that timeline more interactive and invite you to explore. About half the cards (in browsers that support anchor positioning) have an affordance you can click to get a little popover with more detail about the piece.</p>
<p>At first I thought it was too subtle / not “anchor position-y” enough. But then I realized something; <em>that’s the point</em>. I’d never reach for javascript library for something like this, but a little bit of markup and CSS? Sure.</p>
<h2>HSL -> OKLCH</h2>
<p>I took some of of what I learned from <a href="https://aaadaaam.com/notes/chasing-color/">chasing color</a> and switched the color system from HSL to OKLCH. This was particularly beneficial for the “random” theme option, which previously converted the colors to RGB in order to apply some arcane javascript that I <em>flatly did not understand</em> in order to derive a contrast ratio between two randomly generated colors.</p>
<p>The OKLCH version? 5 lines:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">static</span> <span class="token function">lightnessDelta</span><span class="token punctuation">(</span><span class="token parameter">color1<span class="token punctuation">,</span> color2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> lightness1 <span class="token operator">=</span> <span class="token function">parseFloat</span><span class="token punctuation">(</span>color1<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'%'</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> lightness2 <span class="token operator">=</span> <span class="token function">parseFloat</span><span class="token punctuation">(</span>color2<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'%'</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> Math<span class="token punctuation">.</span><span class="token function">abs</span><span class="token punctuation">(</span>lightness1 <span class="token operator">-</span> lightness2<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>OKLCH rules.</p>
<h2>New themes, new patterns</h2>
<p>A big part of how I’m making a <em>re-design resistant</em> design happen is by making change a constant. Patterns change on every page load. There’s a whole range of color themes, and a random option for even more variety. <em>It’s tough to get sick of something that’s constantly changing.</em></p>
<p>But there are somewhat fixed parts of these systems, so I gave them some attention. I loaded up a new set of theme presets, and made a bunch of subtle tweaks to the pattern system that makes for a nice overall change in <em>the vibe</em>. Just enough changes to stave off re-design-itis for a few more years.</p>
Hibernating2025-01-06T00:00:00Zhttps://aaadaaam.com/notes/hibernating/<p>The older I get, the more I find myself drawn towards seasonality and cyclicality. I find that it both gives structure and meaning to the present while satisfying that deep seated human need for <em>change</em>.</p>
<p>One of the ways I embrace cyclicality in my life is by taking off the two weeks between Christmas and New year’s. And I really, really mean “off”. No work, no side-projects, no blogging, no social media. The goal is to rest, recharge, and reset my perspective in a way that can only happen when you truly <em>stop</em>.</p>
<p>It’s something I’ve done for the past 5 or 6 years, and it’s become a self-care cornerstone. I look forward to it all year, and it lets me come back to work with a spring in my step and a smile on my face. Anecdotally, I’ve never hit a burnout wall since I started doing this. What can I say? Bears know what’s up.</p>
<p>That doesn’t mean I do <em>nothing</em>. Quite the opposite! Here’s my non-exhaustive list of leisure accomplishments for winter break 2024:</p>
<ul>
<li>Played 40+ hours of <a href="https://baldursgate3.game/">Baldur’s Gate 3</a>.</li>
<li>Watched <em>literally</em> every Studio Ghibli movie.</li>
<li>Read a few thousand pages of fiction, notably including the latest entry in <a href="https://www.brandonsanderson.com/pages/the-stormlight-archive-series">Brandon Sanderson’s Stormlight Archive</a>.</li>
<li>Made (and ate) a bunch of great food, including some amazing leftover potroast sandwiches using my homemade Rosemary & Preserved Meyer Lemon Focaccia.</li>
<li>Built this <a href="https://www.lego.com/en-us/product/dungeons-dragons-red-dragon-s-tale-21348">truly epic Dungeons & Dragons lego set</a> with my kid.</li>
<li>Went to a fun <a href="https://imaginarium360.com/sacramento/">interactive light experience</a> at the fairgrounds.</li>
<li>Played many, many rounds of hide and seek.</li>
<li>Slept in.</li>
</ul>
<p>It was a <em>damn fine</em> break. Here’s to doing it again next year.</p>
Tools that build you2025-04-27T00:00:00Zhttps://aaadaaam.com/notes/tools-that-build-you/<p>While putting the finishing touches on the latest release of <a href="https://grease.aaadaaam.com/">Grease</a>, the words <em>“5 years ago”</em> caught my eye in GitHub. 5 years? Working on this? Of all the things I could do with my free time, of all the side-quests that fizzled out, <em>why am I still working on this?</em></p>
<p>Upon reflection, my conclusion; <strong>it’s good to make and modify your own tools.</strong></p>
<p>Backing way up; tools can be crudely grouped into one of two categories. There are <strong>off-the-shelf tools</strong> that you buy or borrow; a table saw, a hammer, <em>React</em>. And then there are <strong>bespoke tools that you make or modify</strong>. Elaborate handmade jigs, bent bits of wire taped to handles that carve clay <em>just so</em>.</p>
<p>Off-the-shelf tools are the foundation of any professional practice, and rightly so. If a cabinet maker built their own table saws and drill presses, there’d be precious little time for <em>making cabinetry</em>. But bespoke tools fill an important role, in that they do <em>precisely</em> what off-the-shelf tools cannot; they fit your <em>exact</em> needs and desires.</p>
<p>Professionally and creatively, <strong>that’s important</strong>. Case in point; prior to Grease, I’d inherited a large Jekyll website that was long in the tooth, and slow to build. Sufficed to say, waiting 2 minutes after pressing save to see a CSS change show up in browser is <em>a serious problem</em> for someone who’s process revolves around designing in-browser. It made for worse work that took longer, and was a real flow-killer to boot. Tools that fit the way you work <em>matter</em>.</p>
<p>But the value of building your own tools goes beyond functional min/maxing and workflow efficiencies. This took me a while to notice, but ultimately it’s the thing that keeps me coming back. <strong>Working on Grease has made me a better designer and developer.</strong></p>
<p>I don’t remember when it happened, but at some point, Grease became my canvas for exploring new ideas & platform features, a receptacle for lessons learned from production projects, and a game of <em>“OK but now how can I do this better?”</em>. A functional, ever-evolving “note to self” of how I think websites should be made.</p>
<p>Building a reusable foundation in Grease pushed me to think more deeply about approaching CSS as a declarative system instead of an ever-growing collection of disconnected rules. It’s made me spend time considering just how much tooling is “worth it”. It’s made me take more care in designing with the grain of the web. It’s made me more conscientious about naming things. It’s made me think about the craft and the process in a way that wouldn’t have otherwise happened if I were just doing typical professional work. It’s made me better.</p>
<p>The thing is, you can’t make good tools without thinking long and hard about <em>the craft and process</em> the tools will be used in. And that act – stepping outside of <em>doing the work</em> and examining the whole – creates a virtuous cycle. You build the tool, the tool builds you.</p>
There’s no such thing as a CSS reset2025-07-13T00:00:00Zhttps://aaadaaam.com/notes/useful-defaults/<p>CSS resets are a technique nearly as long lived as the modern CSS era itself, stretching all the way from the CSS Zen Garden days to inclusion in mass-market current-day tools like Tailwind. Resets are a <em>staple</em> in the CSS toolkit.</p>
<p>But here’s the thing; <em>there’s no such thing as a CSS reset</em>. Now, to be clear, I’m not suggesting some kind of long-running mass hallucination. I’m suggesting CSS resets <em>don’t do what the name says</em>. The word “reset” implies an objective default state that you’re restoring to, but the only <em>objective</em> default state <em>is what browsers ship</em>. <strong>You cannot “reset” browser styles by addition.</strong> Resets are <em>inherently subjective</em>, which means in practice, you are not so much <em>resetting</em> as you are <em>defining default element styles for a website</em>.</p>
<p>You might think I’m nit-picking semantics – it’s true, I am – but it’s a useful semantic distinction. Because when it comes to defining default styles you only have two choices:</p>
<ol>
<li>Define defaults that are useful without additional styling.</li>
<li>Define defaults that <em>are not</em> useful without additional styling.</li>
</ol>
<p>Using those same two bookends of CSS Zen Garden and Tailwind, we started with approach #1, and over time went <em>hard</em> for approach #2. Like, <em>“men would rather invent a CSS framework rather than style a single <code><button></code> tag”</em> hard.</p>
<p>To be fair, there were reasons.</p>
<h2>Mo’ CSS, mo’ problems</h2>
<p>At the risk of flattening nuance, you can more or less summarize the the shift from “embrace the cascade” to “burn the cascade with fire” in two words; <em>specificity woes</em>.</p>
<p>Using tag selectors exclusively inherently lead to deeply nested selectors, which to say another way, <em>increasingly specific selectors</em>. Inevitably you’d run into a situation where you want a style to override one of those highly specific selectors, and you only had a few options:</p>
<ol>
<li>Add more selector depth to further increase specificity.</li>
<li>Play with source order.</li>
<li>Use the nuclear option, AKA <code>!important</code>.</li>
</ol>
<p>These tools were fine-ish at small scales and in small doses, but big projects would quickly spiral out of control. The more CSS you added, the more painful it got.</p>
<p>Which is why when strategies like BEM emerged, they felt like a breath of fresh air. You could just… <em>not think about specificity and the cascade at all</em>. Everything was flat, tightly encapsulated, and highly predictable, with nary an <code>!important</code> in sight.</p>
<p>That idea of highly encapsulated component styles became the more or less defacto approach across a variety of permutations; Tailwind, styled components, Atomic CSS, shadow DOM, etc. But through all of those permutations, the humble CSS reset lived on as an element style vestigial tail. It would after all be pretty obnoxious to have to zero out the margin, remove list bullets, etc. in every component in a project where that’s not what you want 90% of the time.</p>
<h2>Why use element styles?</h2>
<p>The status quo became the status quo for valid reasons, but that’s <em>not</em> to say there weren’t any tradeoffs made along the way. There are some <em>wonderful</em> benefits to directly usable styled elements, and new tools like <a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers">cascasde layers</a>, and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:where">:where()</a> mitigate so many of the cons that the pros are worth a second look. The top 3, in my humble opinion:</p>
<ol>
<li><strong>Free names:</strong> Word on the street is that naming things is <em>hard</em>. What if I told you that there are 100+ names that you can use right now, today, for the low, low price of “free”? Think of elements like components, but ones that come packed in the browser. Custom elements, without the “custom” part. You can just like, <em>use them</em>.</li>
<li><strong>Enforce semantics:</strong> People (rightly) bemoan the fact that so many websites composed of undifferentiated div soup, but that’s large in part due to the fact that <em>most modern approaches to CSS intentionally decouple style from structure</em>. By making elements functional components, you can strongly encourage use of semantic HTML. Out with <code><div class="button"></code>, in with <code><button></code>.</li>
<li><strong>Simple page composition:</strong> With styled elements, you can just write simple markup or Markdown to scaffold a page. Maybe toss a class in here or there when there’s an exception. I’ve built a bunch of marketing sites over the years and it’s extremely pleasant to design/build a page by starting with semantic markup and having 50%-60% of the work done.</li>
</ol>
<h2>Putting the baby back in the bathwater</h2>
<p>Cascade layers are <em>incredible</em>. I stan cascade layers. Every word of this post to this point has been a tee up for me to sing the praises of cascade layers. I am convinced that if cascade layers had existed from the jump, we would have an order of magnitude fewer “CSS hard” memes. They’re easy to use, but give you precise control over CSS specificity (which is to say, they’re very powerful). Here’s how I like to use them:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@layer</span> base<span class="token punctuation">,</span> components<span class="token punctuation">,</span> utilities<span class="token punctuation">;</span></span>
<span class="token atrule"><span class="token rule">@import</span> <span class="token string">"_base/_base.css"</span> <span class="token function">layer</span><span class="token punctuation">(</span>base<span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="token atrule"><span class="token rule">@import</span> <span class="token string">"_components/_components.css"</span> <span class="token function">layer</span><span class="token punctuation">(</span>components<span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="token atrule"><span class="token rule">@import</span> <span class="token string">"_utilities/_utilities.css"</span> <span class="token function">layer</span><span class="token punctuation">(</span>utilities<span class="token punctuation">)</span><span class="token punctuation">;</span></span></code></pre>
<p>Each of those imports are an entrypoint that in turn imports more granular CSS files. You can use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@layer">@layer</a> rules too, but I prefer this approach since it means you can ignore cascade layers outside of this basic config. With 4 lines of CSS, you can more or less run wild with useful, directly usable element styles without them ever overriding your component and utility styles. This is a pretty simple example (I like to keep things simple), but you can use more layers, add sub-layers, whatever makes sense for the project at hand.</p>
<p>Note that I said <em>“more or less run wild”</em> above. Unless you’re already using a defensive approach to writing component CSS, it’s still true that it’d often be impractical or annoying to have to zero out margins, unset pseudo elements, etc. from base styles in all of your component definitions.</p>
<p>A great way to solve that problem is by making spicy element styles opt-in, like so:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">:where(article)</span> <span class="token punctuation">{</span>
<span class="token selector">& > * + *</span> <span class="token punctuation">{</span>
<span class="token property">margin-block-start</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--flow-space<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">& li::before</span> <span class="token punctuation">{</span>
<span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">" "</span><span class="token punctuation">;</span>
<span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span>
<span class="token property">height</span><span class="token punctuation">:</span> 0.1rem<span class="token punctuation">;</span>
<span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Your core element styles take care of the 80% case, opt-in element styles take care of the 20% case, and you can keep writing component CSS as if you’re using a reset + pure encapsulation approach.</p>
<p>With cascade layers and opt-in spicy styles, you might still have a few instances where you need to manage specificity <em>within</em> layers. A sub-layer might be a good choice, but I rely on :where() selectors primarily, which let you use nested selectors <em>without</em> increasing specificity. Combining cascade layers with :where() lets you create some <em>truly wimpy styles</em>:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@layer</span> base</span> <span class="token punctuation">{</span>
<span class="token selector">:where(ul > li)</span> <span class="token punctuation">{</span>
<span class="token property">color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p><em>Everything</em> beats this style. You look at this style wrong and it crumbles. This is the fainting goat of CSS.</p>
<p>There are more techniques and more tools in the tool belt when it comes to managing the cascade (looking at you <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope">@scope</a>), but the point is, CSS is in a <em>fundamentally different place</em> than it was even 5 years ago, and it’s worth taking the time to reconsider decisions made around outdated tradeoffs.</p>
Kobo-pilled2025-08-11T00:00:00Zhttps://aaadaaam.com/notes/kobo-pilled/<p>About 6 months ago – right around the time that Amazon made it crystal clear that <a href="https://www.theverge.com/news/612898/amazon-removing-kindle-book-download-transfer-usb">you do not own ebooks you purchase in any meaningful sense</a> – I bought a Kobo Libra Color.</p>
<p>I don’t really do product reviews, and this <em>isn’t really</em> a product review. I’m writing about my switch because it’s the most refreshing technology experience I’ve had <em>in years</em>, and one that meaningfully changed my behavior for the better. <em>Whew.</em></p>
<p>Prior to the aforementioned <em>enshittification event</em>, my Kindle experience was “fine”. I bought and read books for years; mostly on my phone (I know) instead of my ancient PaperWhite. I didn’t really think about it at all, in the way I don’t think about my internet provider. It was just the conduit for my reading habit. I probably would have kept muddling along that way for years if Amazon hadn’t decided to be… <em>Amazon</em>. But they did, and in retrospect I’m better off, so (grits teeth) <em>thanks Amazon</em>.</p>
<p>The device itself is nice, but that’s not really what this story is about, so here’s the hardware TL;DR. It feels like tech that’s intended to be used and abused. It’s solid, light, and decidedly un-precious. Case? Screen protector? LOL, no. Throw it around, get it wet, <em>it’s fine</em>. People have <em>big feelings</em> about color e-ink screens. I like it. The UI is snappy by e-ink standards. <em>End hardware review.</em></p>
<p>The real star of the show is the Kobo software and ecosystem. I say this as a pure, unabashed compliment; the Kobo experience feels like a product of a past era. A few highlights:</p>
<ul>
<li>You can plug you Kobo into a computer and sync it up with the open source e-book swiss army knife, <a href="https://calibre-ebook.com/">Calibre</a>. To say it another way; <em>you can completely ignore the Kobo store if you want</em>. How many modern portable devices can you say that about?</li>
<li>You have a ton of control over the reading experience, in a Win95 control panel kind of way. It’s <em>a lot</em>, but also empowering.</li>
<li>The Kobo store <em>is not</em> filled with an endless sea of self-published platform exclusive drek. It’s filled with… exactly the same books you’d see at a good local bookstore. In other words, it seems like it’s run by people <em>who actually read and like books</em>.</li>
</ul>
<p>There’s more, but all these things add up to a device that feels like tech from 20 years ago. And in the era of enshittification, I find that <em>incredibly refreshing</em>.</p>
<p>But the real killer feature is Kobo’s <a href="https://www.overdrive.com/">Overdrive</a> integration. Prior to owning a Kobo, I couldn’t tell you the last time I checked a book out from the library. It just wasn’t part of my reading habit. But now I had this rectangle in my hand that could do all sorts of <em>library things</em>, so I was like <em>hey, why not give this a shot</em>.</p>
<p>The good times start with discovery. Overdrive is baked directly into the on-device storefront, so you can browse around and seamlessly place a hold. You get notified when holds become available, and can check out, read on device, return, the whole 9 yards. A whole library of books, stuffed into a 6" rectangle. <em>It’s awesome</em>. So awesome that I went from being a “I only buy books” guy to a <em>certified Library Guy</em>. I read daily, and I’ve done <em>nothing</em> but read e-books from the library for the past 6 months.</p>
<p>I struggle to think of another piece of technology that has had such an <em>immediate and drastic</em> impact on one of my core habits. In a way, it feels like I’ve discovered reading all over again. Every time a hold becomes available – particularly when it lands early – it feels like Christmas. I love it. Kobo (and library) pilled.</p>
This website has no class2025-09-14T00:00:00Zhttps://aaadaaam.com/notes/no-class/<p>In my recent post, <a href="https://aaadaaam.com/notes/useful-defaults/">“There’s no such thing as a CSS reset”</a>, I wrote this:</p>
<blockquote>
<p>Think of elements like components, but ones that come packed in the browser. Custom elements, without the “custom” part. You can just like, <em>use them</em>.</p>
</blockquote>
<p>The line continued to rattle around in my head, and a few weeks later when I was digging into some cleanup work I came to an uncomfortable realization; <em>I wasn’t really taking my own advice</em>. Sure, I was setting some default element styles, but I was leaving <em>a lot</em> on the table. I felt attacked. Called out even. Present me, <em>positively roasted</em> by past me. There was only one possible solution; <strong>refactor my website.</strong></p>
<p>I like to apply severe constraints in designing and building this site – I think constraints lead to interesting, creative solutions – and it was no different this time around. Instead of relying on built in elements <em>a bit more</em>, I decided to <em>banish classes from my website completely</em>. I haven’t used a class-free approach since the CSS Zen Garden days, and wanted to se how it felt with modern HTML and CSS.</p>
<h2>Doubling down on styled defaults</h2>
<p>CSS for the site was structured around 3 cascade layers; <code>base</code>, <code>components</code>, and <code>utilities</code>. Everything in <code>base</code> was already tag selectors, so the task at hand was to change my approach for components, and eliminate utilities completely.</p>
<p>Step 1? <em>Mitigation.</em> There was plenty of code that could have been styled defaults but wasn’t, so I gave all my markup a thorough review, increasing use of semantic elements, extracting common patterns in the form of new element defaults, and making more use of contextual element styling. By contextual styling, I mean going from something like this:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.header-primary</span> <span class="token punctuation">{</span>
<span class="token property">margin-block</span><span class="token punctuation">:</span> <span class="token function">clamp</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--size-sm<span class="token punctuation">)</span><span class="token punctuation">,</span> 4vw<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--size-lg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--size-flex<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>To something like this:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">body</span> <span class="token punctuation">{</span>
<span class="token property">background-color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-sheet<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token selector">& > header</span> <span class="token punctuation">{</span>
<span class="token property">margin-block</span><span class="token punctuation">:</span> <span class="token function">clamp</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--size-sm<span class="token punctuation">)</span><span class="token punctuation">,</span> 4vw<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--size-lg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--size-flex<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>It was a good start, and modern features like nesting, <code>:where()</code>, and <code>:has()</code> made this feel better that it did 20 years ago, but I took things way too far with contextual styles. Taken to the extreme, you end up with overloaded selector definitions and progressively more esoteric selector patterns. I knew I was down the rabbit hole when I did something like this:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">li</span> <span class="token punctuation">{</span>
<span class="token selector">&:has( > a + p)</span> <span class="token punctuation">{</span>
<span class="token property">padding-block</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--size-lg<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">border-block-end</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--border-default<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">text-wrap</span><span class="token punctuation">:</span> balance<span class="token punctuation">;</span>
<span class="token selector">& > a</span> <span class="token punctuation">{</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--font-xxl<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">& > p</span> <span class="token punctuation">{</span>
<span class="token property">margin-block</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--size-sm<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>I still needed a “real” solution for components, and a way to manage variants.</p>
<h2>Custom tags & custom attributes</h2>
<p>I had an inkling of a solution, which is to leverage patterns from custom elements and web components, sans js. By virtue of their progressively enhanced nature, custom tag names and custom attributes are 100% valid HTML, javascript or no. That inkling turned into fervent belief after reading Keith Cirkel’s excellent post <a href="https://www.keithcirkel.co.uk/css-classes-considered-harmful/">“CSS classes considered harmful”</a>.</p>
<p>Revisiting the example above, now we’ve got a pattern like this:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">note-pad</span> <span class="token punctuation">{</span>
<span class="token property">padding-block</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--size-lg<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">border-block-end</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--border-default<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">text-wrap</span><span class="token punctuation">:</span> balance<span class="token punctuation">;</span>
<span class="token selector">& a</span> <span class="token punctuation">{</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--font-xxl<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">& p</span> <span class="token punctuation">{</span>
<span class="token property">margin-block</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--size-sm<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Custom attributes become a go-to for handling former BEM modifiers, but instead of relying on stylistic writing convention to fake a key-value pair, you get an <em>actual</em> key-value pair.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">random-pattern</span> <span class="token punctuation">{</span>
<span class="token selector">& [shape-type="1"]</span> <span class="token punctuation">{</span>
<span class="token property">border</span><span class="token punctuation">:</span> 0.1rem solid <span class="token function">var</span><span class="token punctuation">(</span>--color-sheet<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">background-color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-sheet<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">filter</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">"#noise1"</span><span class="token punctuation">)</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">& [shape-type="2"]</span> <span class="token punctuation">{</span>
<span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--pattern-lines-horizontal<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">background-size</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--pattern-scale<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Now, you can use <code>data-whatever</code> for attributes, but really, any two dash-separated words are safe. Personally, I think dropping the <code>data</code> prefix feels better and allows for richer semantics.</p>
<p>You can argue that both of these techniques are re-inventing classes in various ways. Kind of! You can use custom element names in lieu of semantic tags, just like you can slap a class on a div. But these techniques, particularly with how you can seamlessly enhance to true custom elements or web components, feels like a coherent end-to-end system in a way that class-based approaches don’t. <em>It’s tags and attributes, all the way down.</em></p>
<h2>Would I do this again?</h2>
<p>On the plus side, the user outcomes are decidedly positive; I removed a non-trivial amount of CSS (now about ~5KB of CSS over the wire for the entire site), and accessibility is without question better due to having to paid much closer attention to markup. Also, <em>just look</em> at that markup. So clean. So shiny.</p>
<p>On the flipside, this feels like an approach that <em>simply asks more of authors</em>. It requires more careful planning compared to pure component approaches; you can’t think of things in purely isolated terms. All to say, I’m very happy to ship this on my personal website, I’d be less likely to advocate for this approach on a large project with varied levels of frontend knowledge.</p>
<p>There’s a variation here that’s more encapsulated (use custom tag names with abandon), but that pulls on what feels like an unresolved thread; replacing a semantic element with a custom tag name that has no semantic value <em>feels bad</em>, and adding extra wrappers around everything <em>also feels bad</em>.</p>
<p>All to say, I’m not quite ready to say that this is The One True Way I’ll build all sites from now on, but I also can’t help but feel like I’ve crossed some kind of threshold. I used to think classes were fine. Now I’m not so sure. I don’t know exactly where it’ll lead yet, but this feels like one of those exercises that’ll have a lasting influence on my work.</p>
<hr />
<p><em>A mea culpa; I only got 99% of the way there. I use <a href="https://www.11ty.dev/docs/plugins/syntaxhighlight/">11ty’s syntax highlighting plugin</a>, which uses classes for styling. I gave <a href="https://andreruffert.github.io/syntax-highlight-element/">syntax-highlight</a> a hard look, but I don’t love the idea of introducing client-side js where none need exist, and the authoring experience would be a step back, so I begrudgingly left it alone for now.</em></p>
Taming Tailwind, part 22025-10-01T00:00:00Zhttps://aaadaaam.com/notes/taming-tailwind-part-two/<p>In the first <a href="https://aaadaaam.com/notes/taming-tailwind/">Taming Tailwind</a> post, I documented how I’d inherited a Tailwind project that I eventually switched from a utility-first approach to a hybrid modern CSS + Tailwind architecture. I didn’t intend to write any more on the subject, but then they went an released a major rewrite with Tailwind 4. This is the story of that upgrade; the good, the bad, the WTFs, with even more taming.</p>
<h2>Tailwind 4; now featuring CSS</h2>
<p>The major change from v3 is a <em>significantly</em> more CSS-forward stance; CSS-based configuration, design tokens as custom properties, real cascade layers instead of the faux-layer system that made it impossible to use real cascade layers (I’m not bitter). No beating around the bush; this is an <em>extremely positive</em> change, and coupled with streamlined tooling, v4 is flatly better, and significantly so.</p>
<p>Here’s the entirety of the v4 configuration for the project:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@import</span> <span class="token string">'tailwindcss'</span> <span class="token function">source</span><span class="token punctuation">(</span><span class="token string">"../../../app"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
@source <span class="token string">"../../../app/**/*.svg"</span><span class="token punctuation">;</span>
@source <span class="token string">"../../../app/frontend/**/*.css"</span><span class="token punctuation">;</span>
@source <span class="token selector">"../../../app/</span><span class="token punctuation">{</span>components<span class="token punctuation">,</span>views<span class="token punctuation">}</span><span class="token comment">/**/</span>*.html*"<span class="token punctuation">;</span>
<span class="token atrule"><span class="token rule">@source</span> <span class="token string">"../../../config/initializers/*.rb"</span><span class="token punctuation">;</span></span>
<span class="token atrule"><span class="token rule">@variant</span> dark <span class="token punctuation">(</span>&<span class="token punctuation">:</span><span class="token function">where</span><span class="token punctuation">(</span>[data-theme=<span class="token string">"dark"</span>]<span class="token punctuation">,</span> [data-theme=<span class="token string">"dark"</span>] *<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="token atrule"><span class="token rule">@theme</span></span> <span class="token punctuation">{</span>
--color-*<span class="token punctuation">:</span> initial<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Sufficed to say, this is much more pleasant – and much less config – than the json v3 equivalent. Note the <code>--color-*: initial;</code> line, we’ll get to that later.</p>
<h2>Upgrading; ouch</h2>
<p>Some libraries take great pains to avoid breaking changes, and/or maintain backwards compatibility. <em>And then there’s Tailwind.</em> Breaking changes were everywhere. Some were obviously due to under the hood changes, but the majority of it seems to have come from a “let’s revisit all of our decisions” place.</p>
<p>To their credit, there is a migration script, but <a href="https://github.com/tailwindlabs/tailwindcss/releases">as you can see from the releases</a>, it’s been an ongoing work in progress. At the time that I made the switch, I’ll hazard a guess that the migration script took care of about half of the work, and I had to exhaustively QA every single route. Woof.</p>
<p>Not unique to Tailwind, but this is a prime example of why I don’t like using CSS libraries. They come with a tax, and the bill comes due at upgrade time.</p>
<h2>Out with the theme() function, in with custom properties</h2>
<p>Tailwind 3 included a <code>theme()</code> function that used a dot notation to access various token values. You’d use it like this:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.button</span> <span class="token punctuation">{</span>
<span class="token property">font-weight</span><span class="token punctuation">:</span> <span class="token function">theme</span><span class="token punctuation">(</span>fontWeight.normal<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>That’s toast in v4. Instead, most tokens are just regular old custom properties:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.button</span> <span class="token punctuation">{</span>
<span class="token property">font-weight</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--font-weight-normal<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p><em>So much better</em>, particularly for the kind of approach I’m using here, where Tailwind is being used alongside regular CSS. It feels like one system now, which makes things easier to reason about.</p>
<p>Note that I said “most tokens”. V4 includes a few single purpose functions; one for spacing values, and another for manipulating opacity. Here’s how they work:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.button</span> <span class="token punctuation">{</span>
<span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">--alpha</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--color-text<span class="token punctuation">)</span> / 10%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">padding</span><span class="token punctuation">:</span> <span class="token function">--spacing</span><span class="token punctuation">(</span>2<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>These compile down to <code>color-mix()</code> and <code>calc()</code> respectively. You can also use the underlying <code>var(--spacing)</code> custom property if you want the default value. Obviously you can use any number in these functions, so they don’t set you up for success consistency-wise. On the plus side, they’re syntactically identical to forthcoming <a href="https://www.w3.org/TR/css-mixins-1/">CSS functions</a>, which’ll make for an easy transition when they’re ready for prime time.</p>
<h2>Color management; still bad</h2>
<p>Aside from switching to custom properties, the Tailwind color story is unchanged from v3. Which is to say, <em>it’s still one of the worst aspects of Tailwind</em>. It starts with the assumption that you’re going to litter every component with <code>dark:</code> variants. So right out of the gate, you’re signing up for utility bloat, and touching every component if you want to change colors. Now let’s say you want to take thinks a step further and introduce additional color themes. Here’s the Tailwind way, pulled straight from the docs:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@custom-variant</span> theme-midnight <span class="token punctuation">(</span>&<span class="token punctuation">:</span><span class="token function">where</span><span class="token punctuation">(</span>[data-theme=<span class="token string">"midnight"</span>] *<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span></code></pre>
<p>Which would get used like:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bg-white dark:bg-black theme-midnight:bg-blue-800<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>That’s right, the answer is <em>even more component-level utility bloat</em>. Utter madness. I honestly struggle to think of a worse way to managing color with CSS. So I relieved Tailwind of the job, and implemented a system that I outlined in <a href="https://aaadaaam.com/notes/chasing-color/">Chasing Color</a>. First step is dropping all the Tailwind provided colors in the Tailwind config:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@theme</span></span> <span class="token punctuation">{</span>
--color-*<span class="token punctuation">:</span> initial<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Then defining some new <code>:root</code> colors:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span>
<span class="token comment">/* color: core */</span>
<span class="token property">--color-white</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>100% 0 0<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-neutral</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>60% 0.04 276<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-accent</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>55% 0.23 276<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-success</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>69% 0.12 195<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-danger</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>60% 0.18 10<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-warning</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>69% 0.15 33<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-link</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>60% 0.18 256<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-highlight</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>79% 0.3 90<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">/* color: steps */</span>
<span class="token property">--50</span><span class="token punctuation">:</span> 98% <span class="token function">calc</span><span class="token punctuation">(</span>c/16<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--100</span><span class="token punctuation">:</span> 95% <span class="token function">calc</span><span class="token punctuation">(</span>c/8<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--200</span><span class="token punctuation">:</span> 93% <span class="token function">calc</span><span class="token punctuation">(</span>c/4<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--300</span><span class="token punctuation">:</span> 85% <span class="token function">calc</span><span class="token punctuation">(</span>c/1.5<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--400</span><span class="token punctuation">:</span> 75% c h<span class="token punctuation">;</span>
<span class="token property">--500</span><span class="token punctuation">:</span> 55% c h<span class="token punctuation">;</span>
<span class="token property">--600</span><span class="token punctuation">:</span> 50% c h<span class="token punctuation">;</span>
<span class="token property">--700</span><span class="token punctuation">:</span> 35% <span class="token function">calc</span><span class="token punctuation">(</span>c/1.5<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--800</span><span class="token punctuation">:</span> 25% <span class="token function">calc</span><span class="token punctuation">(</span>c/2<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--900</span><span class="token punctuation">:</span> 21% <span class="token function">calc</span><span class="token punctuation">(</span>c/2<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token property">--950</span><span class="token punctuation">:</span> 19% <span class="token function">calc</span><span class="token punctuation">(</span>c/2<span class="token punctuation">)</span> h<span class="token punctuation">;</span>
<span class="token comment">/* color: presets */</span>
<span class="token property">--color-text</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--color-neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--900<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--color-neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--50<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-text-muted</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--color-neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--600<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--color-neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--400<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">--color-text-subtle</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--color-neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--400<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token function">oklch</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--color-neutral<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--600<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">/* more presets... */</span>
<span class="token punctuation">}</span></code></pre>
<p>Those presets are used directly in CSS, and a set of non-Tailwind utilities applies select presets in the random instances where UI is composed of utilities (mostly non-customer facing admin views).</p>
<p>Want to change the accent color globally? <em>Change one custom property.</em> Want to add additional themes? <em>Redefine a few custom properties.</em> What’s more, this approach adds a significant amount of consistency, makes it so you don’t have to think about themes and color modes at the component level <em>at all</em>, and in practice shed a surprising amount of weight from the production CSS bundle.</p>
<h2>Closing thoughts</h2>
<p>V4 is the best version of Tailwind, and a vast improvement from v3. But the simple truth remains; utility-only CSS is a deeply flawed architectural approach, and using more modern CSS features doesn’t change that fact. Which makes mitigation – like with asbestos – the best approach.</p>
<p>For this project in particular, the worst offenders have been fully excised. The project is mostly component CSS now, with Tailwind relegated to build, token management, and exception management duty. Because of that, there’s not a strong reason to spend the time it’d take to full remove it. It just doesn’t make sense given all the other priorities we have. But as more CSS features become widely available, like functions and custom media queries, I’ll keep chipping away at it. If I write a third post in this series, it’ll be brief. <em>“I got rid of Tailwind. The end.”</em></p>
Lowriders & websites2025-10-06T00:00:00Zhttps://aaadaaam.com/notes/Lowriders-and-websites/<p>My recent post on <a href="https://aaadaaam.com/notes/no-class/">removing classes from my personal site</a> was unusual in a few ways; I got a surprising number of kind emails about it, and on the flip side, it broke containment and ended up on Hacker News. I know you shouldn’t look at the comments. No good can <em>possibly</em> come from looking at the comments. <em>I looked at the comments.</em></p>
<p>My immediate takeaway; the advice on not reading Hacker News comments is <em>good advice</em>. My less immediate takeaway had to do with one comment in particular. The commenter in question – let’s call them ElonStan420 – was taking me to task for the following line:</p>
<blockquote>
<p>Also, <em>just look</em> at that markup. So clean. So shiny.</p>
</blockquote>
<p>According to ElonStan420, I’d committed the ultimate Very Serious Programmer sin of putting time and energy towards something that “doesn’t really matter”.</p>
<p>It instantly reminded of our annual trip to the California State Fair. Along with corn dogs cattle, and carnival rides, the state fair features yearly exhibitions of various facets of California culture. On rotation this year was an exhibit on lowriders, with a dozen or so examples arranged in a circle, hoods, trunks, and doors ajar so you could see every detail.</p>
<p>I don’t profess to know much about lowrider culture, but I can tell you this; those cars were <em>beautiful</em>. Obviously, the paint and pinstripes were immaculate. But what struck me was the fact that <em>there was’t a single part</em> of these vehicles that received anything other than the utmost care and attention. The springs, the struts, the frame, the engine compartment, <em>everything</em>. They felt more like functional sculptures than “car”, and absolutely oozed pride-of-craft. That’s not me editorializing; in many instances, the owner was standing by, beaming with pride at the oooohs and ahhhs from the crowd.</p>
<p>I think of ElonStan420 standing in that exhibit hall, eyeing those cars with disdain because all that time, energy, care, and expression “doesn’t really matter”. Those hand-painted pinstripes don’t make the car faster or cheaper. Chrome-plated everything doesn’t make it more efficient. No one is going to look under the hood anyway.</p>
<p>The thing is, this particular brand of “functional absolutism” that’s widely held in tech circles is a bankrupt philosophy. It leaves no room for beauty, no room for expression, no room for investing time and care in something for no other reason than <em>you find it satisfying to do so</em>.</p>
<p>Should every website be the subject of <em>maximal craft</em>? No, of course not. But in a industry rife with KPI-obsessed, cookie-cutter, vibe-coded, careless slop, we could use more lowriders.</p>
Pumped up2025-10-28T00:00:00Zhttps://aaadaaam.com/notes/pumped-up/<p>If you asked people to name the most impactful technology of the last 100 years, some might say the lightbulb, others the transistor, and others still the microprocessor.</p>
<p>Each of those has played a major role in shaping modern life, but there’s another technology that’s equally as impactful, but day in, day out, goes largely unnoticed aside from the low hum coming from your kitchen. Or more specifically, <em>from your refrigerator</em>.</p>
<p>The thing inside that makes your food cold – the humble heat pump – is a true modern marvel that doesn’t get <em>nearly</em> the credit it deserves. You don’t need to look any further than refrigeration to make the case. We take it for granted, but do you know what food was like before modern refrigeration? <em>Not great.</em></p>
<p><a href="https://www.cdc.gov/mmwr/preview/mmwrhtml/mm4840a1.htm">Food-related illnesses were rife</a>, good luck getting fresh fruit and vegetables out of season, everything had to be canned or preserved to prevent spoilage, most food ended up spoiled anyway, if you were lucky you had the privilege of constantly dragging giant blocks of ice into your icebox, and so on. In other words, food <em>regularly killed people</em>, was less nutritious, comparatively <em>tasted like shit</em>, and required <em>exponentially more effort</em>. Thank you heat pumps.</p>
<p>But refrigeration is just the tip of the iceberg. Let’s move on to the next most familiar use of heat pumps; air conditioning. Now, if you live in a cool climate you might write off this particular use case off as unnecessary or even wasteful, but air conditioning <em>saves lives</em> in hot climates. According to <a href="https://www.iea.org/reports/sustainable-affordable-cooling-can-save-tens-of-thousands-of-lives-each-year">this report from the IEA</a>, AC saved an estimated <em>190,000 lives</em> from 2019-2021 alone. The fact that you can live in a climate like mine here in Sacramento without feeling like you’re living on the surface of the sun is just a <em>nice bonus</em>.</p>
<p><em>But wait, there’s more.</em> Heat pumps also have the power to improve our health <em>and</em> play a major role in saving our sorry selves from the existential threat that is global warming by eliminating fossil fuel use and significantly increasing efficiency.</p>
<p>I’ll use my house as an example. It relied quite a bit on natural gas, with some low-efficiency electric appliances in the mix; gas furnace, gas stove, gas water heater, resistance heat dryer. That made it remarkably similar to the <a href="https://www.eia.gov/todayinenergy/detail.php?id=55940">75% of homes in the western USA</a> that rely on natural gas, and part of the <a href="https://www.eia.gov/tools/faqs/faq.php?id=50&t=8">4.5 trillion cubic feet of natural gas used by residential homes</a> in the USA each year.</p>
<p>Now, this is a bit of an aside, but bear with me. <em>“Natural gas” is the greatest marketing con job ever perpetrated on society.</em></p>
<p>“Oh, it’s natural, it must be harmless”. No. You know what natural gas is? <a href="https://en.wikipedia.org/wiki/Natural_gas">Methane</a>. Just like what comes out of a cow’s ass. You know that prank where 13 year old boys cup their hands around a fart and then release it in someone’s face? That’s basically what people are doing with natural gas, except the fart was dealt 200 million years ago, the 13 year old boy is an energy utility company, and <em>you’re paying them</em> to un-cup that ancient, decrepit fart in your face.</p>
<p>You might be thinking, “it doesn’t go into <em>my</em> face, it gets burned up in my stove and my water heater”. Not quite. Like with the cupped fart, leakage is a fact of life with natural gas. Leaks happen <a href="https://www.eia.gov/todayinenergy/detail.php?id=62383">during extraction</a>, transport, and finally, at the point of use, <em>directly into your face</em>. The former contributes to global warming and local air pollution, the latter literally <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC10901287/">poisons you</a>.</p>
<p>Now don’t get me wrong, burning methane is better than burning coal. But <em>we have the technology</em>. Yes, you guessed right, <em>heat pumps</em>. Up to this point, I’ve only talked about ways heat pumps cool things. But heat pumps are called “heat pumps” and not “cold makers” for a reason; they <em>move</em> heat from place to place. They can move heat <em>out of a space</em> cool it, but they can also do the reverse and move heat <em>into a space</em> to warm it up.</p>
<p><em>Moving</em> heat is also the key to why heat pumps can be so efficient in heating applications. A gas furnace or resistive electrical elements <em>can never</em> be more than 100% efficient. The best you can do is turn 100% of that fuel source into heat. But heat pumps don’t <em>directly</em> turn farts or watts into heat. They use watts to run a compressor that moves heat that’s in the air or ground into your home, which lets you readily break that efficiency barrier. Why settle for 100% efficiency when you can have 300% or more?</p>
<p>Historically, heats pumps haven’t been used nearly as much for heating purposes, but all sorts of companies have improving that particular use case, and as of today are cranking out uber-clean, ultra-efficient alternatives to common home appliances. Going back to the example of my house:</p>
<ul>
<li><strong>Gas furnace and separate air conditioner → whole house heat pump.</strong> Basically a more efficient AC that runs in reverse in the winter, capable of pulling heat out of sub-zero temperatures. Much cheaper to run in my market, zero venting risk, and it <em>eliminated an entire HVAC system</em>.</li>
<li><strong>Gas water heater → hybrid heat pump water heater.</strong> It has standard electric heat strips for backup; we keep it in heat-pump only mode. Again, cheaper, safer.</li>
<li><strong>Electric vented dryer → ventless heat pump washer/dryer combo.</strong> Way more efficient, and extra so because you’re not taking conditioned air from inside your home and shoving it out you dryer vent. Also, not having to switch loads <em>rules</em>.</li>
<li><strong>Gas range → induction range.</strong> Not a heat pump, <em>but totally awesome</em>. You’re cooking with <em>magnets</em>.</li>
</ul>
<p>The net result of these changes are a decarbonized home that’s significantly safer, more comfortable, and cheaper to operate, which keeps my local air cleaner, and the world less toasty. Heat pumps are awesome.</p>