Aquileo | David LuhrDavid Luhr is a designer, developer, and independent consultant with a passion for accessibility and creating a responsible web. Learn more about his work.2026-06-04T22:53:46Zhttps://luhr.co/David Luhrhello@luhr.coAquileo | A 10x engineer isn't what you think2025-09-05T00:00:00Zhttps://luhr.co/blog/2025/09/05/a-10x-engineer-isnt-what-you-think/<h1>A 10x engineer isn't what you think</h1>
<p>If 10x engineers exist, they don't work 10x faster or create 10x the output. Speed and output without quality are waste.</p>
<p>A 10x engineer creates 10x the value with the same resources. They question requirements, cut scope and hone in on what's essential, continuously improve the process, and iterate on their work.</p>
<h2>The essential 10%</h2>
<p>Lean manufacturing shows that 90% of any process is waste. By identifying the waste and eliminating it, we are left with the 10% of value-adding activities.</p>
<p>Now, this doesn't mean we fill 100% of our capacity with the 10% of value-adding activities at 10x the scale. That likely is impossible and unsustainable.</p>
<p>Instead, we can work more calmly (increased sustainability), with stricter criteria for what we take on (better framing), and enjoy more slack in the system (increased resilience) while using fewer resources (increased efficiency).</p>
<h2>Pairing</h2>
<p>Pairing is another dimension engineers can scale their impact. Pairing is the most effective form of knowledge sharing and cross-mentorship I've experienced, as two people working on the same project can continuously support, unblock, and mentor each other. This is especially powerful when the pairing partners have different skillsets, as they level their skills over time.</p>
<p>Pairing on work is also faster, more efficient, and higher quality than solo work. Having someone to look up documentation while you work, help troubleshoot issues, keep you unblocked, offer more creative ideas, motivate you to stay focused, and act as a continuous code reviewer is many times more effective than the typical way of working.</p>
<p>So if 10x engineers exist, they likely are pairing.</p>
<h2>How to enable 10x engineers</h2>
<p>If we understand that the goal is to increase quality and value creation, not just pace or volume, we prioritize creating space instead of simply moving faster or shipping more features.</p>
<p>We should reserve capacity for people to reflect on their work and form novel ideas. They should have time to identify and eliminate waste, which improves quality, increases efficiency, and allows us to focus on more valuable work. This will also naturally lead to an increase in pace and volume while preserving quality and value creation.</p>
<p>We should encourage people to question requirements, including whether a potential project is even worth doing if it's not sufficiently timely and impactful. One of the best outcomes of potential projects is determining that we shouldn't do them at all, as we save 100% of the resources we would have invested, and avoid the risks of diluting our focus and maintenance overhead. We should celebrate and encourage these decisions instead of only incentivizing doing more, new things.</p>
<p>Lastly, when making hiring, resourcing, or process decisions, defer to those doing the work to understand what they need. Often, teams try to hire their way out of bottlenecks, thinking that more people will increase pace and output. In reality, this has diminishing or even negative returns, as it simply scales or exacerbates existing waste and bottlenecks. Instead, by focusing our resources and efforts on the 10% of work that is valuable, we can have much larger impact with the same or even fewer resources, while being happier and more fulfilled along the way.</p>
Aquileo | 10 things I've learned from 10 years of studying lean manufacturing2025-07-22T00:00:00Zhttps://luhr.co/blog/2025/07/21/10-things-ive-learned-from-10-years-of-studying-lean-manufacturing/<h1>10 things I've learned from 10 years of studying lean manufacturing</h1>
<p>Lateral thinking has proven to be one of the most effective habits I've developed in my career. Over the last 10 years, learning about lean manufacturing has benefited my work in software more than any other topic.</p>
<p>10 years ago, I stumbled across the topic of lean manufacturing. I had recently shifted my career into web design and engineering and was underwhelmed by Scrum and other permutations of Agile. The original intent of Agile seemed clear and focused, but the day-to-day reality of how teams were managing projects and their overall process seemed over-engineered, heavy, and exhausting.</p>
<p>Then I discovered modern lean manufacturing and was drawn to the simplicity and lightness, along with the deep commitment to quality and continuous improvement.</p>
<p>For the last decade, I've independently studied all aspects of lean manufacturing, including the history of the Toyota Production System, the emergence of 2 Second Lean, and all the recent transformations companies have undergone.</p>
<p>There's something so concrete about observing physical processes moving through a factory, learning to identify and address waste, and clearly recognize improvements in quality, efficiency, and most importantly, the happiness of the teams and customers. These learnings have allowed me to spot and correct the much more abstract wastes found in digital work, and address areas of need that no software development methodology even considers.</p>
<p>I want to cover 10 things I've learned that have most impacted my work. I'm going to avoid technical terms, concepts, and theories wherever possible, but would be happy to expand on anything in future posts.</p>
<h2>1. Everything is a process</h2>
<p>The first mistake many individuals and teams make is to think lessons from another industries or types of companies don't apply. How can learning about physical production lines in a factory help with software development?</p>
<p>The truth is, the industry, product, production steps, etc. are all arbitrary.</p>
<p>What unites all forms of work is: everything is a process.</p>
<p>This realization is critical to understanding that everything, from welding an aluminum frame, to reviewing scientific findings, to designing and engineering a dropdown menu in a web app, is a process that creates or destroys value.</p>
<p>In fact, our job titles are misleading. A designer's job isn't to design user interfaces. Their job is to continuously improve the process of designing user interfaces. Any role should basically be considered to that of a "process engineer."</p>
<p>So, the lessons learned by other industries, even with wildly different contexts and approaches, have underlying, universal truths to learn.</p>
<h2>2. There's always a customer</h2>
<p>In addition to everything being a process, we need to realize every process has a downstream customer.</p>
<p>Of course, a business serves customers who buy a product. And of course, we want to serve them and produce the best quality and value for our customers.</p>
<p>But, we need to recognize that everything we do always has a customer. Within a team, that customer is usually internal. The customer might be someone in a different role or layer of the organization, and sometimes is yourself, present or future.</p>
<p>If we ask "who is the customer?" with any project or decision, we can be clear on who we're serving and creating value for. This practice completely inverts an org chart. The CEO's customers include everyone in the company. A manager's customers include everyone on the team. An engineer's customers include a pairing partner, a designer, or customer advocate.</p>
<p>And of course, everyone in the company is there to serve the actual customers, first and foremost. But, this is usually top of mind already, and sometimes is used as an excuse for not serving or even sacrificing internal customers, which ironically will also destroy values for external customers.</p>
<h2>3. Blame the process, not the person</h2>
<p>With processes behind everything we do in service of a customer, how should we respond when someone makes a mistake or we discover waste?</p>
<p>We should blame the process, not the person.</p>
<p>This inverts the typical reflex of assuming a mistake is an individual failing, shortcoming, or skill issue.</p>
<p>Instead, we need to realize that the vast majority of people really just want a challenging, interesting, and rewarding career. We're all just hoping to do a good job, contribute meaningfully, and benefit from the shared success of our work.</p>
<p>Next, we need to recognize that, outside of rare cases of actual sabotage, mistakes are the result of some upstream process, decision, or even standard. Everything from a complete lack of process to a very considered and detailed process has waste and risk of mistakes.</p>
<p>With this in mind, our first thought when something goes wrong is to blame the process. This engages everyone, including the person who made a mistake, in making things better.</p>
<p>Issues no longer become a point of individual blame and shame. They become an opportunity for improvement. The individual who made the mistake is no longer the wrongdoer. It shouldn't reflect on their personal performance. They become the most recent expert in what went wrong, what caused the issue, and how we might prevent it in the future. Their experience is the most valuable information to understand, and their ideas and creative solutions are key to improving. They should be evaluated on their contributions to the improvement.</p>
<h2>4. Learn to identify and not tolerate waste</h2>
<p>The default mode for many teams is performing routine work without question or reflection. Because everything is a process, and every process has waste, it's critical to learn to identify waste.</p>
<p>The best way to do this is to be sensitive to anything that creates friction, frustration, or struggle, no matter how small. As you work, continuously ask if there's a better way.</p>
<p>In learn manufacturing, there are 8 types of waste:</p>
<ol>
<li>Transportation: unnecessary movements of products or materials</li>
<li>Inventory: excess raw materials, work in progress (batch processing), or finished goods. Often the root cause of other wastes.</li>
<li>Motion: unnecessary movements of people</li>
<li>Waiting</li>
<li>Overproduction: making more than the next process needs</li>
<li>Overprocessing: unnecessary steps or processing that often lead to transportation waste</li>
<li>Defects: efforts caused by reject, rework, returns</li>
<li>Skills: wasted human genius and potential. The worst of the 8 wastes.</li>
</ol>
<p>Although these originated from physical manufacturing, they easily map to software development:</p>
<ol>
<li>Transportation: every code change has to go from commit, to pull request, go through review, get merged to staging, get validated, get rolled out to production, etc.</li>
<li>Inventory: concurrent projects create excessive work in progress, leading to waiting, overproduction, overprocessing, transportation, defects, and more.</li>
<li>Motion: documentation is hard to find. Knowledge and communication are split across many tools and locations. Code is divided across multiple codebases and services. Work is spread across multiple tools, services, and logins.</li>
<li>Waiting: tests, pre-commit hooks, CI/CD actions take too long run. Pull requests are waiting for review. A question or request for help goes unanswered and blocks progress.</li>
<li>Overproduction: ideas are easy to spin up, which exacerbates already stressed engineers. Low-value features and side projects don't have customer demand and dilute the product.</li>
<li>Overprocessing: upfront, high-fidelity design is often unnecessary and thrown away after a feature is built. A proposal goes through several layers of review and formalities.</li>
<li>Defects: bugs, typos, downtime, etc. This is one of the only wastes software teams are actually aware of and sensitive to.</li>
<li>Skills: wasted human genius and potential in any industry is common and equally damaging</li>
</ol>
<p>Once you notice something wasteful, or even something that simply bugs you, don't accept it. All too often, teams can list tons of things that frustrate them or are clunky, but never get around to doing anything about it. They defer to how things have always been and assume that's how they have to be. Teams can have incredibly high tolerance for tech and process debt, where things that drag down daily work are allowed to persist for years or never get addressed.</p>
<h2>5. Stop and fix</h2>
<p>So, everything is a process in service of a customer, every process has waste, and we should blame the process instead of the person when finding mistakes, defects, or waste. But what do we do when we find a problem?</p>
<p>We should stop and fix.</p>
<p>Yes, we should immediately stop what we're doing and fix the problem. We should prevent it from occurring again. And, others should stop what they're doing to help.</p>
<p>This sounds absurd and completely inefficient to teams when they first hear it. The thought of halting all output to fix a problem seems to threaten deadlines, burn valuable time and energy, and be wildly disruptive.</p>
<p>The main pushback I've heard is: "we don't have time to do that. We're too busy to stop everything every time a problem comes up."</p>
<p>But, the truth is: you're too busy to fix your problems because you're too busy to fix your problems.</p>
<p>The reason why you're so busy, the reason why there's no slack in the system, the reason why stopping to fix a problem would jeopardize shipping on time, is because you never address the problems that create waste, destroy value, and eat away at your capacity.</p>
<p>To illustrate this, let's talk about Toyota. Toyota assembles tens of thousands of vehicles a week. And anytime a person on the assembly line encounters an issue, they pull a chord that halts production. Everyone around immediately comes to assist the person who pulled the chord to help diagnose and fix the problem.</p>
<p>In just one of their factories with 250 assembly line workers, they do this 16,000 times in 1 week. Each person pulls the chord more than 60 times a week. And this is a company that is world-class in quality and consistency. They got this way by stopping and fixing to ensure defects don't make it past where they originally occurred.</p>
<h2>6. Make daily improvements</h2>
<p>Like stopping and fixing, this next practice receives the same excuse of "we don't have time to do that." Teams dedicate the first 30 to 60 minutes of their day to fix what bugs them and make small, incremental improvements.</p>
<p>This practice involves something called Daily 3S. 3S stands for "Sweep, Sort, Standardize."</p>
<ul>
<li>Sweep: clean the work area. This can be physical, digital, or both. As you clean, remove anything unnecessary, make things nice, and look for waste or signs of defects.</li>
<li>Sort: put things back. Reset the work area to the way it should be.</li>
<li>Standardize: make the improvement stick. If something doesn't have a dedicated place, make one. Organize or reorganize things to make them more readily accessible based on what you need in your work.</li>
</ul>
<p>This practice has immediate benefits for each day of work. It's also a great warm up to get momentum for deep work and deeper problems. And, it's the best way to learn to identify way, try ideas, and engage in creative problem solving.</p>
<p>This daily habit also prevents massive, wasteful refactors, redesigns, etc. in favor of incremental, continual improvements. And, if there isn't time for all the improvements in one morning, then the team only needs to wait 1 day until the next opportunity.</p>
<p>No improvement is too small. Many improvement ideas are quick, rough experiments. But each improvement will spawn many others, including improving an improvement. If it's better than what we have now, it's worth doing.</p>
<h2>7. Prioritize improvements</h2>
<p>With improvements that can't be made immediately, it's important to prioritize them.</p>
<p>The criteria for prioritizing improvements is:</p>
<ul>
<li>Safer</li>
<li>Better quality</li>
<li>Simpler</li>
<li>Faster</li>
</ul>
<p>Most teams jump right to "faster," thinking that will correlate to efficiency. But, faster is the last layer of optimization to make. And, fixating on making something faster can result in speeding up something that shouldn't even exist.</p>
<p>Better quality, simpler, and faster are easy to translate to software development, but safety can be harder to grasp. Safety in software actually has many important qualities:</p>
<ul>
<li>Security</li>
<li>Privacy</li>
<li>Psychological safety</li>
<li>Accessibility</li>
<li>Ergonomics</li>
</ul>
<p>Ideally, an improvement benefits safety, quality, simplicity, and speed all at once. But, we should make tradeoffs based on these priorities. If something improves safety, we should be comfortable with it being more complex and slower. Over time, we can make further improvements to capture these other qualities.</p>
<p>Don't get caught up in tracking improvement ideas, just use this ranking to determine which current idea is most important. Individuals can keep their own lists if they like, but maintaining improvement backlogs (like any backlog) is wasteful. People will remember good ideas as they encounter repeated frustrations. Ideas that don't come up again are almost always no longer relevant.</p>
<h2>8. Mistake-proof</h2>
<p>When we find the source of an issue, mistake, or defect, we should aim to prevent it in the future. This is called mistake-proofing and leads to valuable improvements.</p>
<p>There are lots of examples of mistake-proofing in software development, especially with tooling, validation, and automation. Test-driven development prevents failing code as soon as it arises. Linting errors give realtime feedback, and pre-commit hooks prevent issues from making it to CI/CD checks. Validation on form inputs guards submission.</p>
<p>Practices like code review and QA help identify and correct mistakes before they reach customers, but aren't mistake-proofing. Mistake-proofing makes the mistake impossible in the first place, or at least provides feedback as close to the mistake occurring as possible.</p>
<h2>9. Follow one-piece flow</h2>
<p>A central misconception many teams have is confusing activity for productivity.</p>
<p>This confusion leads to the wasteful and detrimental practice of having multiple projects in progress for any given team and/or individual.</p>
<p>Concurrent projects create waste through increased overhead, waiting, context switching, miscommunication, and exhaustion. These wastes cause more defects, lead to burnout, and harm both velocity and quality.</p>
<p>To address this, strive for one-piece flow, where each person, team, or even company has a single project at a time, carried through to completion, before taking on a new project.</p>
<p>Like stop and fix, one-piece flow gets a lot of initial pushback. It seems significantly slower than batching work and having multiple projects at once. But the chaos and motion of concurrent projects is an illusion: if you combined the cycle time of each project and summed it up, it will be longer, often significantly, compared to the total duration of completing those projects one at a time.</p>
<p>One-piece flow makes everything easier, especially when performed at a team level. All team members start a project at the same and build shared context as they collectively focus on a single effort. This creates pairing opportunities, eliminates waiting, handoff, and context switching, and makes work more engaging and successful. By pairing on the work together, team members offer each other continuous review, eliminating the need for PR reviews and alleviating QA significantly. This mistake-proofs much of the work, as pairing partners catch a mistake as it occurs. If a larger problem or blocker arises, everyone has shared context to help and is available to offer support.</p>
<p>Not only is one-piece flow faster and higher quality, it increases actual agility. By only starting and finishing one project at a time, the team has more frequent opportunities to propose and select the next project with a completely clean slate, based on the most recent context and experience. When multiple projects are in progress, there is perpetual work and overlapping timelines, which can lock teams into decisions for weeks, months, or even years at time before reflecting on what should come next.</p>
<p>Another way to improve one-piece flow is to work on smaller projects. Aim to reduce scope from multiple weeks, to a single week, to even a day. This increases agility further and often results in more impactful work by focusing on what's essential.</p>
<p>One-piece flow should also be done at each hierarchy of work: from initiative, to project, to task, to step in the process. It's crucial to maintain focus and reduce work-in-progress and context switching at every level.</p>
<h2>10. Achieve flow</h2>
<p>All of these practices culminate in achieving flow: the ease of work progressing smoothly, swiftly, and successfully from idea to the customer. Flow is an energizing, rewarding, and valuable state of work that leads to a happier team and more capable company.</p>
<p>Flow also involves identifying waste and issues at higher level. Central to this is understanding and spotting bottlenecks. Bottlenecks occur in every process. They're easy to spot with the waste of inventory: simply look for where work is waiting or work-in-progress is piling up more instead of one-piece flow.</p>
<p>Teams can make a damaging mistake when they become aware of bottlenecks. In attempt to get caught up, they try to push the work through the bottleneck faster, often by simply pressuring people to work harder, longer hours. This simply makes the bottleneck worse, as people are over capacity, which harms quality and can lead to complete burnout. It's disrespectful to people, blames them instead of the process, and is also the completely wrong way to resolve a bottleneck.</p>
<p>The first step in actually resolving a bottleneck is leveling production. This means reducing the throughput of all surrounding steps in the process to match the pace of the bottleneck.</p>
<p>For example, if there are a bunch of designs waiting to built, stop designing more features, and stop doing upfront design. Allow engineers to complete the work-in-progress, and when they're ready, start on a new project together with one-piece flow. This guarantees design and engineering will work at a successful pace and prevent future excess inventory.</p>
<p>The bottleneck has been eliminated once the production across stages is running evenly. Often, this will make it obvious that there is another bottleneck elsewhere. Repeat this process until the throughput of the whole system is even. Then, the overall throughput can be increased by making continuous improvements, such as stop and fix, daily 3S, and the other practices explained above, which improve speed and quality together.</p>
<p>So, as you learn to identify waste, identify what isn't flowing, adjust everything else in the process to match, and then make incremental improvements to lift up the whole process together. This will result in a calmer, happier, more efficient, engaging, and successful experience for everyone, while bringing more value than ever to customers.</p>
<h2>Wrapping up</h2>
<p>I could write an entire book on this, and have often considered doing so. Each of these 10 lessons deserve chapters of their own, and there are dozens of other lessons I haven't covered here. Let me know if you have any questions, what you're struggling with, and if you'd like me to write further on any of these topics.</p>
Aquileo | A deep dive on the UX of number inputs2025-07-01T00:00:00Zhttps://luhr.co/blog/2025/07/01/a-deep-dive-on-the-ux-of-number-inputs/<h1>A deep dive on the UX of number inputs</h1>
<p>One of my favorite things about design engineering is how deep the UX considerations can be. I love thinking through and improving every little detail of even an isolated element.</p>
<p>A surprisingly deep topic is the UX of a number input.</p>
<p>You could simply use a default <code><input type="number"></code> and be done, but there are many surrounding usability and accessibility considerations to explore.</p>
<p>First off, any interactive element needs an accessible name. So, at the very least, we need the following:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>quantity<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Quantity<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>label</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</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>quantity<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>number<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<p>Next, we have validation. There are a lot of scenarios to consider here:</p>
<ul>
<li>Is there a minimum acceptable value?</li>
<li>Is there a maximum acceptable value?</li>
<li>Should we allow decimals or enforce some other step amount?</li>
<li>How should we handle non-numeric or invalid input values?</li>
</ul>
<p>Luckily, <code><input type="number"></code> has native attributes to help with this:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>quantity<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Quantity<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>label</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</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>quantity<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>number<span class="token punctuation">"</span></span> <span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span> <span class="token attr-name">max</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>100<span class="token punctuation">"</span></span> <span class="token attr-name">step</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0.01<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<p>The <code>step</code> attribute provides further functionality. It not only enforces validation on the input's value, but it changes the increment/decrement amount of the up and down arrow keys, as well as the native "spin" buttons (increment/decrement buttons within the input) that accept clicks.</p>
<p>For example, using <code>step="0.01"</code> would allow any number up to 2 decimal places, but it comes with a usability tradeoff: we can now only increment/decrement by <code>0.01</code> at a time. This is great if users need fine-grained control over an input value, but cumbersome if users need to rapidly change the input value by whole values. Using <code>step="any"</code> can help with this, as it reverts the increment/decrement amount to the default of <code>1</code>, but allows arbitrary decimal values.</p>
<p>The <code>min</code>, <code>max</code>, and <code>step</code> attributes allow us to use the <code>:invalid</code> and <code>:valid</code> pseudo classes to provide visual feedback about the input's state. You may also want to provide feedback to the user of what kind of values are accepted:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>quantity<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Quantity<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>label</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</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>quantity<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>number<span class="token punctuation">"</span></span> <span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span> <span class="token attr-name">max</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>100<span class="token punctuation">"</span></span> <span class="token attr-name">step</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0.01<span class="token punctuation">"</span></span> <span class="token attr-name">aria-describedby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>quantity-help-text<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 attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>quantity-help-text<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Please provide a value between 0 and 100. Decimal values are allowed.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span></code></pre>
<h2>Enhancing <code>type="number"</code></h2>
<p>We can improve the UX of a native number input with some customizations.</p>
<h3>Larger increment/decrement amounts</h3>
<p>Let's say users often need to adjust the input's value by a large amount. We could increase <code>step</code> to <code>10</code>, but then users can't increment/decrement by less than 10 with the keyboard of spin buttons. Instead, we could add support for <code>Shift + ArrowUp</code> and <code>Shift + ArrowDown</code> as a larger increment/decrement interaction. We just need to add an event listener for <code>keydown</code> on the input and change the input value by <code>10</code>.</p>
<p>But, now that we're manually changing the input's value, we're now responsible for enforcing the <code>min</code> and <code>max</code> values. Let's start with the "big" increment use case (<code>Shift + ArrowUp</code>). We have a few scenarios to handle:</p>
<ol>
<li>Prevent the default behavior of incrementing the input value by 1.</li>
<li>The input's current value is already the <code>max</code> value. Don't do anything.</li>
<li>If we increment the input's value by 10, we would exceed the <code>max</code>. Set the input's value to the <code>max</code>.</li>
<li>If we increment the input's value by 10, we are below the <code>max</code>. Increment the input's value by 10.</li>
</ol>
<p>And of course, we need to handle the inverse considerations for the <code>min</code> value. Here's some pseudo code of this event handler:</p>
<pre class="language-js"><code class="language-js">numberInput<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"keydown"</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">const</span> currentValue <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token function">parseInt</span><span class="token punctuation">(</span>numberInput<span class="token punctuation">.</span>value<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> stepAmount <span class="token operator">=</span> <span class="token number">10</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>shiftKey<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>key <span class="token operator">===</span> <span class="token string">"ArrowUp"</span><span class="token punctuation">)</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> maxValue <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token function">parseInt</span><span class="token punctuation">(</span>numberInput<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">"max"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>currentValue <span class="token operator">===</span> maxValue<span class="token punctuation">)</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">if</span> <span class="token punctuation">(</span>currentValue <span class="token operator">+</span> stepAmount <span class="token operator">></span> maxValue<span class="token punctuation">)</span> <span class="token punctuation">{</span>
numberInput<span class="token punctuation">.</span>value <span class="token operator">=</span> maxValue<span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
numberInput<span class="token punctuation">.</span>value <span class="token operator">=</span> numberInput<span class="token punctuation">.</span>value <span class="token operator">+</span> stepAmount<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">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>key <span class="token operator">===</span> <span class="token string">"ArrowDown"</span><span class="token punctuation">)</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> minValue <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token function">parseInt</span><span class="token punctuation">(</span>numberInput<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">"min"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>currentValue <span class="token operator">===</span> minValue<span class="token punctuation">)</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">if</span> <span class="token punctuation">(</span>currentValue <span class="token operator">-</span> stepAmount <span class="token operator"><</span> minValue<span class="token punctuation">)</span> <span class="token punctuation">{</span>
numberInput<span class="token punctuation">.</span>value <span class="token operator">=</span> minValue<span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
numberInput<span class="token punctuation">.</span>value <span class="token operator">=</span> numberInput<span class="token punctuation">.</span>value <span class="token operator">-</span> stepAmount<span class="token punctuation">;</span>
<span class="token keyword">return</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>Using <code>keydown</code> instead of <code>keyup</code> is another UX consideration. Native number inputs respond to <code>keydown</code>, which allows key repeat to quickly change the input value without having to repeatedly press a key. We want to match this behavior with our event listener.</p>
<h3>Smaller increment/decrement values</h3>
<p>We can also add functionality in the other direction by allowing even finer-grained incrementing and decrementing. We can follow the same approach as above with a dedicated keyboard shortcut, say <code>Control + ArrowUp</code> and <code>Control + ArrowDown</code>, and update the input value by a small amount (<code>0.1</code>, <code>0.01</code>, etc.).</p>
<p>Having multiple levels of granularity can create a very fluid and efficient interaction pattern for rapidly adjusting a value to a precise target.</p>
<h3>Replacing native spin buttons</h3>
<p>Native number inputs have "spin" buttons, which are the small increment/decrement buttons nested inside the input.</p>
<p>While these dedicated controls are a nice consideration, they have incredibly small tap targets which are difficult to use. We can replace these with our own increment and decrement buttons.</p>
<p>First, we need to hide the native spin buttons. We can use <code>appearance: textfield</code> to achieve this.</p>
<p>Next, let's provide some dedicated <code><button></code> elements:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>quantity<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Quantity<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>label</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</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>quantity<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>number<span class="token punctuation">"</span></span> <span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span> <span class="token attr-name">max</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>100<span class="token punctuation">"</span></span> <span class="token attr-name">step</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0.01<span class="token punctuation">"</span></span> <span class="token attr-name">aria-describedby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>quantity-help-text<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>button</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 punctuation">></span></span>Decrease quantity<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>button</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 punctuation">></span></span>Increase quantity<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span></code></pre>
<p>Like our <code>Shift + ArrowUp</code> and <code>Shift + ArrowDown</code> event handlers, the <code>click</code> event handler for these buttons needs to respect the <code>min</code> and <code>max</code> values. Luckily, the <code>click</code> event handler fires on <code>keydown</code> for <code>Enter</code> and <code>Space</code>, so holding a key will rapidly change the value.</p>
<p>Lastly, we should consider tab order for these controls. Visually, we may want to have the decrease button, input, and increase button appear in that order. While it's best to have visual order and tab order match, it may create a confusing user experience if the first control to receive focus is a decrease button, and not an input with an accessible name. For that reason, I think it's worth the tradeoff of placing the increment and decrement buttons after the number input in the DOM, and using CSS to change their visual order. This way, the user first tabs into the number input and then proceeds the increment/decrement controls.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/tM5DE-NXRI-320.avif 320w, https://luhr.co/assets/images/generated/tM5DE-NXRI-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/tM5DE-NXRI-320.webp 320w, https://luhr.co/assets/images/generated/tM5DE-NXRI-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/tM5DE-NXRI-320.jpeg" alt="The focus order for a custom number input with custom spin buttons used on the Buffer.com pricing page go from input, to decrement button, to increment button." loading="lazy" decoding="async" width="640" height="343" srcset="https://luhr.co/assets/images/generated/tM5DE-NXRI-320.jpeg 320w, https://luhr.co/assets/images/generated/tM5DE-NXRI-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>The focus order for a custom number input I built for <a href="https://buffer.com/pricing">Buffer.com's pricing page</a> goes from the input, to the decrement button, to the increment button.</figcaption>
</figure>
<p>It could also be argued that these increment/decrement buttons are primarily tap targets and should be excluded from the tab order. In that case, they could use <code>tabindex="-1"</code> so the number input is the only focusable control.</p>
<h3>Always working state</h3>
<p>If the number input is used in a user-submittable form, it's best to provide visual and text feedback if the input's value is invalid. This allows the user to correct the value to be able to submit the form.</p>
<p>However, if the number input is part of an interactive control for something on the current page, we can improve the user experience by keeping the input in an always valid state.</p>
<p>Natively, <code><input type="number"></code> allows the user to type any character, and simply enters an <code>:invalid</code> state if the value is non-numeric and doesn't respect the <code>min</code>, <code>max,</code> and <code>step</code> attributes.</p>
<p>To improve on this, we could prevent invalid key presses from altering the input's value. As a user, this may cause some initial confusion, but can teach the user what is valid input.</p>
<p>We could also immediately correct values outside of our desired range, such as increasing a value that is below our <code>min</code> to the min value. However, this can be a frustrating user experience when editing a field. If our <code>min</code> is <code>10</code>, the user may want to delete the input value to type <code>23</code>, but are prevented from doing that if they type <code>2</code> on its own. THis forces users to come up with frustrating workarounds, such as temporarily typing <code>123</code>, moving to the first digit, and deleting <code>1</code> to input <code>23</code>. Preventing this issue becomes its own rabbit hole. Do we debounce the input value before correcting the value? What debounce length is appropriate?</p>
<p>Alternatively, we can correct the input's value on <code>focusout</code> or <code>Enter</code>. This allows the user to input any value, and then corrects the value once focus has left the field or the user has "submitted" the value. If the user increments or decrements the value, we still want to update the interface immediately. But, if the value is invalid, we don't do anything until <code>focusout</code> or <code>Enter</code> and then convert the value to something sensible:</p>
<ul>
<li>If the value is non-numeric, revert to the default value (or, the last valid value if there isn't a default)</li>
<li>If the value is larger than the <code>max</code>, reduce it to the <code>max</code></li>
<li>If the value is less than the <code>min</code>, increase it to the <code>min</code></li>
</ul>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/Upf6L_MZcL-320.avif 320w, https://luhr.co/assets/images/generated/Upf6L_MZcL-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/Upf6L_MZcL-320.webp 320w, https://luhr.co/assets/images/generated/Upf6L_MZcL-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/Upf6L_MZcL-320.jpeg" alt="A number input that converts non-numeric values to the last valid number value." loading="lazy" decoding="async" width="640" height="367" srcset="https://luhr.co/assets/images/generated/Upf6L_MZcL-320.jpeg 320w, https://luhr.co/assets/images/generated/Upf6L_MZcL-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>A custom number input I built for a design tool that converts invalid values into the the last valid number value.</figcaption>
</figure>
<h3>Allowing math expressions</h3>
<p>For more advanced web apps, such as a design tool, allowing users to enter math expressions in a number input saves them the effort of opening up a dedicated calculator, keeping them in the flow. This enhancement adds a lot of implementation complexity, but can be a major UX upgrade in situations where math is frequently needed.</p>
<p>Similar to keeping the input in an always working state, we would want to wait for <code>focusout</code> or <code>Enter</code> to evaluate the expression. When we do our validation checks, we also need to parse the input's value to check if it's a valid math expression. If it is, we evaluate the expression (think PEMDAS), then use the resulting value for our remaining checks (enforcing <code>min</code>, <code>max</code>, and <code>step</code>).</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/g625LUkWCC-320.avif 320w, https://luhr.co/assets/images/generated/g625LUkWCC-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/g625LUkWCC-320.webp 320w, https://luhr.co/assets/images/generated/g625LUkWCC-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/g625LUkWCC-320.jpeg" alt="A number input that accepts math expressions and converts the result to a valid value." loading="lazy" decoding="async" width="640" height="367" srcset="https://luhr.co/assets/images/generated/g625LUkWCC-320.jpeg 320w, https://luhr.co/assets/images/generated/g625LUkWCC-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>The design tool custom number input also accepts math expressions. The expressions are evaluated to produce the number input's value.</figcaption>
</figure>
<h2>When to go beyond <code>type="number"</code></h2>
<p>All together, <code><input type="number"></code> is a good fit if users need to select or provide a value from a discrete range. But, in some cases, such as a phone number, incrementing and decrementing the value isn't useful or expected behavior.</p>
<p>In this case, we may want to use <code><input type="text"></code>. This means we'll have complete control over the input's functionality, but we also have to manually handle all validation and functionality.</p>
<p>This includes rolling our own versions of <code>min</code>, <code>max,</code> and <code>step</code> and the resulting validation states.</p>
<p>We also need to use <code>inputmode="numeric"</code> or <code>inputmode="decimal"</code> to bring up the appropriate keyboard on mobile devices, and <code>pattern</code> to enforce input value rules with Regex.</p>
<h2>Custom formatting</h2>
<p>Whether using <code>type="number"</code> or <code>type="text"</code>, it may be helpful to provide automatic formatting, such as adding separators to large values for readability or adding hyphens, spaces, and/or parentheses to common formats such as phone, postal, credit card, or ID numbers.</p>
<p>This can either be done dynamically as the input value changes or on <code>focusout</code> or <code>Enter</code>.</p>
<p>With dynamic formatting, it's important to advance the cursor properly to allow the user to continue typing as expected. Similarly, if the user deletes a character, it may be helpful to automatically remove any adjacent formatting characters. Basically, automatic formatting should be transparent with the typing experience: don't make users move the cursor around automatic formatting characters or be responsible for deleting them.</p>
<p>With formatting on <code>focusout</code> or <code>Enter</code>, the user is allowed to type as expected without interference, and automatic formatting is only applied once they're done. Similar to dynamic formatting, the formatting characters shouldn't affect editing or typing and be purely presentational.</p>
<h2>There's probably more to consider</h2>
<p>Even with all this in mind, there are probably other UX wrinkles to smooth and enhancements to provide. Much of this is use case dependent, and all of it benefits from testing with a wide variety of users, especially with a variety of assistive technology and devices.</p>
<p>The depth of a single number input shows just how far design engineering can go. It's this level of thoughtfulness and refinement that makes this my favorite kind of work to do.</p>
Aquileo | Tradeoffs with quality, scope, and deadlines2025-04-02T00:00:00Zhttps://luhr.co/blog/2025/04/02/tradeoffs-with-quality-scope-and-deadlines/<h1>Tradeoffs with quality, scope, and deadlines</h1>
<p>Every project involves tradeoffs with quality, scope, and deadlines.</p>
<p>Tradeoffs happen whether you decide or want them to. Strategy is deciding which tradeoffs to make. The default mode for most teams is a lack of strategy with tradeoffs happening to them.</p>
<p>So, we have these 3 interdependent variables of quality, scope, and deadlines.</p>
<p>Teams typically operate in 1 of 2 ways.</p>
<h2>1. Fixed deadline, fixed scope, variable quality</h2>
<p>With a fixed deadline and fixed scope, the only variable that can give is quality. It goes down.</p>
<p>This is the damage of upfront design and hard deadlines. You've committed to a fixed scope before the build has even kicked off.</p>
<p>As a result, only quality can suffer. Either the current project has immediately worse quality from rushing to make the deadline, or long-term quality suffers as tech debt piles up and the team is burnt out from too much crunch and overwork.</p>
<h2>2. Fixed scope, fixed quality, variable deadline</h2>
<p>With a fixed scope and fixed quality, the only variable that can give is the deadline. It goes long.</p>
<p>This means that while we have good intentions of only shipping high quality work, we have no predictability of when we'll ship it.</p>
<p>We move slowly, work takes several times longer than expected, the team gets demoralized, and the product becomes stale and irrelevant.</p>
<p>This is the default mode of waterfall companies and is typical of things like government contractors where strict standards and the scope of the work are locked in from the beginning.</p>
<p>But, there's a third way we can operate: Fixed deadline, fixed quality, variable scope.</p>
<h2>3. Fixed deadline, fixed quality, variable scope</h2>
<p>With a fixed deadline and variable, the only variable that can give is the scope. It gets smaller.</p>
<p>Instead of doing worse work or taking longer than we like, we simply do less, but better.</p>
<p>This smaller amount of work is the most essential and captures the majority of the value. It maintains our quality standards, which respects our customers and protects our reputation. We can always come back and add more to it, but usually, it fully address the problem.</p>
<p>Often, this more focused is even better than the full scope would have been. We've forced ourselves to make tradeoffs, hone in on what's valuable, and cut anything that isn't.</p>
<p>With this approach, we're able to move quickly, have a consistent shipping cadence, produce high quality work we're proud of, and stay motivated and energized.</p>
<p>We now have reliable deadlines that mean something. We've capped the potential downsides of building the wrong thing or making the wrong bet. We've retained all future optionality because we haven't created maintenance or scaling issues.</p>
<p>And, we're now able to have a clear mind to ask what's next and begin the process over again.</p>
Aquileo | Analog productivity2024-11-18T00:00:00Zhttps://luhr.co/blog/2024/11/18/analog-productivity/<p><picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/lxnzBSSsti-320.avif 320w, https://luhr.co/assets/images/generated/lxnzBSSsti-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/lxnzBSSsti-320.webp 320w, https://luhr.co/assets/images/generated/lxnzBSSsti-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/lxnzBSSsti-320.jpeg" alt="Walnut Analog card stand with matching pen tray and clear hourglasses with black sand on walnut desk." loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/lxnzBSSsti-320.jpeg 320w, https://luhr.co/assets/images/generated/lxnzBSSsti-640.jpeg 640w" sizes="(min-width: 100px)" /></picture></p>
<h1>Analog productivity</h1>
<p>Lately I've been shifting my day-to-day productivity process to more physical and analog tools.</p>
<p>I still love a <a href="https://luhr.co/blog/2023/04/19/my-custom-second-brain-setup-part-1-why-go-custom/">digital second brain for personal knowledge management</a>, and it's still <a href="https://luhr.co/blog/2024/09/02/markdown-files-as-the-digital-workplace/">the best digital workplace</a> I've experienced.</p>
<p>But, <a href="https://gitjournal.io/">GitJournal</a>, the app I used to sync and edit my second brain on my phone, has had some Git diffing issues for a while now. As a result, I haven't been able to access my second brain on my phone. This has really tanked my use of it, especially for task management.</p>
<p>Open to switching things up, I got a pack of <a href="https://ugmonk.com/pages/analog">Analog</a> cards by <a href="https://ugmonk.com/">Ugmonk</a> in a recent order and was excited to try a different task management approach.</p>
<h2>Physical task management</h2>
<p>I've been a fan of <a href="https://ugmonk.com/">Ugmonk</a> for years now. In 2021, we partnered with them to provide the beautiful example product imagery for the <a href="https://tailwindui.com/components#product-ecommerce">Tailwind UI Ecommerce components</a> I built.</p>
<p>After trying my first pack of <a href="https://ugmonk.com/pages/analog">Analog cards</a> for a week, I really liked the thoughtful design, simplicity, and always-present nature of having my task list visible on my desk instead of tucked behind another window on my screen.</p>
<p>I felt very focused and productive, motivated by checking off each item in front of me. It's ridiculous how filling in an empty circle can be motivating, but I'll take it.</p>
<p>After this trial period, I decided to order the actual <a href="https://ugmonk.com/products/analog-starter-kit?variant=36903475150998">Analog starter pack</a>, which comes with 3 refill packs and a walnut card stand. I also went for <a href="https://ugmonk.com/products/pen-tray-walnut?variant=40503455744150">the matching walnut pen tray</a> for the best pen ever, the <a href="https://www.unibrands.co/collections/rollerball-pens/products/roller-rollerball-pens?variant=39362289795278">uniball Roller 0.5mm</a>. Fight me.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/Xb_CMvj5VB-320.avif 320w, https://luhr.co/assets/images/generated/Xb_CMvj5VB-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/Xb_CMvj5VB-320.webp 320w, https://luhr.co/assets/images/generated/Xb_CMvj5VB-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/Xb_CMvj5VB-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/Xb_CMvj5VB-320.jpeg 320w, https://luhr.co/assets/images/generated/Xb_CMvj5VB-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Walnut Analog card stand and matching pen tray.</figcaption>
</figure>
<p>True, I can make a task list on an index card and skip the walnut accessories. But there's value in having nice objects to look at throughout the work day, and to appreciate good design while crafting things in my own work. I spend at least half of my waking life in my home office, so it's important to me to enjoy my space and only have objects that bring me joy.</p>
<h2>Hourglass pomodoro</h2>
<p>As part of leaning in to physical productivity tools, I also purchased some hourglasses to do <a href="https://todoist.com/productivity-methods/pomodoro-technique">the Pomodoro Technique</a>.</p>
<p>Since a Pomodoro interval is 25 minutes, I ordered 2 hourglasses by <a href="https://hightidestoredtla.com/">Hightide</a>: a <a href="https://hightidestoredtla.com/products/hourglass-ll-wide-amber?variant=15786386325570">30 minute hourglass</a> and a <a href="https://hightidestoredtla.com/products/hourglass-m-ambar?variant=15786344513602">5 minute hourglass</a>.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/CIO4Y7wzTF-320.avif 320w, https://luhr.co/assets/images/generated/CIO4Y7wzTF-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/CIO4Y7wzTF-320.webp 320w, https://luhr.co/assets/images/generated/CIO4Y7wzTF-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/CIO4Y7wzTF-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/CIO4Y7wzTF-320.jpeg 320w, https://luhr.co/assets/images/generated/CIO4Y7wzTF-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>30 and 5 minute clear hourglasses with black sand.</figcaption>
</figure>
<p>At the start of each interval, I flip both hourglasses over.</p>
<p>The 5 minute hourglass is for my break, which I use to get water, walk around, do some stretches, or do some workout sets with the kettlebell, maces, and olympic rings I have in my office.</p>
<p>When the 5 minute hourglass is done, the remaining 25 minutes of the 30 minute hourglass is for work. When that hourglass is done, I repeat the interval.</p>
<p>I verified these hourglasses are accurate to within a few seconds, which is pretty impressive.</p>
<h2>Erasable paper</h2>
<p>My design process typically involves physical UI sketches and then immediately jumping into code to design the browser. I like to annotate these UI sketches with notes, questions, and other ideas to get most of my ideas and explorations out before switching to digital tools.</p>
<p>I've used these <a href="https://www.swipi.es/">erasable sheets by Swipies</a> for about 7 years now and they've helped up perfectly. The <a href="https://www.swipi.es/collections/build-your-own-kit/products/staedtler-pen">Staedtler fine point pens</a> allow me to be detailed in my sketches. I'll then take a photo of the sheet to reference later, and erase it using some water.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/o4faVvmsbG-320.avif 320w, https://luhr.co/assets/images/generated/o4faVvmsbG-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/o4faVvmsbG-320.webp 320w, https://luhr.co/assets/images/generated/o4faVvmsbG-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/o4faVvmsbG-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/o4faVvmsbG-320.jpeg 320w, https://luhr.co/assets/images/generated/o4faVvmsbG-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Erasable paper sheets with dot grid and a fine point felt pen.</figcaption>
</figure>
<p>I also use these sheets around the house for taking measurements, sketching interior design ideas, and woodworking, so I keep them on a clipboard.</p>
<h2>My analog productivity takeaways</h2>
<p>This shift has created some nice benefits to my work. I used to do a lot of drawing and oil painting, so returning to more physical thinking engages more of my creative brain. It also adds some nice variety to my fully digital career.</p>
<p>It's worth it to invest in well-designed objects. They make my workspace look nicer, offer mini breaks from screen time, add a more tactile experience to my day, and give my eyes something more interesting to gaze at while thinking. I'm a minimalist, so whatever items I do own should add value to my life and be well made. And, as someone who does woodworking and used to do packaging design, it's nice to have thoughtfully made objects around as inspiration.</p>
<p>After using this setup for the past couple weeks, I'm excited to keep going along this path, and I'll provide updates with any iterations.</p>
Aquileo | Is the best digital workplace just a bunch of Markdown files?2024-09-02T00:00:00Zhttps://luhr.co/blog/2024/09/02/markdown-files-as-the-digital-workplace/<h1>Is the best digital workplace just a bunch of Markdown files?</h1>
<p>For over a year, I've been working on a major project with a close collaborator. This project has all the complexities of a full product business, meaning we need to define and prioritize what we work on, document our standards, and capture our day-to-day thinking as we work.</p>
<p>There are endless tools that try to create the perfect digital workplace, with all-in-one tools such as Basecamp or Campsite, or communication-specific tools like Notion, Slack, Paper, etc.</p>
<p>After the past year, we've crafted the best digital workplace I've experienced. It has 3 simple properties:</p>
<ol>
<li>A bunch of markdown files</li>
<li>Neatly organized in folders</li>
<li>Inside the codebase</li>
</ol>
<p>This approach is absurd—and it works better than any other tool or remote team I've worked with.</p>
<p>Let's dig into how our folders and files are structured, the benefits of this approach, and the intentional tradeoffs it creates.</p>
<h2>Second brain as the workplace</h2>
<p>I've been a fan of <a href="https://luhr.co/blog/2023/04/19/my-custom-second-brain-setup-part-1-why-go-custom/">building a second brain for over 2 years now</a>, and it's transformed how I manage my personal and professional knowledge and productivity. I capture all my thinking, learning, and reference into a simple markdown file and folder structure that has scaled to several thousand notes.</p>
<p>A second brain is organized into top-level folders with the <a href="https://fortelabs.com/blog/para/">PARA method, which stands for "Projects, Areas, Resources, Archives"</a>. This approach organizes notes based on actionabilty: Projects are current efforts with a strict work-in-progress limit, Areas of responsibility are ongoing efforts with standards, Resources are reference not tied to a specific effort, and Archives store any past notes from these top-level folders.</p>
<p>Turns out, the PARA method is well-suited for a digital workplace. Companies have projects, areas (business functions, teams, practices, etc.), resources (documentation, reference, notes, etc.), and archives of past work.</p>
<p>So, we added a root-level <code>_second-brain</code> folder with the PARA structure <em>inside</em> our codebase. This centralized everything we do into a single, version controlled repository.</p>
<p>We also do <a href="https://basecamp.com/shapeup">Shape Up</a>, which is our favorite way to decide what to work on and de-risk potential projects. When I learned about the concept of the <a href="https://www.fractalproductivity.club/p/paramore-5-paria-unlocking-the-incubator">Incubator from Fractal Productivity</a>, I knew this was the perfect concept to house Shape Up within our second brain.</p>
<p>Here's a breakdown of our full second brain structure:</p>
<ul>
<li><code>00-inbox</code>
<ul>
<li>Default home for capturing new notes without worrying about where to put them
<ul>
<li>Perform routine cleaning to keep the inbox tidy</li>
</ul>
</li>
<li><code>00-tasks</code>
<ul>
<li>Unplanned work (seek to keep this to a minimum)</li>
<li>Tasks not tied to active projects (day-to-day work)</li>
</ul>
</li>
<li><code>01-improvements</code>
<ul>
<li>A log of any potential improvement ideas, perfect for Daily 3S (more on this in the future)</li>
</ul>
</li>
<li><code>02-scratchpad</code>
<ul>
<li>An outlet for thinking notes and quick/initial ideas to help stay focused on current tasks</li>
<li>Informal and no commitment to potential projects or tasks</li>
<li>Can create dedicated files for company-wide, team-wide, or per-person use</li>
</ul>
</li>
</ul>
</li>
<li><code>01-projects</code>
<ul>
<li>Used for active projects</li>
<li>Contains all notes related to active work</li>
<li>Strict WIP limits (ideally 1 per team)</li>
</ul>
</li>
<li><code>02-incubator</code>
<ul>
<li>Used for Shape up with framing, shaping, and betting stages</li>
<li>Not a backlog! Potential projects are nothing more than ideas until we bet on them.</li>
<li><code>00-framing</code>
<ul>
<li>Projects that need to be validated for business value and relevant timing</li>
<li>Archived if they don't move on to <code>01-shaping</code></li>
</ul>
</li>
<li><code>01-shaping</code>
<ul>
<li>Projects that have strategic value but need to be defined and de-risked</li>
<li>Archived if they don't move on to <code>02-betting</code></li>
</ul>
</li>
<li><code>02-betting</code>
<ul>
<li>Projects that are defined and de-risked but need to be committed to</li>
<li>Archived if they aren't bet on</li>
<li>Projects we bet on are promoted to <code>01-projects</code> when they become active work</li>
</ul>
</li>
</ul>
</li>
<li><code>03-areas</code>
<ul>
<li>Areas of responsibility, ongoing efforts, and teams/departments</li>
<li>Standard operating procedures by area or practice</li>
</ul>
</li>
<li><code>04-resources</code>
<ul>
<li>Research</li>
<li>Learning notes</li>
<li>Documentation</li>
<li>Reference</li>
<li>Handbook</li>
<li>Operations</li>
</ul>
</li>
<li><code>05-archive</code>
<ul>
<li><code>01-projects</code>
<ul>
<li>Completed projects</li>
</ul>
</li>
<li><code>02-incubator</code>
<ul>
<li><code>00-framing</code></li>
<li><code>01-shaping</code></li>
<li><code>02-betting</code></li>
<li>Projects that didn't make it through Shape Up</li>
<li>Helpful to know which stage they were archived</li>
</ul>
</li>
<li><code>03-areas</code>
<ul>
<li>Business functions or practices that are no longer relevant</li>
</ul>
</li>
<li><code>04-resources</code>
<ul>
<li>Resources that are outdated or no longer applicable</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>Benefits</h2>
<p>The many benefits of this approach include simplicity, flexibility, cost, security, transparency, centralization, lightness, clarity, and calmness.</p>
<h3>Simplicity</h3>
<p>The second brain workplace is just markdown files and folders. Files load instantly and are easy to traverse, search, create, update, move, and delete. Markdown has a simple, readable syntax that is easy to learn. There's no custom engineering, infrastructure, or maintenance required.</p>
<p>With simple files and folders, there's no vendor lock-in. Your digital workplace isn't dependent on other companies surviving. There are no proprietary file formats, unexpected changes in features or functionality, or software rot to deal with.</p>
<h3>Flexibility</h3>
<p>The simplicity of the second brain structure makes it quite flexible and scalable. It's easy to organize and reorganize files and folders as the organization and work evolve.</p>
<p>For smaller teams, it's best to have the root-level <code>01-projects</code> represent all projects across the organization, with a global work in progress limit. This helps with transparency and creates a more unified focus for the organization.</p>
<p>For larger teams, it's possible to organize projects by area (team or business function), while still setting work-in-progress (WIP) limits per area.</p>
<p>Because everything is just text in a file, it's easy to create new conventions and patterns (which we'll explore below) without having to depend on a vendor to add requested features.</p>
<p>The format of markdown files and folders offers a lot of individual flexibility, as well. Team members are free to use whatever text editor they like, which can be customized with different themes, shortcuts, extensions, snippets, and more. These customizations can be out of preference, need (accessibility), or both. This second brain digital workplace is adaptive instead of only offering whatever features and options a vendor decides to build.</p>
<h3>Cost</h3>
<p>This approach can be completely free. All the team needs is a open source text editor, Git, and a version control host such as GitHub or GitLab with free private repositories.</p>
<h3>Security</h3>
<p>Git with SSH credentials is very secure, especially with 2-factor authentication for the version control host. If this approach works for the software a business runs on, it should work well for the company's digital workplace.</p>
<p>Large companies might have an on-prem or self-hosted version control system, which would make this approach even more secure.</p>
<p>Contrast this with trusting another company to secure and preserve your team's internal communications, which might contain proprietary and sensitive information.</p>
<h3>Transparency</h3>
<p>Having all communications in a centralized, accessible format makes transparency much more attainable. Communications aren't siloed by varying tools or permissions, and team members can easily peruse information across the team.</p>
<p>If this sounds like a tradeoff, then you likely have a cultural issue of secrecy and distrust.</p>
<h3>Centralization</h3>
<p>This second brain centralizes all relevant information into a simple digital workplace, which increases access and reduces context switching. This helps eliminate waste by making it obvious where information lives (in one spot) and making information quicker to find.</p>
<p>For teams with a codebase, the second brain is even more powerful when it's embedded directly in the main repository. This creates a direct connection between the communication and thinking of a team with the product being built. It also helps achieve the lean principle of "The answer is where the question is asked" by putting all necessary information in the same place as the work being done.</p>
<h3>Lightness, clarity, and calmness</h3>
<p>This way of working creates ease and quiet in a team.</p>
<p>It's asynchronous, making fewer demands of individual's attention and time. There are no notifications, alerts, or pings. Everyone can access the information they need, reflect on ideas and proposals, and contribute their considered thoughts.</p>
<h2>Tasks</h2>
<p>At the most granular level of work, teams manage countless tasks for tracking day-to-day efforts.</p>
<p>With Markdown, it's easy to create task lists that can replicate many of the features of task software with simple text patterns:</p>
<ul>
<li>Incomplete task: <code>- [ ] Task title</code></li>
<li>Task with deadline: <code>- [ ] yyyy-mm-dd | Task title</code></li>
<li>Task assigned to someone: <code>- [ ] @assigneeName (individual or team) | Task title</code></li>
<li>Task with deadline and assignment: <code>- [ ] yyyy-mm-dd | @assigneeName | Task title</code></li>
<li>In progress task <code>- [/] Task title</code></li>
<li>Optional task <code>~ [ ] Task title</code></li>
</ul>
<p>With these text patterns, it's easy to do searches in the second brain to find various types of tasks. These searches can be global or scoped to specific files and folders, which is helpful for excluding archived files. They can even be pinned in tools like VS Code for recurring searches.</p>
<p>Example searches include:</p>
<ul>
<li>To do (<code>- [ ]</code>)</li>
<li>In progress (<code>- [/]</code>)</li>
<li>Complete (<code>- [x]</code>)</li>
<li>Tasks with dates</li>
<li>Tasks by person or team (<code>@</code> syntax)</li>
</ul>
<p>Optional tasks don't appear in these searches by design, because they aren't necessary. But, it's easy to search for them: (<code>~ [ ]</code>).</p>
<h2>My experience applying this approach</h2>
<p>I've been using this approach for over a year now as I'm building a product with a close collaborator.</p>
<p>It's scaled well across several cycles and projects, many research efforts, countless tasks, and a steady stream of daily ideas, thoughts, and improvements.</p>
<p>Having the digital workplace in the same repo as the code is very natural and eliminates all overhead. You can spend your day in the same tool, reference information with ease, and don't have to manage multiple logins and notification feeds.</p>
<p>This structure also allows for very fluid pairing and contributions. When pairing on work, the driver can stay focused on the task, while the navigator can note future ideas, improvements, and considerations. By practicing the lean principle of "stop and fix", it's also easy to create just-in-time documentation and improve existing notes.</p>
<p>As a big fan of Shape Up, it's felt natural and productive to carry out the full shaping process in the root-level <code>02-incubator</code> folder, promote active projects to <code>01-projects</code>, and archive completed work in <code>05-archives</code>.</p>
<p>For quick coordination and ephemeral communication, we just text each other. This only happens a couple times per week outside of scheduled pairing sessions, where the bulk our work and communication happen. We can easily upgrade this to a tool like Discord or Slack in the future, albeit with very limited use cases.</p>
<h2>Tradeoffs</h2>
<p>This approach has its tradeoffs, but they are minor. Some are also intentional advantages.</p>
<p>One piece of anticipated pushback is this approach requires everyone to learn Markdown and Git. I'd argue there's a learning curve to any tool, and these skills are much more widely useful and transferable than learning the quirks of a specific workspace tool that won't be around in 5 years or won't be used in a future role. Markdown is easy to learn within a session or two, and while Git can be quite complex, a second brain typically only requires adding files, committing them, and pushing/pulling. I wouldn't encourage using branches or other advanced version control approaches. This can also be a form of skill-leveling across a team, by giving non-developer team members some bridging skills. If the second brain is colocated in the main codebase, this also gives team members the ability to explore and learn how things are being built.</p>
<p>There isn't a way to make fancy content blocks or layouts, such as tables with filters, data plots, Gantt charts, etc. This is fine for me as I prefer lightweight, simple text over complex visuals, and the time invested in crafting the perfect Notion template or a color-coded multi-view database is largely waste.</p>
<p>There's no great way to handle images. We rarely need to include images, so our solution is to just put them in a folder colocated with a note. Text is lighter, more accessible, and editable, so images are often unnecessary.</p>
<p>Internal linking is clunky between notes, especially if relative paths change as files and folders are moved. Wiki links would be great, but would involve more complex tooling. Luckily, linking between notes hasn't been a frequent need.</p>
<p>There isn't a great solution for mobile access. GitJournal for Android is one example that works decently for a second brain, and there are likely equivalents for iOS. But it's probably healthier to not have the workplace accessible from a phone in the first place.</p>
<p>Same-document, real-time collaboration isn't possible. This is solvable by having a designated scribe during calls who is sharing their screen, similar to pairing. Collaboration in general is a solved problem, since Git resolves conflicts and companies can obviously develop massive codebases at scale using the same tools and practices.</p>
<p>As mentioned above, this approach is not well suited for quick coordination and ephemeral communication. That's better handled by a chat tool, and that chat tool's purpose is greatly clarified to only be used for a quick question or reminder. Everything else should live in the second brain.</p>
<p>Lastly, this approach doesn't allow private communication or private personal notes. Direct communication might be better handled by a chat tool, and anything beyond quick coordination should ideally be part of the second brain.</p>
<p>Personal notes can be handled by team members maintaining their own second brain for their role. This is actually how I came up with the idea of using a second brain at a team level, as I've created a second brain for each long-term client engagement and my current full-time role. A personal second brain for my role has been essential for making sense of the chaotic communications that occur across teams in many different workplace tools. Having all communications in a team-wide second brain would greatly reduce the need for personal notes.</p>
<p>It's also possible to create files and folders for specific teams and individuals if the information doesn't need to be private. For example, it might be useful for each team member to have their own scratchpad for in-the-moment thoughts or thinking notes.</p>
<h2>Wrapping up</h2>
<p>I'm confident there's value in this approach and the past year of using it for a major project only makes me more excited to keep going.</p>
<p>It's strange that this approach would be considered radical because of its simplicity. Most organizations believe they need tons of complexity, almost as evidence of sophistication, and never reflect on what's essential. Most organizations aren't aware of the waste caused by having several heavy, cumbersome, and expensive communication tools as their digital workplace. There is so much time, money, and effort over-invested in these tools that are really just glorified text documents. There's a lot of value in cutting away all the unnecessary and just managing a simple file and folder structure.</p>
<p>I'm interested in hearing your thoughts on this approach, especially if you try it for yourself or your team. Let me know what you think: hello@luhr.co</p>
Aquileo | The next step in my career: joining Buffer as Senior Design Engineer2024-04-09T00:00:00Zhttps://luhr.co/blog/2024/04/09/the-next-step-in-my-career-joining-buffer-as-senior-design-engineer/<h1>The next step in my career: joining Buffer as Senior Design Engineer</h1>
<p>Over 5 years ago, I created <a href="https://www.youtube.com/@buildux">Build UX</a> to provide free, high-quality educational content on building accessible and polished user experiences. And nearly 4 years ago, I started consulting full-time in the summer of 2020. Over these past few years, I've loved the flexibility, interesting problems, and scaled impact of being fully independent.</p>
<p>So, it may come as a surprise that I've chosen to return to full-time employment. But, I have an opportunity that I can't pass up with many of the same benefits I've enjoyed as an independent consultant.</p>
<h2>Why I'm stepping away from full-time consulting</h2>
<p>The opportunities to guide my clients through technical, process, and cultural transformations have made the past few years the highlight of my career, but running your own consulting business is not without its challenges.</p>
<p>Following the collapse of Silicon Valley Bank early last year, the tech industry was in a reactionary and panicked state. Nearly every consultant and educational content creator I know, including myself, experienced a significant drop in demand through much of last year.</p>
<p>While last year was rewarding in its own way as I explored my potential future product business, it was unpredictable and challenging financially. Luckily, demand started recovering last fall as confidence started returning to the industry, and I was returning back to normal levels of engagement.</p>
<p>At the same time demand started returning, I found the single opportunity that started calling me away from consulting: joining the team at <a href="https://buffer.com/">Buffer</a>.</p>
<h2>Why I'm joining Buffer</h2>
<p>I've followed Buffer for years through their continuous experiments with their way of working and passion for doing what's right. In past leadership positions, I used Buffer as a leading example in <a href="https://buffer.com/open">transparency with open salaries</a> and equitable pay. In consulting, Buffer has served as a trusted point of reference that teams can and should question assumptions and continuously improve their culture, process, and product, with practices such as 4-day work weeks, embracing side projects, and building for the long-term.</p>
<p>Back in December, I was reading a post and was curious what roles they typically hire for. I saw an opening for a completely new design engineer role, which aligned with my whole career as both a designer and developer. Without hesitation, I decided on the spot to apply with the hope that I could join a team I've admired for years.</p>
<p>I'm not an impulsive person, especially when it comes to my career. But, I can be compelled to move fast and when good opportunities appear, and this was one of them.</p>
<p>Buffer's job openings can be quite competitive: a previous job posting in the same team had around 1,500 applicants just a few months prior. I wasn't even searching for a full-time role. But I knew I wanted to try.</p>
<p>Soon after, I was asked to start interviewing in January. I had several rounds of interviews from January to March, and each step in the process only strengthened my interest in joining. Everyone I spoke to, including the CEO, was incredibly transparent and kind, openly sharing their successes and struggles, their needs and ambitions, and their personal interests and projects.</p>
<p>Throughout the process, I experienced reassuring honesty and genuine support. When I was offered the role in early March, I accepted without hesitation.</p>
<h2>What's next?</h2>
<p>As a design engineer at Buffer, I bridge design and development and oversee technical strategy for Buffer's marketing website and blog, while also contributing to accessibility and their evolving design system. I also benefit from the cross-mentorship of a larger, senior team, which is something that's difficult to get from consulting.</p>
<p>Now that I'm 3 weeks into this role, I can confidently say that <a href="https://buffer.com/about">Buffer actually lives up to their values</a>. I've observed people in all areas of the company act without ego, express sincere gratitude and optimism, be fully transparent to inform and empower others, and be open to any suggestions to continuously improve.</p>
<p>Like any long-standing business, Buffer has technical and process debt. But with such a trusting and supportive culture, I've been able to make significant dents in tech debt and propose different ways of working, especially between design and engineering.</p>
<p>Buffer not only supports, but encourages meaningful side projects. People openly share and celebrate everything from hobbies to content creation to even successful businesses outside of Buffer. And, with a 4-day work week, people are able to retain energy and curiosity for things outside of work that make them more fulfilled and productive in the long-term.</p>
<p>For me, this transition has been surprisingly easy. Nearly all of my time as a consultant has been with fully embedded, long-term engagements, so the schedule hasn't been too much of an adjustment. And, with the autonomy of my role, I've been able to start introducing practices such as Daily 3S (sweep, sort, standardize) and pairing that I would bring to my clients.</p>
<p>The 4-day work week and calm way of working has also left me with solid energy and inspiration to continue working on my own projects. I'm planning on reserving my Fridays for creating educational content and continuing to build the product that might become a business at some point.</p>
<p>After going through this change in my career, I feel relieved and grateful to be part of a team that's optimizing for the long-term with a people-first approach to work. And I look forward to the calm, stability, and balance it provides, so I can optimize for the long-term in my own life.</p>
Aquileo | The origins of design engineering2024-02-26T00:00:00Zhttps://luhr.co/blog/2024/02/26/the-origins-of-design-engineering/<h1>The origins of design engineering</h1>
<p>My career for the past 10 years has always combined design and development. I've either been responsible for both design and coding in the same role, or acted as the bridging role between designers and engineers. For me, code is the most effective way to realize designs.</p>
<p>We've never settled on how to classify this work, as evidenced by all the evolving titles over time. Some of my past titles are Senior Web Designer, UX Lead, UX Developer, Lead Developer, etc. During this same time period, industry titles have evolved with Product Designer, UX Designer, Full-Stack Designer, UI Engineer, UX Engineer, etc.</p>
<p>Recently I've come across several posts on design engineering and noticed some convergence in new roles with the title of Design Engineer. The responsibilities of these roles aligns with the work I've done for my whole web career, so I was curious where the title came from.</p>
<h2>2019</h2>
<p>In January 2019, <a href="https://chriscoyier.net/">Chris Coyier</a> wrote a landmark post, <a href="https://css-tricks.com/the-great-divide/">The Great Divide</a>, that surveyed the industry and identified the gap between design and engineering. Chris refers to himself as a web craftsman, which I dig.</p>
<p>From what I've found, the earliest resources on design engineering were created by <a href="http://www.artist-developer.com/">Natalya Shelburne</a> in her fall 2019 Beyond Tellerrand talk, <a href="https://www.youtube.com/watch?v=VjSNqCDBWZA">CSS at the intersection</a>.</p>
<p>Some highlights of the talk:</p>
<blockquote>
<p>Why tell designers to learn code when we can create tools that serve as bridges between mental models?</p>
</blockquote>
<blockquote>
<p>Hire people who are good at different things...Enable them to contribute their best way</p>
</blockquote>
<blockquote>
<p>Roles are arbitrary things we decided</p>
</blockquote>
<p><a href="http://www.artist-developer.com/">Natalya's website is called Artist-Developer</a>, which is such a great term to encapsulate the dual nature of this work.</p>
<h2>2020</h2>
<p>In 2020, Natalya Shelburne, <a href="https://www.adekunleoduye.com/">Adekunle Oduye</a>, <a href="https://www.youtube.com/watch?v=wLQNqhwgrjY">Kim Williams</a>, and <a href="https://www.linkedin.com/in/edlou/">Eddie Lou</a> created the <a href="https://marketing.invisionapp-cdn.com/www-assets.invisionapp.com/epubs/InVision_DesignEngineeringHandbook.pdf">Design Engineer Handbook</a>, published by Design Better Co.</p>
<p>Adelkunle Oduye uses the title UX Engineer. Kim Williams has led teams of product designers, UX designers, and UX researchers. And Eddie Lou had UI development and UI engineering roles before forming a design engineering team at Indeed. This is one of the earliest instances I've found of an actual design engineering team at a company.</p>
<p>In October of 2020, <a href="https://shoptalkshow.com/434/">Natalya Shelburne discussed design engineering on episode 434 of Shop Talk Show</a>. In this interview, Natalya discusses coming up with the term with <a href="https://www.aarronwalter.com/">Aarron Walter</a> at Design Exchange in Sydney.</p>
<h2>2021</h2>
<p>In February 2021, <a href="https://bradfrost.com/">Brad Frost</a> coined the term "<a href="https://bradfrost.com/blog/post/front-of-the-front-end-and-back-of-the-front-end-web-development/">front-of-the-front-end web development</a>", building on the idea of a "<a href="https://bradfrost.com/blog/post/frontend-design/">frontend designer</a>" that he wrote about in 2016. Brad has unwaveringly used the title of web designer over the years, which I respect.</p>
<p>A day later, <a href="https://www.trysmudford.com/">Trys Mudford</a> published a series of blog posts on design engineering:</p>
<ul>
<li><a href="https://www.trysmudford.com/blog/i-think-im-a-design-engineer">On Design Engineering: I think I might be a design engineer...</a></li>
<li><a href="https://www.trysmudford.com/blog/prototyping/">On Design Engineering: The designer & developer relationship</a></li>
<li><a href="https://www.trysmudford.com/blog/design-foundations/">On Design Engineering: Systemised design foundations</a></li>
<li><a href="https://www.trysmudford.com/blog/designer-and-developer-relationship/">On Design Engineering: Prototyping</a></li>
</ul>
<p>These posts were followed by a Tweet that summarizes design engineering perfectly:</p>
<figure>
<blockquote>
<p>What I *love* about the name "Design Engineer", is that it's entirely focused on the handshake between those two other roles.</p>
<p>There's no mention of UI, CSS, front-end, design systems, documentation, prototyping, tooling or any 'hard' skills that could be used in the role itself.</p>
</blockquote>
<figcaption>
<a href="https://twitter.com/trysmudford/status/1362375379038707712">@trysmudford</a>
</figcaption>
</figure>
<p>In true design engineering fashion, Trys is a cofounder with <a href="https://www.hustlersquad.net/">James Gilyead</a> of <a href="https://utopia.fyi/">Utopia</a>, which generates fluid type and spacing scales.</p>
<p>James Gilyead is a product designer at <a href="https://clearleft.com/">Clearleft</a>, the design consultancy co-founded by <a href="https://adactio.com/">Jeremy Keith</a>.</p>
<p><a href="https://adactio.com/journal/17838">Jeremy Keith wrote a post on design engineering</a> in response to Trys' posts soon after. Jeremy Keith describes his work as a web developer, or even more directly, as "making websites".</p>
<p>In September 2021, Jeremy Keith published <a href="https://podcast.clearleft.com/season03/episode02/">an episode of the Clearleft podcast on design engineering</a> that featured <a href="https://tobiasahlin.com/">Tobias Ahlin</a>, <a href="https://www.adekunleoduye.com/">Adekunle Oduye</a>, <a href="https://www.jonaizlewood.com/">Jon Aizlewood</a>, and <a href="https://www.trysmudford.com/">Trys Mudford</a>.</p>
<p>Tobias is a design engineer at GitHub and Jon Aizlewood is a design leader. Tobias is another instance of design engineer in a formal role.</p>
<h2>2022</h2>
<p>In May 2022, <a href="https://jim-nielsen.com/">Jim Nielsen</a> wrote a post on <a href="https://blog.jim-nielsen.com/2022/the-case-for-design-engineers/">The Case for Design Engineers</a>. This post links to <a href="https://adactio.com/journal/17838">Jeremy Keith's 2021 post</a> and <a href="https://adactio.com/journal/18982">Jim's post on Declarative Design from April 2022</a>.</p>
<p>The term declarative design really captures the emerging design and engineering opportunities of modern CSS. It builds on the concept of intrinsic web design from <a href="https://jensimmons.com/">Jen Simmons</a>, <a href="https://andy-bell.co.uk/">Andy Bell</a> and <a href="https://heydonworks.com/">Heydon Pickering's</a> work with <a href="https://every-layout.dev/">Every Layout</a>, and <a href="https://www.trysmudford.com/">Trys Mudford</a> and <a href="https://www.hustlersquad.net/">James Gilyead's</a> work with <a href="https://utopia.fyi/">Utopia</a>.</p>
<p>At this point, design engineering is building on a lot of the most exciting work in the space.</p>
<h2>2023</h2>
<p>I haven't found as many resources in design engineering that were published in 2023, but I started noticing actual job listings with Design Engineer as the title.</p>
<p>After the industry panic in 2023 with mass layoffs, it's interesting that as companies being hiring again, they are identifying this new role as one to hire for.</p>
<h2>2024</h2>
<p>When I'm interested in how companies operate, I often poke around their blogs and job listings to get a pulse on their team and direction. We're just a couple months into 2024, and I've already noticed a significant increase in job listings with the title of Design Engineer.</p>
<p>This same month, <a href="https://jim-nielsen.com/">Jim Nielsen</a> wrote a follow up to his 2022 post: <a href="https://blog.jim-nielsen.com/2024/the-case-for-design-engineers-pt-ii/">The Case for Design Engineers, Pt. II</a>.</p>
<p>A single quote illustrates the strength of this discipline:</p>
<blockquote>
<p>design work with code</p>
</blockquote>
<h2>The future</h2>
<p>It's interesting to trace how early ideas around our roles can spread and eventually shape the titles companies hire for.</p>
<p>2 years after Natalya's Beyond Tellerrand talk, there's an initial surge in content around design engineering, and 2 to 3 years later, noticeably more jobs are using the term.</p>
<p>With CSS continuing to rapidly evolve, we have so many new patterns to explore and refine. Container queries, CSS subgrid, CSS layers, and more are going to push our design opportunities further. It's crucial that we combine design and engineering, or at least eliminate handoff in favor of collaborating, to make the most of these capabilities.</p>
<p>Over the past 10 years, we've had so many different titles, but one thing remains constant: the essential work of bridging design and development. It's uncertain whether design engineering will last as a title, but any momentum that further legitimizes this valuable role is worth supporting.</p>
<p>If I've missed any important people, resources, or milestones in this history, let me know.</p>
<p>If you or your team need help with design engineering and establishing more effective processes, let's talk: hello@luhr.co</p>
Aquileo | Test-driven development as prompt engineering2024-02-07T00:00:00Zhttps://luhr.co/blog/2024/02/07/test-driven-development-as-prompt-engineering/<h1>Test-driven development as prompt engineering</h1>
<p>As AI code generator tools gain adoption, Test-Driven Development (TDD) becomes even more essential and a key differentiator for developers.</p>
<p>As Kent Beck said: "<a href="https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0">90% of my skills just dropped to $0. The leverage for the remaining 10% went up 1000x</a>." I don't think we're anywhere close to 90% currently, but it's likely we'll get there. TDD is going to be a large portion of that remaining 10% that'll become the majority of meaningful work we do as programmers.</p>
<p>I briefly trialed GitHub Copilot in the summer of 2023. While it produced interesting results, I haven't integrated into my daily work. I'll be revisiting this decision periodically. Since then, I've built further experiences working with clients who use these tools and observing videos and posts from the community in general.</p>
<p>Throughout this period, I've observed people being frustrated with the code output of tools like Copilot and ChatGPT, and there are early signs that <a href="https://visualstudiomagazine.com/articles/2024/01/25/copilot-research.aspx">AI code generators might be lowering code quality at scale</a>.</p>
<p>While these tools can definitely vary in output, I've learned their quality is highly dependent on prompt engineering.</p>
<p>With AI code generator tools like GitHub Copilot, most people leave code comments, wait for generated responses, and are underwhelmed with their choices.</p>
<p>If you use these tools, you'll get the best results by instead following a test-driven development (TDD) workflow.</p>
<p>With TDD, you follow a loop of <a href="https://www.jamesshore.com/v2/blog/2005/red-green-refactor">Red, Green, Refactor</a>: you first write a failing test (Red), then just enough code to make that test pass (Green), then safely improve your code while ensuring it still works (Refactor).</p>
<p>Typically, I only work on one test at a time, but a tradeoff with copilot tools is they benefit from you writing every test you can think of in advance. This helps guide the generated code output address all expected scenarios at once. Otherwise, you'd have to resort to prompting the copilot to iterate the code for each new scenario, which currently doesn't seem as successful.</p>
<p>My preferred copilot workflow is the following:</p>
<ol>
<li>Write all tests you can think of for a function (Red)</li>
<li>Only use copilot tools for the implementation stage of writing just enough code to make the test pass (Green)</li>
<li>Improve the generated code to meet coding standards and add any other tests that come to mind (Refactor)</li>
</ol>
<p>This is similar to <a href="https://anthonysciamanna.com/2015/04/18/ping-pong-pair-programming.html">ping-pong pairing</a> (also known as strict pairing), where one pairing partner writes a test, the other partner writes the code, and then the pair trade roles. Except with copilot tools, you should always be the one writing the tests. It's fine to prompt the tool for additional test ideas, but make sure you are the one setting expectations for how the code should work.</p>
<ul>
<li>Tests are essential to ensure the code does what <em>you</em> want</li>
<li>Tests are essential to refactoring
<ul>
<li>If the copilot output passes the tests, that's a good stopping point</li>
<li>If you want to refactor the code, you can safely do so with test coverage</li>
</ul>
</li>
<li>It's a great learning mechanism
<ul>
<li>I can often state the objective of code, with the function name, inputs, and outputs I'd like to have, but I might deliberate the <em>how</em> too much</li>
</ul>
</li>
</ul>
<p>It's important for humans to write tests for the behavior that is interesting to our work, and "<a href="https://anthonysciamanna.com/2018/03/25/too-simple-to-test.html">human judgement is always part of TDD</a>."</p>
<h2>Issues with common copilot workflows</h2>
<p>The most common workflow I've observed with copilot tools is developers writing code comments that describe the implementation they want. The copilot generates a function, and the developer may make some small tweaks. At best, the developer might then prompt the copilot to generate some tests for the new function.</p>
<p>The first issue of this workflow is waste. Prompting code comments can be quite verbose and require multiple attempts. Plus, these code comments have to be deleted after the code is generated or they add noise to the codebase. Additionally, there may be rework needed to get the code in the desired shape and functionality.</p>
<p>The second and more important issue is bugs with false positives. Even without copilot tools, writing tests <em>after</em> (Test-After Development, or TAD) introduces the risk of only demonstrating what the code currently does. Even if all the tests pass, that means the tests simply describe the current functionality, but they don't prove that the code under test has the desired functionality. By writing tests first, we ensure the generated code meets our needs and accounts for all the scenarios or potential errors that matter to us.</p>
<h2>Benefits of TDD as prompt engineering</h2>
<p>TDD makes quality suggestions much more likely.</p>
<p>Writing tests first sets expectations for the generated code. It defines:</p>
<ol>
<li>What the function is called</li>
<li>What it should do (at least for the current case)</li>
<li>What arguments the function accepts (its signature)</li>
<li>What it returns</li>
</ol>
<p>This gives much more effective guidance for AI as your pairing partner to generate useful code, making it more likely that the generated code will handle the implementation successfully.</p>
<p>After writing tests, it's helpful to evaluate the generated suggestions (using the <code>Ctrl/Cmd + Enter</code> command in VSCode, for example) and pick the one you feel is most likely to pass the tests. This is a good opportunity to make a hypothesis/bet and learn from the results.</p>
<p>Because TDD encourages good design and single responsibility principle for testable functions, copilot tools are more likely to generate more focused, cleaner functions.</p>
<h2>Tips</h2>
<h3>Code comment prompts as code smells</h3>
<p>If you have to resort to scaffold code or comments for prompts, you're not writing tests first, probably taking too big a step, doing more than one thing, and working at too high a level of abstraction.</p>
<p>Instead, write tests for each operation, which will prompt the generated code to first create more testable helper functions that are largely one liners.</p>
<h3>Have a pair programming mindset</h3>
<p>Working with AI is like being the navigator in pair programming. I found the constant code suggestions to be quite distracting at first, but when I started thinking of it as another form of pairing, I reframed the suggestions as immediate feedback about my role as navigator and using tests as prompts.</p>
<h3>Speed and scale are dangerous priorities</h3>
<p>AI code generator tools are an accelerator, but accelerating isn't always desirable. Going faster doesn't mean anything if it's in the wrong direction. Going in the right direction, then going faster, is a true advantage.</p>
<p>Speed and scale in isolation create waste, not value. In the world of lean manufacturing, the ranked criteria for any improvement is:</p>
<ol>
<li>Safer</li>
<li>Better quality</li>
<li>Simpler</li>
<li>Faster</li>
</ol>
<p>The post "<a href="https://anthonysciamanna.com/2018/04/29/safety-accuracy-efficiency-then-scale.html">Safety, Accuracy, Efficiency, Then Scale</a>" offers a related perspective focused on product teams.</p>
<p>My experience working with TDD, with or without AI code generators, is that it results in much, much greater speed and scalability over time. This is thanks to fewer bugs, less manual debugging, better documented/simpler code, and improved system design.</p>
<p>So, if copilot tools are appealing for their speed gains, the best way to leverage these gains is by incorporating TDD.</p>
<h2>TDD is even more important as AI code generators gain adoption</h2>
<p>As more and more teams start using AI code generators to churn out variable code quality, following Test-Driven Development will be a real competitive advantage.</p>
<p>In programming, <a href="https://blog.ploeh.dk/2018/09/17/typing-is-not-a-programming-bottleneck/">typing isn't the bottleneck</a>. Thinking is. Thinking about the tests ("what" the code should do) is far more important than thinking about the implementation code ("how" the code should do it). Without tests, we're handing off too much responsibility for copilot tools to determine both "what" and "how".</p>
<p><a href="https://luhr.co/blog/2023/05/16/prompt-engineering-is-shaping/">Prompt engineering is shaping</a>, and TDD is prompt engineering. Without tests, we're not actually capturing or asserting our thinking for what generated code should do. We're just taking suggestions as our code quality, long-term productivity, and maintainability suffer at scale.</p>
<p>If you or your team need help implementing test-driven development and want to achieve meaningful speed and scale, let's talk: <strong>hello@luhr.co</strong></p>
Aquileo | Build-free type annotations with JSDoc and TypeScript2024-01-25T00:00:00Zhttps://luhr.co/blog/2024/01/25/build-free-type-annotations-with-jsdoc-and-typescript/<h1>Build-free type annotations with JSDoc and TypeScript</h1>
<p>I've used <a href="https://www.typescriptlang.org/">TypeScript</a> in various client projects, but sometimes got bogged down with migrating to <code>.ts</code> files and the tooling. After Svelte switched to using JSDoc with TypeScript in the spring of last year, it caught my attention and motivated me to try it out in my own projects.</p>
<p>My solo projects are entirely in vanilla JS. I like the simplicity, direct learning with exposure to browser APIs and web standards, and adaptability it gives me so I can help clients in various frameworks with better fundamental skills.</p>
<p>One of my favorite benefits of working this way is a build-free setup with no production dependencies and normal <code>.js</code> files. As a result, the build setup and file extension for TypeScript have been a deal breaker for my personal work. If your project already has a build step, then just using TypeScript can be great. But in my work, using JSDoc with TypeScript unlocked all the value of type annotations without the unwanted overhead.</p>
<h2>Using JSDoc</h2>
<p>On it's own, <a href="https://jsdoc.app/">JSDoc</a> is a documentation generator for JavaScript that uses comments to describe your code with various tags that use the <code>@</code> syntax.</p>
<p>The tags we're most interested in for type annotations are <code>@type</code>, <code>@typedef</code>, <code>@property</code>, <code>@params</code>, and <code>@returns</code>.</p>
<p>For example, when creating a new element and assigning it to a variable, we can document the <code>@type</code> as <code>HTMLElement</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/** @type {HTMLElement} */</span>
<span class="token keyword">const</span> headingElement <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">"h1"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Or, we can document a function's signature with its parameter and return types using the <code>@param</code> and <code>@returns</code> tags:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @param {Node} node
* @returns {String}
*/</span>
<span class="token keyword">function</span> <span class="token function">getNodeName</span><span class="token punctuation">(</span><span class="token parameter">node</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> node<span class="token punctuation">.</span>nodeName<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Occasionally, it's necessary to cast a type so it respects a function's signature and expected types, such as specifying an <code>Event</code> type as a <code>KeyboardEvent</code>. This can be done with the inline comment syntax, <code>@type</code> tag, and wrapping the expression you want to cast in parentheses:</p>
<pre class="language-js"><code class="language-js"><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"keydown"</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 function">handleKeydown</span><span class="token punctuation">(</span><span class="token comment">/** @type {KeyboardEvent} */</span> <span class="token punctuation">(</span>event<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>The last thing I commonly do is create a custom type definition for objects that are used between functions. This uses the <code>@typedef</code> and <code>@property</code> tags to document the structure of an object. Here's an example of creating a custom type for menu option objects that contain properties for a display name, action, and keyboard shortcut:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @typedef {Object} Option
* @property {String} displayName
* @property {String} action
* @property {String} [shortcut=""]
*/</span></code></pre>
<p>This example also demonstrates the optional syntax using square brackets and an optional default value with the equals sign.</p>
<p>With this type definition, we can use this custom type elsewhere in our annotations:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @param {Option} option
* ...
*/</span></code></pre>
<p>The main pushback I find with JSDoc that leads to "Just use TypeScript!" responses is the syntax is too verbose. I personally don't mind the syntax. It's simple, readable, and with snippets takes little effort to author.</p>
<p>Most JSDoc examples include a description in the first line of the comment, but these add little value if you have well named, readable code. In fact, I treat descriptions much like code comments in general: an anti-pattern that indicates there are code quality issues. There are rare exceptions, such as explaining why something obscure is needed due to a browser bug, but my standard is to omit descriptions.</p>
<p>VSCode has strong support for JSDoc, including default snippets for both the multi-line and single line comments, which start with a forward slash and two asterisks: <code>/**</code>. You can expand multi-line comments with the <kbd>Enter</kbd> key, and VSCode will do a decent job of filling out the parameter and return types for you. You can expand single-line comments with the <kbd>Tab</kbd> key, but VSCode can't infer the tags or types in this case.</p>
<p>As a standalone tool, JSDoc provides a lot of value with standardized documentation throughout a codebase. However, it's just static documentation and doesn't provide any feedback while coding. For that, we'll add TypeScript into the mix for continuous type checking.</p>
<h2>Using JSDoc with TypeScript</h2>
<p>The first thing we need to do to leverage TypeScript is install it as a dev dependency:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> i typescript <span class="token parameter variable">-D</span></code></pre>
<p>Next, we need a configuration file, such as <code>jsconfig.json</code>, with our desired settings:</p>
<pre class="language-js"><code class="language-js"><span class="token punctuation">{</span>
<span class="token string-property property">"compilerOptions"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token string-property property">"allowJs"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token string-property property">"checkJs"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token string-property property">"strict"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token string-property property">"noEmit"</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 string-property property">"include"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"src"</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span></code></pre>
<p>With TypeScript set up, it'll immediately provide syntax highlighting for any type issues. If you don't already have type annotations, you'll find red squiggles throughout your codebase. If you have existing type annotations that are inaccurate or function calls that aren't respecting the proper types, these will be surfaced as well.</p>
<p>This continuously reinforces standards to annotate your code, helps prevent accidental type coercion, and makes your code more resilient by accounting for edge cases from unexpected types.</p>
<p>These edge cases often go completely unaccounted for and create bugs that are hard to track down or replicate. Test-Driven Development (TDD) helps reduce these bugs, but these types of tests are lower value, tedious to write, and difficult to exhaustively predict for every function and variable.</p>
<h2>How I use tests and type annotations together</h2>
<p>As an example, let's enforce proper types in a <code>getNodeName</code> function:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">getNodeName</span><span class="token punctuation">(</span><span class="token parameter">node</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> node<span class="token punctuation">.</span>nodeName<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>With tests, we should assert the function throws an error if the type of <code>node</code> passed into the function is not <code>Node</code>. We should also test that the returned value is a string:</p>
<pre class="language-js"><code class="language-js"><span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">"getNodeName"</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 function">it</span><span class="token punctuation">(</span><span class="token string">"throws an error if the provided node is not a type of Node"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">expect</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 function">getNodeName</span><span class="token punctuation">(</span><span class="token string">"string"</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>to<span class="token punctuation">.</span><span class="token function">throw</span><span class="token punctuation">(</span>Error<span class="token punctuation">,</span> <span class="token string">"node has incorrect type of [object String]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">it</span><span class="token punctuation">(</span><span class="token string">"returns the node name as a string"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">expect</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">const</span> testDivElement <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">"div"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> returnValue <span class="token operator">=</span> <span class="token function">getNodeName</span><span class="token punctuation">(</span>testDivElement<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">typeof</span> returnValue<span class="token punctuation">)</span><span class="token punctuation">.</span>to<span class="token punctuation">.</span><span class="token function">equal</span><span class="token punctuation">(</span><span class="token string">"string"</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>To make these tests pass, we'll need to improve our function:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">getNodeName</span><span class="token punctuation">(</span><span class="token parameter">node</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 operator">!</span>node<span class="token punctuation">.</span>nodeName<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">node has incorrect type of </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token class-name">Object</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>
node
<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 punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> node<span class="token punctuation">.</span>nodeName<span class="token punctuation">.</span><span class="token function">toLowerCase</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 can be persuaded that even with type checking, these tests are valuable. But I write tests to get continuous feedback about the code I'm writing. JSDoc with TypeScript provide that feedback, so I'm personally comfortable skipping these types of tests in my own projects. This is a decision I might revisit in the future.</p>
<p>So instead, or in addition to these tests, we can add type annotations to our function:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @param {Node} node
* @returns {String}
*/</span>
<span class="token keyword">function</span> <span class="token function">getNodeName</span><span class="token punctuation">(</span><span class="token parameter">node</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 operator">!</span>node<span class="token punctuation">.</span>nodeName<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Cannot get node name of "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>node<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" with incorrect type of </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token class-name">Object</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>
node
<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 punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> node<span class="token punctuation">.</span>nodeName<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>To finish our function, it's still important to assert our function guards against null or undefined values. Let's write a test for that:</p>
<pre class="language-js"><code class="language-js"><span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">"getNodeName"</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 function">it</span><span class="token punctuation">(</span><span class="token string">"throws an error if no node is provided"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">expect</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 function">getNodeName</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>to<span class="token punctuation">.</span><span class="token function">throw</span><span class="token punctuation">(</span>Error<span class="token punctuation">,</span> <span class="token string">"node is undefined"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">/* ... */</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And we add the guard to our function:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @param {Node} node
* @returns {String}
*/</span>
<span class="token keyword">function</span> <span class="token function">getNodeName</span><span class="token punctuation">(</span><span class="token parameter">node</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 operator">!</span>node<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"node is undefined"</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token comment">/* ... */</span>
<span class="token punctuation">}</span></code></pre>
<h2>A helpful snippet for type guards</h2>
<p>One of the more common ways TypeScript makes code more resilient is in preventing <code>null</code> or <code>undefined</code> values from being operated on in subsequent steps.</p>
<p>I typically handle this with <code>if</code> statements that act as type guards. Here's an example of two type guards to prevent <code>null</code> and <code>undefined</code> values from causing errors later in the function:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @returns {String}
*/</span>
<span class="token keyword">function</span> <span class="token function">getMenuActiveDescendantId</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">/** @type {HTMLElement|null} */</span>
<span class="token keyword">const</span> docMenu <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"#menu"</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 operator">!</span>docMenu<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"docMenu is null"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">/** @type {String|undefined} */</span>
<span class="token keyword">const</span> activeDescendantId <span class="token operator">=</span> docMenu<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">"aria-activedescendant"</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 operator">!</span>activeDescendantId<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span>"activeDescendantId is <span class="token keyword">undefined</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> activeDescendantId<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>To make repeatedly typing this easier, I have a simple VSCode snippet that I invoke with <code>nil</code> followed by the <kbd>Tab</kbd> key:</p>
<pre class="language-json"><code class="language-json"><span class="token property">"Throw error if null"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"prefix"</span><span class="token operator">:</span> <span class="token string">"nil"</span><span class="token punctuation">,</span>
<span class="token property">"body"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"if (!$1) {"</span><span class="token punctuation">,</span> <span class="token string">" throw new Error(\"$1 is $2\");"</span><span class="token punctuation">,</span> <span class="token string">"}"</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span></code></pre>
<h2>Wrapping up</h2>
<p>I've been using JSDoc with TypeScript for months now and feel it's the perfect balance for my personal work. The nice thing is these skills are transferable to using TypeScript directly by simply adapting the syntax, making my transition to client work seamless.</p>
<p>I'm really excited for the <a href="https://github.com/tc39/proposal-type-annotations">native type annotations proposal</a> which is currently Stage 1. I hope we end up with a nice syntax similar to the optional types I use in Python or in GDScript (a Python-like language used in the Godot open source video game engine).</p>
<p>If you implement JSDoc with TypeScript, be sure to reference the <a href="https://jsdoc.app/">JSDoc documentation</a> and especially the <a href="https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html">TypeScript JSDoc reference</a>, which I find even more useful.</p>
Aquileo | Vision Pro, rabbit r1, LAMs, and accessibility2024-01-19T00:00:00Zhttps://luhr.co/blog/2024/01/19/vision-pro-rabbit-r1-lams-and-accessibility/<h1>Vision Pro, rabbit r1, LAMs, and accessibility</h1>
<p>Today Apple released <a href="https://www.youtube.com/watch?v=Vb0dG-2huJE">a video guided tour of the Vision Pro</a> which gave a larger preview of the spatial user interfaces and how people will interact with them.</p>
<p>A little over a week ago, <a href="https://www.youtube.com/watch?v=22wlLy7hKP4">rabbit announced r1</a>, a "personalized operating system" and companion device that primarily uses voice controls.</p>
<p>Both of these devices are interesting from an accessibility perspective. On one hand, they could provide new opportunities for people with limited mobility. On the other, the way they display UIs and handle user interactions makes me concerned that they will encourage bad practices that harm accessibility over time.</p>
<h2>Concerns with spatial computing and the Apple Vision Pro</h2>
<p>Back in June, I privately noted accessibility concerns with the initial announcement of the Apple Vision Pro and the <a href="https://developer.apple.com/videos/play/wwdc2023/10279/">Meet Safari for spatial computing</a> video.</p>
<p>With today's video guided tour, I noticed the same issues and wanted to further detail them.</p>
<h3>Text color contrast ratio</h3>
<p>Like many Apple user interfaces, the text color contrast ratio is very low in menus, titles, and overlay text.</p>
<p>In the guided tour, there's white title text on top of the image grid with no defensive design considerations for how the text will appear on top of photos with bright content. As a result, the example text in the guided tour has a failing contrast ratio of 1.47:1. The lowest possible ratio is 1:1 with two identical colors, so this is a substantial readability issue. It needs to be at least 4.5:1 to meet the WCAG 2.1 AA standard.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/6z1stf5Gkx-320.avif 320w, https://luhr.co/assets/images/generated/6z1stf5Gkx-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/6z1stf5Gkx-320.webp 320w, https://luhr.co/assets/images/generated/6z1stf5Gkx-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/6z1stf5Gkx-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/6z1stf5Gkx-320.jpeg 320w, https://luhr.co/assets/images/generated/6z1stf5Gkx-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>The white overlay text on the image grid has 1.47:1 contrast.</figcaption>
</figure>
<p>For UI elements with a background, the frosted glass effect of a semi-transparent background with background blur similarly compromises on contrast. Even for buttons with more opaque backgrounds, the contrast ratio I measured was only around 2.16:1.</p>
<p>Native app UIs like Mail and Safari have similar issues. The <strong>highest</strong> contrast ratio I measured was 4.18:1, which only meets the AA standard for large text. All other instances of text had failing color contrast ratios.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/khyCdj9Pr6-320.avif 320w, https://luhr.co/assets/images/generated/khyCdj9Pr6-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/khyCdj9Pr6-320.webp 320w, https://luhr.co/assets/images/generated/khyCdj9Pr6-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/khyCdj9Pr6-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/khyCdj9Pr6-320.jpeg 320w, https://luhr.co/assets/images/generated/khyCdj9Pr6-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>The Mail app in Apple Vision Pro has insufficient color contrast across the UI.</figcaption>
</figure>
<h3>"Cursor" hover indicators</h3>
<p>The Apple Vision Pro uses eye tracking to determine hover on interactive elements, and hand/finger gestures to determine click, drag, and swipe interactions.</p>
<p>When an interactive element has hover from the user's gaze, it's indicated with a very, very subtle change in brightness, sometimes paired with a similarly subtle scaling. These changes are so slight that even though I have perfect vision with contact lenses, I have a hard time telling the difference.</p>
<p>When the user looks at different photos in the photo app, the brightness difference between hover and default states is almost imperceptible as it moves between photos.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/SiksrSxKd1-320.avif 320w, https://luhr.co/assets/images/generated/SiksrSxKd1-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/SiksrSxKd1-320.webp 320w, https://luhr.co/assets/images/generated/SiksrSxKd1-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/SiksrSxKd1-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/SiksrSxKd1-320.jpeg 320w, https://luhr.co/assets/images/generated/SiksrSxKd1-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>The before and after comparison of hover moving from one image to another is almost imperceptible.</figcaption>
</figure>
<p>This issue carries over to interacting with the web. In <a href="https://developer.apple.com/videos/play/wwdc2023/10279/">the June WWDC video demonstrating Safari for spatial computing</a>, the hover indicator for links is incredibly difficult to see, with a contrast ratio of 1.09:1.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/J5U-XH5op--320.avif 320w, https://luhr.co/assets/images/generated/J5U-XH5op--640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/J5U-XH5op--320.webp 320w, https://luhr.co/assets/images/generated/J5U-XH5op--640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/J5U-XH5op--320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/J5U-XH5op--320.jpeg 320w, https://luhr.co/assets/images/generated/J5U-XH5op--640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>On a page will multiple card links, the hover indicator on the center link has incredibly low color contrast.</figcaption>
</figure>
<p>In the home view, app icons receive the same subtle increase in brightness and a slight scaling on hover.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/crdnOI33vD-320.avif 320w, https://luhr.co/assets/images/generated/crdnOI33vD-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/crdnOI33vD-320.webp 320w, https://luhr.co/assets/images/generated/crdnOI33vD-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/crdnOI33vD-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/crdnOI33vD-320.jpeg 320w, https://luhr.co/assets/images/generated/crdnOI33vD-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Hover on app icons in the home view are also very difficult to differentiate.</figcaption>
</figure>
<p>In menus, the indicator is more noticeable, but it unfortunately matches the active/selected state.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/P0UVGGSGCd-320.avif 320w, https://luhr.co/assets/images/generated/P0UVGGSGCd-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/P0UVGGSGCd-320.webp 320w, https://luhr.co/assets/images/generated/P0UVGGSGCd-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/P0UVGGSGCd-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/P0UVGGSGCd-320.jpeg 320w, https://luhr.co/assets/images/generated/P0UVGGSGCd-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Menu items share hover and selected state styling.</figcaption>
</figure>
<p>There's an argument that because hover is always where the user is currently looking, there's no ambiguity about where hover is, so it's preferable that it's subtle to not interfere with elements that users are looking at.</p>
<p>While this is true for interactive elements, the hover indicator is so subtle that it's very difficult to determine what elements are actually interactive. This is a particularly bad issue for the web, where each website has unique design, and determining what is or isn't a link through vision alone is nearly impossible due to the prevalence of ambiguous UI design.</p>
<h3>Interactive regions in Safari for spatial computing</h3>
<p>Safari for spatial computing maps pinch and pointer finger gestures to touch events with a coarse pointer and no hover. This helps clarify why vision tracking is essentially a cursor that hovers elements instead of focusing them.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/KKlEw01vJF-320.avif 320w, https://luhr.co/assets/images/generated/KKlEw01vJF-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/KKlEw01vJF-320.webp 320w, https://luhr.co/assets/images/generated/KKlEw01vJF-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/KKlEw01vJF-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/KKlEw01vJF-320.jpeg 320w, https://luhr.co/assets/images/generated/KKlEw01vJF-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Indirect pinch gestures correspond to pointerdown, pointermove, and pointerup touch events.</figcaption>
</figure>
<p>Safari for spatial computing has clear criteria for determining which elements are interactive and should provide the (very subtle) hover indicator when the user looks at them:</p>
<ul>
<li>Buttons, links, and menus</li>
<li>Elements with the equivalent ARIA roles</li>
<li>Input fields and form elements</li>
<li>Elements with CSS <code>cursor: pointer;</code></li>
</ul>
<p>The first and third criteria are sensible, as semantic HTML should determine interactivity. I'm not sure what they mean by "menus" separately from "Elements with the equivalent ARIA roles", as we have no native menu elements in HTML.</p>
<p>I'm concerned with "Elements with the equivalent ARIA roles" that aren't buttons, links, or input fields, as well as "Elements with CSS <code>cursor: pointer;</code>.</p>
<p>Both these criteria enable interaction for elements that may not actually be accessible for keyboard and assistive technology users. The presence of an ARIA role on an element has no guarantee that it will properly announce its function or even handle mouse and keyboard click events. The same goes for using <code>cursor: pointer;</code>, which is completely presentational.</p>
<p>Developers can opt out of these hover indicators with <code>pointer-events: none;</code>, which does affect interactivity and feels asymmetric. The hover indicators are shaped with <code>border-radius</code>, which matches how <code>outline</code> now behaves.</p>
<p>This criteria fully supports a <code><div></code> with a click event listener, which is worrying.</p>
<p>I understand that Apple wants Safari for spatial computing to be as resilient as possible with the unpredictable web, but supporting bad practices can only encourage lacking accessibility. Developers are already prone to use "it works for me" as an excuse, and now an incredibly expensive device will only reinforce that flawed logic further.</p>
<p>This isn't specific to spatial computing, but the code used in the June WWDC video demo shows other bad practices, such as wrapping <code><li></code> with <code><a></code> to change the appearance of the hover indicator, and referring to each link as a "button". Here's some of the code for the example navigation:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>nav</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>sidebar<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>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>home.html<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>li</span><span class="token punctuation">></span></span>home<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 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>index.html<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>selected<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>li</span><span class="token punctuation">></span></span>teas<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 punctuation">></span></span>
<span class="token comment"><!-- ... --></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>nav</span><span class="token punctuation">></span></span></code></pre>
<p>First off, anything but <code><li></code> as a direct child of <code><ul></code>/<code><ol></code> isn't valid HTML. Second, there's no need to wrap the list items in <code><a></code> elements to change the visual appearance of a hover indicator. Simply size and style the link as desired and it'll automatically have a matching size and border radius on hover. Lastly, as a real nitpick, don't hard-code text casing in HTML to achieve visual design. Use the <code>text-transform</code> property.</p>
<p>I point these indirectly related issues out because this is supposed to be an instructional video for a developer conference on how to properly build websites for spatial computing. Having such fundamental mistakes from such a large authority as Apple should be concerning.</p>
<h3>What you should do to provide a universal experience</h3>
<p>Despite Safari for spatial computing accommodating fake interactive elements that simply have ARIA roles and <code>cursor: pointer;</code>, you should still always stick with semantic interactive elements, such as <code><a></code>, <code><button></code>, <code><detail></code>/<code><summary></code>, <code><input></code>, <code><select></code>, and <code><textarea></code>. Valid, semantic HTML remains crucial to accessibility.</p>
<p>To properly support all users, we need to provide accessible focus indicators (with <code>outline</code>) for keyboard and assistive technology users, hover for mouse users with the <code>:hover</code> pseudo selector, and hover indicators with <code>cursor: pointer;</code> for Safari for spatial computing users.</p>
<p>Luckily, all of these indicators inherit <code>border-radius</code>, including <code>outline</code> in recent browser versions.</p>
<p>Interestingly, using <code>cursor: pointer;</code> on interactive elements such as buttons and input fields has always been a source of debate. Some argue that adding it is inconsistent with native apps that have a <code>cursor: default;</code> appearance when hovering interactive elements. Since Apple is now encouraging/requiring <code>cursor: pointer;</code> for all interactive elements, I guess this is a sensible default for all buttons, links, and input fields.</p>
<p>Lastly, sufficient tap targets (44 by 44 pixels) are still universally helpful to accommodate interaction in all forms.</p>
<h2>And now, let's talk about rabbit r1 and LAMs</h2>
<p>A little over a week ago, <a href="https://www.rabbit.tech/">a new company called rabbit</a> announced r1, a companion device they claim allows users to accomplish complex tasks on the web by speaking naturally.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/NctAczQX6X-320.avif 320w, https://luhr.co/assets/images/generated/NctAczQX6X-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/NctAczQX6X-320.webp 320w, https://luhr.co/assets/images/generated/NctAczQX6X-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/NctAczQX6X-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/NctAczQX6X-320.jpeg 320w, https://luhr.co/assets/images/generated/NctAczQX6X-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>The rabbit r1 device, designed by <a href="https://teenage.engineering/">Teenage Engineering</a>.</figcaption>
</figure>
<p><a href="https://www.youtube.com/watch?v=22wlLy7hKP4">The rabbit r1 announcement video</a> discusses a specific type of AI called LAMs, or Large Action Models, which they've trained to accomplish tasks based on language input using user interfaces.</p>
<p>From my understanding, they've trained models to visually parse UIs to identify interactive elements and carry out actions. The example images from this model have red borders around every text and interactive element, such as (icon) buttons, links, and other controls.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/dpg37nRtS6-320.avif 320w, https://luhr.co/assets/images/generated/dpg37nRtS6-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/dpg37nRtS6-320.webp 320w, https://luhr.co/assets/images/generated/dpg37nRtS6-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/dpg37nRtS6-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/dpg37nRtS6-320.jpeg 320w, https://luhr.co/assets/images/generated/dpg37nRtS6-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>rabbit LAMs visually identify text and interactive elements with red borders in both native app and website UIs.</figcaption>
</figure>
<p>It's unclear what goes into this identification. My guess is it's purely visual, as it spans native apps and websites, making it difficult to consider the underlying code. But, it would be possible to also parse native code and HTML to better parse UI elements.</p>
<p>Similar to Vision Pro, an over-reliance on visual parsing might make for a more resilient and adaptive tool for interacting with the unpredictable web, but accommodation also allows for us to let our accessibility standards slip further.</p>
<p>If devices like r1 become successful and prevalent, then companies might optimizing for visual presentation over accessibility even more than we do now. This could create further gaps in accessibility and strengthen the confusion between visual design and semantics.</p>
<p>The announcement video had several magic edits when demonstrating the device actually carrying out a task, so it's still completely unproven.</p>
<p>If it does actually work, then it could be an interesting new opportunity for users with disabilities, with LAMs having potential for enhancing assistive technology.</p>
<p>Nonetheless, this device and the underlying technology will need to prove itself valuable and reliable before I put any weight into the claims or get excited about future possibilities.</p>
<h2>Can we have a unified model of understanding UIs?</h2>
<p>We these emerging vision-based technologies, I wish we could lean more heavily on proper, semantic code and the accessibility tree to determine, well, semantics and interaction. Finding <code><div></code> elements with click handlers is easier than finding correct heading levels, and I'm worried this is only going to get worse.</p>
<p>Accessibility and valid HTML are shockingly bad across the majority of websites. While this emerging wave of devices might open up access by adapting to inaccessible code with lacking semantics, they might also further encourage teams to ignore accessibility, prioritize visual presentation over everything, and buy into the delusion that if it works for me, it works for everyone.</p>
Aquileo | 2023 year in review2023-12-28T00:00:00Zhttps://luhr.co/blog/2023/12/28/2023-year-in-review/<h1>2023 year in review</h1>
<p>2023 was a year of challenges, deep learning, experimentation, patience, and self-reliance.</p>
<h2>Career</h2>
<p>My work this year had a very different balance compared to previous years. Due to the economy, I invested about 75% of my time on personal projects and 25% on client work.</p>
<h3>Consulting</h3>
<p>This year was a rough one for consulting due to the collapse of Silicon Valley Bank in March that sent ripples of panic throughout our industry. Demand for client work evaporated as companies of all sizes reacted with fear of a looming wider collapse. As many people were affected by layoffs, I felt fortunate to have my own business, even if revenue took a substantial hit.</p>
<p>So, faced with fewer prospects, I invested my time in creating educational content, refining my way of working, going deep on interesting problems, and exploring a new product I'd like to launch in the future.</p>
<p>I've noticed confidence and momentum in the industry are picking up again lately, so demand for consulting is returning to normal. I've already started booking out 2024, with the spring filling up quick. If you or your team need help with accessible design and development, test-driven development, Shape Up, or process improvements, feel free to reach out: hello@luhr.co</p>
<h3>Educational content</h3>
<p>I started the year off with recording a course called <a href="https://www.linkedin.com/learning/accessibility-first-design">Accessibility-first design</a> for LinkedIn Learning. It was a great experience to record onsite at the LinkedIn Learning offices in Santa Barbara, California, and enjoy the warm January weather as an escape from the snow.</p>
<p>The script writing process for the course got me in a solid writing habit and the video format pushed me to be clearer and more concise in my teaching. Both of these experiences have continued to improve my educational content.</p>
<p>The course launched in April and now has over 12,000 students with a 4.9/5 rating. I'm happy with how the course turned out and glad so many people want to create more accessible design for everyone. A goal of my career is making accessibility-first the default way to build great products, so this was a solid step in that direction.</p>
<p>Now that I've created a formal course, I'm open to making more courses in the future. I've long considered doing this independently, which I might explore again at some point, but creating a course for Frontend Masters on a topic such as test-driven accessibility would be a fantastic fit.</p>
<p>Outside of course content, I blogged fairly consistently this year with 12 posts. I wrote a series on <a href="https://luhr.co/blog/2023/04/19/my-custom-second-brain-setup-part-1-why-go-custom/">my custom second brain setup</a>, which continues to be one of most valuable investments in my career and personal life. This year, I highlighted over 1,200 articles on design, development, and accessibility, and my second brain setup scaled perfectly for capturing those highlights and referencing them easily when working or creating new content.</p>
<p>I also wrote the first post in what will likely become a series on accessibility with <a href="https://luhr.co/blog/2023/09/12/all-about-accessible-headings/">All about accessible headings</a>. I'd like to write posts in this series about accessible links, buttons, landmark regions, etc. that cover all the considerations of foundational accessibility.</p>
<p>I have dozens and dozens of post ideas, so I expect to keep the blogging pace going, if not accelerate it.</p>
<p>In addition to blog content, I picked my <a href="https://youtube.com/@buildux">Build UX YouTube channel</a> back up with 3 new videos towards the end of the year. I found I really enjoyed writing a post, then using that post as a rough outline for a video, and then publishing them together as complimentary resources. With so many blog post ideas kicking around, I hope to get a rhythm of videos out alongside new posts.</p>
<p>To wrap up educational content for the year, I wrote an article for the <a href="https://www.htmhell.dev/adventcalendar/">2023 HTMHell Advent Calendar</a> called <a href="https://www.htmhell.dev/adventcalendar/2023/12/">Test-driven HTML and accessibility</a>. This article teaches the approach to testing that I've developed throughout this year and transformed how I work. This topic is something I plan on expanding a ton in 2024, as I'm confident it will provide a lot of value for improving and protecting accessibility.</p>
<h3>Learning</h3>
<p>As client work was lighter than usual, I was able to dedicate most of my time to self-directed learning and building.</p>
<p>The bulk of my learning was with test-driven development (TDD), which I was able to incorporate fully in both my personal and client work.</p>
<p>TDD has been the most valuable practice of my development career. It gives so much clarity, confidence, quality, and resiliency to coding. It eliminates nearly 100% of the waste with manual debugging and has made me a substantially better developer. It's also been an incredibly powerful learning tool, even as I've dived into unfamiliar codebases, libraries, problems, and languages.</p>
<p>In April, I attended a small workshop from <a href="https://www.jamesshore.com/">James Shore</a> on <a href="https://www.jamesshore.com/v2/projects/nullables">Nullables</a>, which is his technique for testing production code without hitting external systems. This approach eliminates the need for integration tests and mocks, which are slow and unreliable. Instead, you can turn off communication with the outside world and unit test functionality throughout the codebase.</p>
<p>The workshop was a bit of a stretch for my skills at the time. But a few months later, my main client was struggling with a problem that was the perfect candidate for this approach. With nullables in place, we were able to confidently unit test all of the functionality we were working on with tests that ran predictably and instantaneously. We even replaced existing clunky integration tests and reduced the test suite run time from several minutes down to a few hundred milliseconds.</p>
<p>The last frontier for testing that I wanted to solve was testing HTML and accessibility. James Shore kindly shared his past experiences using tools like Karma and Test'em, but these projects were now aging and I couldn't get them running in my projects.</p>
<p>Luckily, right as Karma was officially deprecated, I came across <a href="https://modern-web.dev/docs/test-runner/overview/">Web Test Runner</a>, which is a tool that runs JavaScript tests in the browser instead of in Node. This makes it possible to run tests in all the major browser engines (Chromium, WebKit, and Firefox) simultaneously in the background with every change. Most importantly, it means that tests have access to the real DOM and actual user interaction events. I cover this in depth in <a href="https://www.htmhell.dev/adventcalendar/2023/12/">my Test-driven HTML and accessibility article</a>, but in short I've been able to unit test everything I build, from logic, to integrations, to rendered DOM, to accessibility, all with a lightweight setup that has scaled perfectly to many hundreds of tests running at once.</p>
<h3>Product</h3>
<p>The reason why I was writing many hundreds of tests is I started exploring a product that I'd like to sell in the future. I'm not ready to discuss what the product is just yet, but I am excited for the value, quality, accessibility, and efficiency it'll bring to designers and developers alike.</p>
<p>This project was the central driver of my learning and will continue to inspire relevant educational content to share what I've found.</p>
<p>It quickly became the largest codebase I've built on my own and was a good proving ground for the level of polish I know is possible if teams prioritize the right things. While I've needed to continuously refine the codebase as it scales, having tests in place made this a calm and steady practice.</p>
<p>This experience was really clarifying for my career. I now am inspired to continue building this product on the side, and if I can one day focus on it full-time, I'd be thrilled and terrified to take on that challenge. While I could make a bet on it full-time, I'd rather come at it from a place of security and patience to do what's best in the long term.</p>
<h3>Working rhythm</h3>
<p>As I worked on my own projects throughout the year, I organically discovered and refined what is now my ideal working rhythm.</p>
<p>At a higher level, I used each month as a <a href="https://basecamp.com/shapeup">Shape Up</a> cycle, with the first 3 weeks focused on product work, and the last week acting as a cooldown focused on creating content and framing/shaping work for the next month.</p>
<p>This larger rhythm feels like the perfect balance of making meaningful progress on features and learning that is substantial and fresh enough to share in a blog post and video. While I didn't directly tie each month's post and video to what I was working on, I'd like to do this in the future. When I'm ready to announce the product I'm working on, I'll still use that last week for creating educational content, but it can use the product as a concrete example if needed. I feel providing value routinely will be more effective than any actual marketing.</p>
<p>At a daily level, I found similar balance. My ideal workday is now the following:</p>
<ul>
<li>8:00 AM to 9:30 AM: deep work</li>
<li>9:30 AM to 10:30 AM: break/shallow work</li>
<li>10:30 to 12:00 PM: deep work</li>
<li>12:00 PM to 1:00 PM: lunch</li>
<li>1:00 PM to 2:00 PM: deep work</li>
<li>2:00 PM onward: go live my life</li>
</ul>
<p>This schedule guarantees 4 hours of deep work, which is far above what most full-time roles can achieve. I find 90 minutes is the longest deep work block I can sustain without diminishing returns, and having hour-long breaks between blocks keeps me fresh, gives an outlet for less important work, and lets my brain subconsciously process important problems.</p>
<p>While I designed and refined this schedule for my solo product work, I found it works perfectly with clients as well. Each deep work block is perfect for pairing/mobbing sessions, and the 8:00 AM to 2:00 PM PST schedule has a lot of overlap with my clients in the EST timezone 3 hours ahead.</p>
<h2>Personal</h2>
<p>I love mountain biking, and this year I finally got into the habit of routinely volunteering to do trail work in our local trail system. It was a ton of fun learning about trail design, which has many parallels to software design, particularly when it comes to accessibility in the form of adaptive riding. It was also super rewarding to make major improvements to trails I love, and then immediately enjoy the improvements by riding as a group.</p>
<p>I made several new friends as part of this group and now have great people to ride with. We even did a mountain bike camping trip in the summer to check out other trail systems in the Alsea and Black Rock areas of Oregon which was one of the highlights of my year.</p>
<p>Outside of mountain biking, I started making wood fire pizzas routinely and was able to hone my Neapolitan dough skills. I had a ton of fun experimenting with different toppings, learning how to shape and stretch dough quickly, and growing ingredients in our garden. Everything always turns out great, and worst case, you just make a calzone if the dough isn't cooperating.</p>
<p>Lastly, I continued my Japanese learning habit with a 450 day streak. I studied in Kyoto, Japan in 2012 but lost much of my conversational skills and Kanji in the decade since. Last year, I got back into studying routinely, and I'm happy to have my speaking and reading coming back strong.</p>
<h2>Wrapping up</h2>
<p>Although this year didn't go according to plan, I feel I made the most of the time I had to evolve as a developer, improve my work and way of working, and lay the foundations for a future business.</p>
<p>While I'm looking forward to having more predictability and stability again, I now have new skills, resilience, and calm that will make this year of unsurety worth it in the long run.</p>
Aquileo | New article: Test-driven HTML and accessibility2023-12-21T00:00:00Zhttps://luhr.co/blog/2023/12/21/new-post-test-driven-html-and-accessibility/<h1>New article: Test-driven HTML and accessibility</h1>
<p>I wrote a new article for the <a href="https://www.htmhell.dev/adventcalendar/">2023 HTMHell Advent Calendar</a> called <a href="https://www.htmhell.dev/adventcalendar/2023/12/">Test-driven HTML and accessibility</a>.</p>
<p>I've been wanting to share my new approach to unit testing JavaScript and I'm really happy with how this article turned out.</p>
<p>For most of this year, I've been exploring the full potential of Test-Driven Development (TDD) in my work and found a tool called Web Test Runner that allows me to run unit tests directly in the browser. This makes testing HTML and user interaction in the DOM possible, which opens up a whole new way to do accessibility testing.</p>
<p>Learn more about this setup with helpful examples: <a href="https://www.htmhell.dev/adventcalendar/2023/12/">Test-driven HTML and accessibility</a></p>
<p>While you're there, check out all the other great articles for the HTMHell Advent Calendar!</p>
<p>Here are some of my favorites so far:</p>
<ul>
<li><a href="https://www.htmhell.dev/adventcalendar/2023/1/">The UX of HTML</a></li>
<li><a href="https://www.htmhell.dev/adventcalendar/2023/5/">The Hellish History of HTML: An incomplete and personal account</a></li>
<li><a href="https://www.htmhell.dev/adventcalendar/2023/6/">Web Components FTW!</a></li>
<li><a href="https://www.htmhell.dev/adventcalendar/2023/16/">Swallowing camels</a></li>
<li><a href="https://www.htmhell.dev/adventcalendar/2023/21/">The Implied Web</a></li>
</ul>
Aquileo | Build UX is 5 years old2023-11-29T00:00:00Zhttps://luhr.co/blog/2023/11/29/build-ux-is-5-years-old/<h1>Build UX is 5 years old</h1>
<p><a href="https://andy-bell.co.uk/my-company-is-5-today/">Andy Bell posted that his company is 5 today</a> and that reminded me that Build UX, <a href="https://www.youtube.com/@buildux">my YouTube channel</a> and now consulting company, is also 5 years old.</p>
<p>I rarely take time to reflect on my past work, so this milestone snuck up on me.</p>
<h2>How it started</h2>
<p>5 years ago, I was mentoring a couple people who wanted to get into frontend development. I strongly believe in applied learning, so I found interesting designs on Dribbble, recreated them in an emerging design tool called Figma (heard of it?) that I started using in 2017, and coached them how to build a responsive website with professional HTML and CSS.</p>
<p>The people I was mentoring took to the process and produced really high quality projects while quickly learning important fundamentals of frontend development. After wrapping up this round of mentoring, I thought it'd be valuable to share this approach more widely with my dual design and development background.</p>
<p>I started making videos on Build UX to demonstrate my process of turning visual designs into resilient websites with semantic HTML, scalable CSS, and accessibility. That new design tool, Figma, was starting to gain adoption, so I began making tutorials on creating "responsive" components and breaking designs into hierarchical components with <a href="https://atomicdesign.bradfrost.com/">Atomic design</a>.</p>
<h2>How it went</h2>
<p>When you create content, you demonstrate your approach, thinking, and experience.</p>
<p>This is very effective for learners, but creating free educational content has also lead to nearly every career opportunity I've had in the last 5 years. This includes conference talks and workshops, articles for other publications, <a href="https://www.linkedin.com/learning/accessibility-first-design">a LinkedIn Learning course</a>, job offers, and consulting and coaching clients.</p>
<p>Similar to Andy, I was fully booked through the pandemic with long-term clients, and continue to get organic inquiries without ever searching for clients.</p>
<h2>Where it's going</h2>
<p>After transitioning into consulting full-time in 2020, I haven't created videos consistently. But this year I've roughly settled into a rhythm of a monthly video and companion blog post that I'd like to continue. I've been creating more focused videos to solve specific accessible design and development problems, and I also want to teach my latest approaches to TDD, JavaScript, and accessibility testing.</p>
<p>I love the freedom and impact I have as an independent consultant with long-term clients. I can act as an embedded team member, directly coach, solve problems, and contribute work, and have the flexibility to dig deep into research projects or create larger educational content. If you're interested in working together on accessible design and development, reach out: hello@luhr.co</p>
<p>There's one more destination on my horizon that I'd like to explore in the next 5 years: starting a product business. I'm not ready to share details yet, but over the past 2 years I've been increasingly investing time in building something I'm confident will provide a ton of value to designers and developers alike.</p>
<p>Thank you for watching my videos, reading my posts, hiring me to solve meaningful problems, and supporting me over these past 5 years. I'm excited to keep Build UX going strong.</p>
Aquileo | All about accessible headings2023-09-12T00:00:00Zhttps://luhr.co/blog/2023/09/12/all-about-accessible-headings/<h1>All about accessible headings</h1>
<p><a href="https://www.youtube.com/watch?v=SvybQ7cECts">Watch the companion video to this post on YouTube</a></p>
<h2>What is a heading?</h2>
<p>A heading is text that describes a section of content.</p>
<p>In HTML, we have 6 heading elements (<code><h1></code>, <code><h2></code>, <code><h3></code>, <code><h4></code>, <code><h5></code>, <code><h6></code>) to create different heading levels. This allows us to create a hierarchical outline to structure our content.</p>
<h3>Heading, header, title</h3>
<p>It's important to always refer to these elements as headings. Don't confuse it with the term "header". In HTML, a <code><header></code> element is a landmark region that semantically groups introductory content, such as the top navigation, search, and settings of a website. Similarly, don't confuse it with the term "title". In HTML, the <code><title></code> element provides meta information about the page that is used in the browser's tab, and appears in search results. Unrelated, there's also the <code>title</code> attribute, which is a poorly supported HTML attribute that provides a tooltip on mouse hover, and is inconsistently announced by different assistive technology.</p>
<h2>Why are headings useful?</h2>
<p>Headings allow people to understand the structure of content and quickly identify relevant information.</p>
<p>For sighted users, headings are often styled distinctly from other text to allow people to visually scan a page and jump between areas of interest.</p>
<p>For assistive technology users, headings provide an overview of the webpage and even allow for quick navigation, such as navigating with the tree structure of all headings, moving to the previous or next heading, or moving to the previous or next heading of a specific heading level.</p>
<p>Like good accessibility in general, in addition to helping people, proper heading structure is also good for SEO. Headings allow search engine crawlers to understand the structure of a page, and search engines reward good heading use.</p>
<h2>What makes for a good heading?</h2>
<p>A good heading, like all good copywriting, should be descriptive, clear, and concise. It should also be unique, at least within the current section of content, to help users identify different chunks of information.</p>
<p>It's best to not overuse headings, especially with more granular content, as this dilutes their usefulness. If content can be grouped thematically, then it would benefit from a heading. If a piece of information or data needs a label, then a heading is likely too much, and other semantic HTML, such as the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl">description list (dl)</a> might be a better fit.</p>
<h3>Myth: <code><hgroup></code> adds semantic content to headings</h3>
<p>HTML 5 over ambitiously introduced the <code><hgroup></code> element, which was intended to associate a heading with one or more paragraphs to act as supplemental heading information:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>hgroup</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>Pricing<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>Plans that scale with your rapidly growing business<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>hgroup</span><span class="token punctuation">></span></span></code></pre>
<p>However, no assistive technology communicates any additional semantics with <code><hgroup></code>, so the heading and paragraph are treated normally. As a result, it's best to ignore this element.</p>
<h2>What are heading levels?</h2>
<p>Headings should be hierarchical, with different heading levels (1 to 6) to structure the relationship of content. The hierarchy of headings should be intuitive and consistent across similar sections.</p>
<h2>How should we use heading levels?</h2>
<p>Heading levels should create a logical outline for content that communicates the relationship of different sections, much like an indented bullet list.</p>
<p>Although HTML offers 6 levels of headings, try to create content that doesn't require more than 3 or 4. Content that repeatedly requires more than 3 heading levels probably means the content is too complex and can be edited or broken into distinct pages.</p>
<h3>Rule: don't use headings for visual styling</h3>
<p>As with all accessibility, visual styling and semantics are not the same, and should be treated separately.</p>
<p>Browsers have default CSS styles that provide a bold font weight for headings and a typographic scale that decreases font size from <code><h1></code> (the largest) to <code><h6></code> elements (the smallest).</p>
<p>As a result, some developers mistakenly choose the heading level based on the font-size they want instead of the appropriate level based on the content's structure.</p>
<p>Similarly, some developers mistakenly turn normal, paragraph text into a heading to make it bold instead of using the <code>font-weight</code> property in CSS.</p>
<p>Don't use headings for visual styling. Use headings to describe sections of content, use other text HTML elements (<code><p></code>, <code><li></code>, <code><dt></code>, <code><dd></code>, etc.) for non-heading text, and use CSS to achieve font size and font weight.</p>
<h3>Rule: use a single, unique <code><h1></code> per page</h3>
<p>Pages should have at least one heading, and every page's heading outline should begin with a single, unique heading that clearly and succinctly identifies the main focus of that page.</p>
<p>This <code><h1></code> heading should be unique to that page and shouldn't be used as the <code><h1></code> on any other page for the site.</p>
<p>Using more than one <code><h1></code> for a page creates an illogical structure, like a book having more than one title on the front cover.</p>
<h3>Myth: the HTML outline algorithm</h3>
<p>The HTML 5 spec included the HTML outline algorithm, which promised to automatically handle heading levels based on the nested structure of landmark region elements, such as <code><main></code>, <code><article></code>, <code><aside></code>, and <code><section></code>.</p>
<p>With this approach, developers would use <code><h1></code> elements for the first heading in each nested section, and the structure of the landmark elements would inform the proper heading level semantics. If you implement this structure in HTML, the default browser CSS styles will actually modify the font-size of <code><h1></code> elements that mimics the different headings levels.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/G97oLnR31Y-320.avif 320w, https://luhr.co/assets/images/generated/G97oLnR31Y-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/G97oLnR31Y-320.webp 320w, https://luhr.co/assets/images/generated/G97oLnR31Y-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/G97oLnR31Y-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/G97oLnR31Y-320.jpeg 320w, https://luhr.co/assets/images/generated/G97oLnR31Y-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Browser default styles of h1 elements with cascading font-sizes in nested sections</figcaption>
</figure>
<p>However, the HTML outline algorithm was never implemented in assistive technology. This approach of using <code><h1></code> elements in each section results in a flat outline with multiple level-one headings, which is confusing and not very useful.</p>
<p>In 2018, the W3C and WHATWG "retired" HTML 5 and introduced the HTML living standard. With the living standard, we no longer have version numbers for HTML, and the HTML outline algorithm is not advised.</p>
<p>Instead, use <code><h1></code> to <code><h6></code> to create a logical outline for content. These heading levels can be used regardless of the structure of landmark region elements, but pair nicely with well-structured landmarks.</p>
<h3>Rule: don't skip heading levels</h3>
<p>A heading should not be more than 1 level deeper than the previous heading.</p>
<p>If the previous heading is <code><h3></code>, then the following heading can be <code><h2></code> (a new section unrelated to the previous heading), <code><h3></code> (a new section as a sibling to the previous heading), or <code><h4></code> (a new section within the previous heading). It can't be <code><h1></code>, because there should only be one <code><h1></code> as the first heading of the page, and it can't be <code><h5></code> or <code><h6></code>, because that would skip a heading level.</p>
<h3>A good heading outline</h3>
<p>Applying these rules together results in a logical heading outline that's similar to an indented list:</p>
<pre class="language-html"><code class="language-html"><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>h1</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>...<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>h3</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 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>h2</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>...<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>h3</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 punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h4</span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h4</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 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 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>h2</span><span class="token punctuation">></span></span></code></pre>
<h2>Headings and landmarks</h2>
<p>While the HTML outline algorithm was never really a thing, headings and landmarks provide rich semantics when used together. Landmark region elements, such as <code><main></code>, <code><article></code>, <code><aside></code>, and <code><section></code>, benefit from dedicated headings.</p>
<p>In fact, the <code><section></code> element, has no semantic value as a landmark unless it has an accessible name. This is achieved with the <code>aria-labelledby</code> attribute, which points to the <code>id</code> attribute of a heading:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span> <span class="token attr-name">aria-labelledby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>features-section-heading<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>features-section-heading<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Features<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>section</span><span class="token punctuation">></span></span></code></pre>
<p>With this approach, the overall HTML structure of a page utilizes landmark elements and headings working together:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>header</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 comment"><!-- ... --></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>header</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>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>Example, Inc.<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>section</span> <span class="token attr-name">aria-labelledby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>features-section-heading<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>features-section-heading<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Features<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- ... --></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">aria-labelledby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>plans-section-heading<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>plans-section-heading<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Plans<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>h3</span><span class="token punctuation">></span></span>Starter<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h3</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- ... --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h3</span><span class="token punctuation">></span></span>Growth<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h3</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- ... --></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>
<h2>Accessibility-first headings</h2>
<p>An accessibility-first workflow incorporates accessibility from the start of any project and preserves it across content design, visual design, and development.</p>
<p>It's useful to determine and note heading levels directly in content documents.</p>
<p>If you're authoring content in Markdown, this is easily achieved with <code>#</code> for <code><h1></code>, <code>##</code> for <code><h2></code>, <code>###</code> for <code><h3></code>, and so on.</p>
<p>In Google Docs, it's possible to style text as "Heading 1" to "Heading 6" using the dropdown menu, or keyboard shortcuts (<code>Ctrl/Cmd + Alt/Opt + 1</code> for <code><h1></code>). It's still a good idea to note what the heading level is with Markdown's <code>#</code> approach to make it clear and convenient to implement in code.</p>
<p>If you're authoring content directly in HTML (as I recommend), then there's no need to separately document heading levels. Getting content into the browser as soon as possible without any styling allows us to evaluate the structure and accessibility of the page early on, and provides a working baseline that we protect throughout the rest of the project.</p>
<p>With raw HTML in the browser, we can then incorporate visual design. Again, it's critical to separate semantics from visual styling. This is why noting heading levels when creating content or working directly in HTML is so important. It frees the visual design to style typography however is needed, whether it's heading or paragraph text. If a heading ultimately isn't going to be visually shown in the final design, it's already in place and can be accessibly hidden using CSS.</p>
<p>With this process, you are much more likely to have well-structured, accessible headings in any design and UI.</p>
<h2>Solutions to common design patterns</h2>
<p>Beyond heading levels being misused to achieve a particular font-size, I typically find issues with headings in some common design patterns. Luckily, there are easy solutions that allow us to achieve the visual design of these patterns while providing accessible headings.</p>
<h3>Eyebrow text followed by large text</h3>
<p>This design pattern appears a lot in marketing sites, especially sections describing different app or service features. The eyebrow text is small, brief text that appears before larger, more descriptive or narrative text.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/FYX98Mtskk-320.avif 320w, https://luhr.co/assets/images/generated/FYX98Mtskk-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/FYX98Mtskk-320.webp 320w, https://luhr.co/assets/images/generated/FYX98Mtskk-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/FYX98Mtskk-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/FYX98Mtskk-320.jpeg 320w, https://luhr.co/assets/images/generated/FYX98Mtskk-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Feature sections on Stripe's homepage incorrectly use h2 for eyebrow text followed by h1 for narrative text in stead of h2 for eyebrow text and a paragraph for narrative text.</figcaption>
</figure>
<p>I often find this incorrect HTML structure:</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 punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>Pricing<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>h2</span><span class="token punctuation">></span></span>Plans that scale with your rapidly growing business<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- ... --></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>To fix this, we can simply make the eyebrow text the heading, and the larger text a paragraph. We can also enhance this chunk of content with a landmark section:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span> <span class="token attr-name">aria-labelledby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pricing-section-heading<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pricing-section-heading<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Pricing<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>Plans that scale with your rapidly growing business<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- ... --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span></code></pre>
<h3>Lack of (hidden) headings</h3>
<p>Sometimes visual designs don't accommodate headings in particular visual regions, or an accurate heading feels too formal compared to more narrative, marketing-oriented text. This seems to be common with missing <code><h1></code> headings on pages that only show sub-sections or have a marketing tagline that doesn't do a good job of describing the site or page as a whole.</p>
<p>In this case, it's best to add headings to properly describe the site, page, or section as needed.</p>
<p>While I recommend visually shown headings whenever possible, this can introduce design challenges that might discourage fixing the issue of missing headings. As a start, at least improve the heading outline with accessible, visually hidden headings while exploring better long-term solutions.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/j2u8LwBxSx-320.avif 320w, https://luhr.co/assets/images/generated/j2u8LwBxSx-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/j2u8LwBxSx-320.webp 320w, https://luhr.co/assets/images/generated/j2u8LwBxSx-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/j2u8LwBxSx-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/j2u8LwBxSx-320.jpeg 320w, https://luhr.co/assets/images/generated/j2u8LwBxSx-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Stripe's homepage incorrectly uses narrative text as the h1 instead of a visually hidden h1 with the text "Stripe".</figcaption>
</figure>
<h3>Home link is <code><h1></code></h3>
<p>Sometimes I come across top navigation that uses a <code><h1></code> element in the home page link.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>header</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 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>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Example, Inc.<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>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 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>/pricing<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Pricing<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 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>/about<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>About<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 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>nav</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>header</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>
<span class="token comment"><!-- --></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>This results in a few issues:</p>
<ol>
<li>The <code><h1></code> isn't located near the main content that it describes</li>
<li>The <code><h1></code> is the same across every page</li>
<li>The <code><h1></code> of the website/brand name is only relevant for the home page</li>
</ol>
<p>To correct this, the top navigation should simply contain links. On the home page, a <code><h1></code> with the website/brand name should be included in the <code><main></code> section. On other pages, the <code><h1></code> should provide a descriptive heading for that particular page.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>header</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>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 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>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Example, Inc.<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 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>/pricing<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Pricing<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 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>/about<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>About<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 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>nav</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>header</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>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>Example, Inc.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- --></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>Linked headings</h3>
<p>Headings and links often go together for lists of articles or to provide anchor links within a page. However, people can make the mistake of wrapping the heading in a link.</p>
<pre class="language-html"><code class="language-html"><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>/a-year-in-review<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>A year in review<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>a</span><span class="token punctuation">></span></span></code></pre>
<p>Instead, the heading should contain the link:</p>
<pre class="language-html"><code class="language-html"><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>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>/a-year-in-review<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>A year in review<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>h2</span><span class="token punctuation">></span></span></code></pre>
<h2>Evaluating the heading outline</h2>
<p>There are helpful tools and manual testing approaches to evaluate the heading outline whether you're improving an existing project or working on a new project.</p>
<p>The WAVE browser extension by WebAIM provides a helpful heading outline and annotates any headings directly on the page. It also flags incorrect heading usage and potential headings.</p>
<p>With these findings, it's helpful to created a bulleted list of the heading outline to share with others and demonstrate whether the current structure makes sense.</p>
<p>It's also valuable to review headings with a screen reader. You can evaluate the page's heading structure and experience if the wording is distinct and understandable.</p>
<p>In NVDA on Windows, use the Elements List with <code>Insert + F5</code> to get all headings on a page and review their tree order or jump between headings. You can also use the <code>H</code> key to jump to the next heading and <code>Shift + H</code> to jump to the previous heading.</p>
<p>In VoiceOver on MacOS, use Rotor Mode with <code>Control + Option + U</code> to get a similar list of all headings.</p>
<h2>Summary</h2>
<ul>
<li>Don't confuse "heading" with "header" or "title"</li>
<li>Write clear, concise, descriptive headings</li>
<li>Only include a single <code><h1></code> per page, that's unique to that page</li>
<li>Use heading levels to create a logical heading outline
<ul>
<li>Plan heading levels early on and test the heading outline throughout a project</li>
</ul>
</li>
<li>Don't skip heading levels</li>
<li>Aim for less complex content that doesn't require all 6 heading levels</li>
<li>Don't use headings to achieve visual styling</li>
</ul>
Aquileo | All the ways to render a WebC component with 11ty2023-07-11T00:00:00Zhttps://luhr.co/blog/2023/07/11/all-the-ways-to-render-a-webc-component-with-11ty/<h1>All the ways to render a WebC component with 11ty</h1>
<p><a href="https://www.youtube.com/watch?v=0I3yQxukg2k">Watch the companion video to this post on YouTube</a></p>
<p>One of my favorite features of WebC is the variety of rendering options it provides. This allows full control to use WebC for HTML templating, custom elements, or web components (with or without the shadow DOM).</p>
<p>All of these rendering options offer some major benefits:</p>
<ul>
<li>Explicit control over the final output for clean markup</li>
<li>Write HTML freely in components. There are no strange limitations, like Fragments in React to render multiple top-level elements.</li>
<li>Slots (with the <code><slot></code> element and <code>slot</code> attribute), including named slots, which alleviate the need for attributes (props) and offer even more control over the final output</li>
<li>Advanced templating with <code>webc:if</code>, <code>webc:elseif</code>, <code>webc:else</code>, and <code>webc:for</code></li>
<li>Components can nest other components. WebC layouts are just components, so layouts can be nested as well.</li>
</ul>
<h2>Which rendering option to use</h2>
<p>Each rendering option has different tradeoffs, so it can be difficult to know which to use for different situations.</p>
<p>Do you want basic HTML templating?</p>
<ul>
<li>Simple HTML templating</li>
<li>Replace an element with the component's markup</li>
</ul>
<p>Do you want scoped styles?</p>
<ul>
<li>Overloading an HTML element</li>
<li>Scoped styles without using a custom element tag</li>
<li>Custom element</li>
</ul>
<p>Do you want JavaScript?</p>
<ul>
<li>Dynamic content with JavaScript render functions (server-side)</li>
<li>Custom element (server-side and global client-side)</li>
<li>Web component (client-side)</li>
</ul>
<p>I prefer the least-powerful options based on my needs, and prioritize options that don't output a custom element tag. Custom element tags wrap around the component's internal markup, which can make layout and semantics more challenging in certain situations. This is somewhat alleviated with <code>display: contents;</code>, but I avoid it when possible.</p>
<h2>Option 1: Simple HTML templating (the default)</h2>
<p>The most common use case for components is HTML templating to reuse chunks of markup across a site, also known as partials or includes.</p>
<p>By default, WebC with 11ty allows you to write any markup you want in a component, use the component's custom element tag in your page, and it'll simply render the markup inside the component without the custom element tag.</p>
<p>In <code>my-component.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Hello from the component<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span></code></pre>
<p>In <code>page.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>my-component</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>my-component</span><span class="token punctuation">></span></span></code></pre>
<p>Rendered output at <code>/page</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Hello from the component<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span></code></pre>
<ul>
<li>Benefits
<ul>
<li>Reuse markup</li>
<li>Doesn't render a custom element tag</li>
</ul>
</li>
<li>Limitations
<ul>
<li>Can't use scoped styles</li>
<li>Can't use JavaScript</li>
</ul>
</li>
</ul>
<h2>Option 2: Replace an element with the component's markup</h2>
<p>This works identically to the default behavior, but allows you to replace any HTML element with a WebC component, without using the custom element tag.</p>
<p>This approach is required when rendering WebC components in the <code><head></code> element, since custom elements aren't allowed inside <code><head></code>.</p>
<p><a href="https://www.11ty.dev/docs/languages/webc/#components">Read 11ty's WebC documentation on <code><head></code> components</a></p>
<p>In <code>my-component.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Hello from the component<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span></code></pre>
<p>In <code>page.webc</code></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"><span class="token namespace">webc:</span>is</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>my-component<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>Rendered output at <code>/page</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Hello from the component<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span></code></pre>
<ul>
<li>Benefits
<ul>
<li>Reuse markup</li>
<li>Works in the <code><head></code> element</li>
<li>Doesn't output a custom element tag</li>
</ul>
</li>
<li>Limitations
<ul>
<li>Can't use scoped styles</li>
<li>Can't use JavaScript</li>
</ul>
</li>
</ul>
<h2>Option 3: Overloading an HTML element</h2>
<p>The custom element spec requires a hyphen in custom element names to avoid conflicts with native HTML elements. However, WebC intentionally allows for naming conflict to make "overloading" native HTML elements possible.</p>
<p>When a WebC component has the same name as a native HTML element, the component will replace any instance of that element.</p>
<p>This makes enhancements to native elements possible, such as always including an <code>alt</code> attribute on an <code><img></code> that defaults to an empty string.</p>
<p>This approach also allows for scoped styles without outputting a custom element tag. Using <code><style webc:scoped></code> in the <code>.webc</code> file will tie all styles to the component. 11ty and WebC do this by creating a hashed CSS class name that's added to the component's root-level element, such as <code><div class="weljrwcay"></code>.</p>
<p>With scoped styles, it's possible to select the root-level element with the <code>:host</code> pseudo-selector. This follows the shadow DOM standard for selection a shadow root host, but in the case of WebC, it can be used for any scoped styles, regardless of creating an actual Web Component with a shadow DOM.</p>
<p>11ty will automatically aggregate any styles (scoped or global) it finds in WebC components. All that's needed is to include the CSS bundle in the base layout file:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span> <span class="token attr-name">@raw</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>getBundle('css')<span class="token punctuation">"</span></span> <span class="token attr-name"><span class="token namespace">webc:</span>keep</span><span class="token punctuation">></span></span><span class="token style"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span></code></pre>
<p>In <code>footer.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Heading with red text color<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>style</span> <span class="token attr-name"><span class="token namespace">webc:</span>scoped</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css">
<span class="token selector">h2</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></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span></code></pre>
<p>In <code>page.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Heading with normal text color<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>footer</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>footer</span><span class="token punctuation">></span></span></code></pre>
<p>Rendered output at <code>/page</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Heading with normal text color<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>footer</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>Heading with red text color<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>footer</span><span class="token punctuation">></span></span></code></pre>
<ul>
<li>Benefits
<ul>
<li>Enhance native elements</li>
<li>Can use scoped styles</li>
<li>Doesn't output a custom element tag</li>
</ul>
</li>
<li>Limitations
<ul>
<li>Must commit to overriding every instance of a native element</li>
<li>Can't use JavaScript</li>
</ul>
</li>
</ul>
<h2>Option 4: Scoped styles without outputting a custom element tag</h2>
<p>If you don't want to overload an HTML element but still want scoped styles, you can override the component's custom element tag with a native HTML element of your choice.</p>
<p>This is useful for creating variations of a common semantic element, such as <code><header></code> elements, without having a custom element tag getting in the way.</p>
<p>This approach makes use of the <code>webc:root="override"</code> attribute, which will replace the custom element tag.</p>
<p>In <code>my-component.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>header</span> <span class="token attr-name"><span class="token namespace">webc:</span>root</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>override<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>Hello from the component<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>header</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span> <span class="token attr-name"><span class="token namespace">webc:</span>scoped</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css">
<span class="token selector">p</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></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span></code></pre>
<p>In <code>page.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>my-component</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>my-component</span><span class="token punctuation">></span></span></code></pre>
<p>Rendered output at <code>/page</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>header</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>Hello from the component<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>header</span><span class="token punctuation">></span></span></code></pre>
<ul>
<li>Benefits
<ul>
<li>Don't have to commit to overloading a native element</li>
<li>Can use scoped styles</li>
<li>Doesn't output a custom element tag</li>
</ul>
</li>
<li>Limitations
<ul>
<li>Can't use JavaScript</li>
</ul>
</li>
</ul>
<h2>Option 5: Dynamic content with JavaScript render functions</h2>
<p>Many components require more dynamic content, such as accessing data from 11ty's data cascade or adding timestamps.</p>
<p>This is achieved with JavaScript render functions, which run server JavaScript in a component. JavaScript render functions can be the entire component, or used within a component's markup just for the dynamic portions.</p>
<p>All it requires is a <code><script></code> tag in the <code>.webc</code> file with the <code>webc:type="js"</code> attribute. This render function outputs the last line of code, which is usually a template string of HTML.</p>
<p>Because this is server JavaScript, there won't be any JS bundling or client-side functionality.</p>
<p>In <code>my-component.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name"><span class="token namespace">webc:</span>type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword">const</span> currentDate <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><h2>Hello from </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>currentDate<span class="token punctuation">.</span><span class="token function">getFullYear</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"></h2></span><span class="token template-punctuation string">`</span></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></code></pre>
<p>In <code>page.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>my-component</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>my-component</span><span class="token punctuation">></span></span></code></pre>
<p>Rendered output at <code>/page</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Hello from 2023<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span></code></pre>
<ul>
<li>Benefits
<ul>
<li>Can use server-side JavaScript</li>
<li>Allows access to 11ty's data cascade</li>
<li>Allows for more advanced templating, such as looping and variables</li>
<li>Doesn't output a custom element tag</li>
</ul>
</li>
<li>Limitations
<ul>
<li>No client-side functionality</li>
<li>Can't add styles (global or scoped) within a render function</li>
<li>Have to author HTML in a template string (no syntax highlighting, autocomplete, linting, etc.)</li>
</ul>
</li>
</ul>
<h2>Option 6: Custom element</h2>
<p>By default, WebC will output the custom element tag any time there are scoped styles or JavaScript in a component.</p>
<p>Custom elements are mainly useful for advanced templating with <code>webc:if</code>, <code>webc:elseif</code>, <code>webc:else</code>, and <code>webc:for</code> that rely on JavaScript. Similar to JavaScript render functions, 11ty will run server JavaScript in a custom element component and statically render the output.</p>
<p>Because 11ty will bundle any styles and JavaScript it finds in WebC components, it's possible to add global client-JavaScript in a custom element component by simply including a <code><script></code> tag. But, this can be hard to maintain, so I wouldn't recommend it.</p>
<p>Custom element tags need to have a hyphenated name to avoid naming collision with native HTML elements, unless you're intentionally overloading an HTML element. Browsers handle custom element tags as a generic element, similar to a <code><div></code>.</p>
<p>Similar to styles, 11ty will automatically aggregate any client-side JavaScript it finds across WebC files into a bundle. If your <code>.webc</code> file includes any (global) client-side JavaScript, be sure to source the bundle in your base layout file:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</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 attr-name">@raw</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>getBundle('js')<span class="token punctuation">"</span></span> <span class="token attr-name"><span class="token namespace">webc:</span>keep</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></code></pre>
<p>In <code>my-component.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Hello from the component<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span></code></pre>
<p>In <code>page.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>my-component</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>my-component</span><span class="token punctuation">></span></span></code></pre>
<p>Rendered output at <code>/page</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>my-component</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>Hello from the component<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>my-component</span><span class="token punctuation">></span></span></code></pre>
<ul>
<li>Benefits
<ul>
<li>Allows for more advanced templating while authoring normal HTML</li>
<li>Can use server-side or global client-side JavaScript</li>
<li>Can use scoped styles</li>
</ul>
</li>
<li>Limitations
<ul>
<li>There are better options for scoped styles without outputting a custom element tag</li>
<li>Custom element tags may introduce some challenges with layout and semantics</li>
<li>Can't use scoped client-side JavaScript</li>
</ul>
</li>
</ul>
<h2>Option 7: Web component</h2>
<p>At last, we arrive at a full-on Web Component, with all its powers and quirks. I only reach for Web Components if I need client-side interactivity and functionality.</p>
<p>As with any client-side JavaScript, it's best to consider progressive enhancement in case JavaScript doesn't load or is disabled.</p>
<p>Additionally, WebC works with the 11ty <code>is-land</code> plugin for hydrating Web Components after the page has loaded. This layers interactivity on top of the statically-rendered content.</p>
<p>Because Web Components depend on client-side JavaScript, we need to source the JS bundle in our base layout file, if we haven't already:</p>
<pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</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 attr-name">@raw</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>getBundle('js')<span class="token punctuation">"</span></span> <span class="token attr-name"><span class="token namespace">webc:</span>keep</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></code></pre>
<p>The <code>type="module"</code> is technically only needed if a WebC component is an actual Web Component, but I always include it so I can freely upgrade any component to a full Web Component.</p>
<p>In <code>my-component.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</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 punctuation">></span></span>Log to the console<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>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
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">"my-component"</span><span class="token punctuation">,</span> <span class="token keyword">class</span> <span class="token class-name">extends</span> HTMLElement <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">const</span> button <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">":scope button"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
button<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 punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Hello from the component"</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></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>In <code>page.webc</code></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>my-component</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>my-component</span><span class="token punctuation">></span></span></code></pre>
<p>Rendered output at <code>/page</code></p>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- Logs "Hello from the component" to the console on click --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</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 punctuation">></span></span>Log to the console<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span></code></pre>
<ul>
<li>Benefits
<ul>
<li>Full Web Component spec</li>
<li>Light or Shadow DOM</li>
<li>Can use scoped styles</li>
<li>Can use scoped client-side JavaScript</li>
</ul>
</li>
<li>Limitations
<ul>
<li>Custom element tags may introduce some challenges with layout and semantics</li>
<li>Need to think through progressive enhancement and hydration</li>
</ul>
</li>
</ul>
<h2>Summary</h2>
<p>These options can be overwhelming, but they provide complete control over the output from WebC components, which is a major win for styling, semantics, and accessibility. I find that WebC results in writing less code and create cleaner output than any framework I've used.</p>
<h2>Resources</h2>
<p>The <a href="https://www.11ty.dev/docs/languages/webc/">official WebC documentation by 11ty</a> details all the features, many of which I couldn't cover in this post.</p>
<p>Shout out to <a href="https://11ty.webc.fun/">11ty & WebC</a> by W. Evan Sheehan has great articles that dig into the nuances and capabilities of WebC with helpful examples.</p>
Aquileo | Prompt engineering is shaping2023-05-16T00:00:00Zhttps://luhr.co/blog/2023/05/16/prompt-engineering-is-shaping/<h1>Prompt engineering is shaping</h1>
<p>Prompt engineering is an emerging practice of creating the most effective prompts to get valuable responses from AI tools like ChatGPT or GitHub Copilot.</p>
<p>Most people will ask ChatGPT a simple, single question and may be underwhelmed by the quality of the response. But, people are finding that there is wide variability in the depth and quality of responses based on the structure and quality of the prompt.</p>
<p>While this practice will continuously shift and evolve alongside each new AI model or version, the early emerging techniques are noticeably more effective and even a little surprising.</p>
<p>Better prompts provide a lot of detail to set context, considerations, constraints, expected format, and more.</p>
<p>A successful prompt might include the following:</p>
<ol>
<li>Tell the AI their role, who they are, and/or what perspective they should adopt</li>
<li>The desired outcome, goal, or solution</li>
<li>Constraints and details</li>
<li>Request the AI to ask questions before responding, and ask if the prompt makes sense</li>
<li>Define the expected output, such as "step-by-step instructions"</li>
</ol>
<p>An example of a higher quality prompt is:</p>
<blockquote>
<p>You are an experienced game developer with expertise in the Godot 4 game engine. You are creating a new racing game with realistic vehicle physics and handling, including independent suspension, manual shifting, tire condition, and surfaces with different friction materials. While there will be different car models with different engine parameters, the player will not be able to tune the vehicle. Your task is to create the core vehicle game object. The vehicle is a central focus of the game, so the implementation should be robust. First, provide step-by-step instructions of how you will approach this task. Once the instructions are finalized, you will next provide the corresponding code in GDScript. Please ask any questions you may have before generating a response. Does that sound alright?</p>
</blockquote>
<p>Note the frame-setting and depth of the prompt, and how it sets up the AI as a partner in the process. I've found requesting the AI to ask clarifying questions is particularly effective, as it will do a fair bit of discovery to further understand the job to be done and constrain the response.</p>
<p>Essentially, this is a form of shaping, the practice of defining and de-risking potential solutions.</p>
<p>Shaping was popularized by <a href="https://basecamp.com/shapeup">the book Shape Up</a>, and the output of the shaping process is a <a href="https://basecamp.com/shapeup/1.5-chapter-06">pitch document</a> with the following:</p>
<ol>
<li>Problem — The raw idea, a use case, or something we’ve seen that motivates us to work on this</li>
<li>Appetite — How much time we want to spend and how that constrains the solution</li>
<li>Solution — The core elements we came up with, presented in a form that’s easy for people to immediately understand</li>
<li>Rabbit holes — Details about the solution worth calling out to avoid problems</li>
<li>No-gos — Anything specifically excluded from the concept: functionality or use cases we intentionally aren’t covering to fit the appetite or make the problem tractable</li>
</ol>
<p>Effective prompt engineering (and answering questions from the AI) should cover all the areas of a defined pitch. It's important to note that the solution in a pitch is high-level but well defined, and the AI is tasked with carrying out the specific implementation.</p>
<p>Sometimes, it's unclear what potential solutions may even exist, especially when learning a new skill or topic. In this case, it's valuable to engage the AI as a partner in the shaping process itself, by crafting a prompt that establishes the problem space, the desired outcome, what in-scope, out-of-scope, and so forth.</p>
<p>As I started learning about prompt engineering, I was surprised that the biggest improvements seem to come from humanizing the AI, such as telling them who they are, requesting that they ask for clarification, or asking if they understand.</p>
<p>With decades of figuring out how to write cold, unnatural prompts for search engines, it's new but more intuitive to be able to write prompts that are closer to having a normal conversation. And, there will be more alignment with the shaping work I do with my clients and the prompt engineering I do with AI, which will push my shaping skills further.</p>
<p>The main quality of effective prompt engineering is engaging AI as a pairing partner or mentor in the process. By treating AI as a collaborator, not as something we merely delegate to, it produces more value.</p>
<p>If an artificial intelligence benefits from this treatment, then surely we can do better to engage individual contributors as equals with mutual respect and involvement in shaping our work.</p>
Aquileo | My Custom Second Brain Setup Part 4: Limitations as Strengths2023-04-26T00:00:00Zhttps://luhr.co/blog/2023/04/26/my-custom-second-brain-setup-part-4-limitations-as-strengths/<h1>My custom second brain setup, part 4: Limitations as strengths</h1>
<nav aria-labelledby="label-series-posts">
<p id="label-series-posts">Posts in this series:</p>
<ol>
<li>
<a href="https://luhr.co/blog/2023/04/19/my-custom-second-brain-setup-part-1-why-go-custom">Why go custom?</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/21/my-custom-second-brain-setup-part-2-how-it-works">How it works</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/24/my-custom-second-brain-setup-part-3-minimalist-productivity">Minimalist productivity</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/26/my-custom-second-brain-setup-part-4-limitations-as-strengths/" aria-current="page">Limitations as strengths (this post)</a>
</li>
</ol>
</nav>
<p>The main limitations (and strengths) of my second brain setup come from Markdown and Git:</p>
<p>There's no great way to handle images, which can be useful for design inspiration (user interfaces, home interior design, etc.). Luckily I rarely need them for notes or reference, so I usually just transcribe the information in text for things like diagrams and visual explanations. If I do need them, I co-locate images in the same folder as the related note, and can preview them in VSCode or with the file explorer.</p>
<p>I don't have a solution for linking between Markdown files. Sometimes I'll just create a link in Markdown with the file name and path, which I'll use to simply search for the file using the command palette in VSCode or the file tree. Having backlinks like Obsidian, Roam, or Dendron offer would be cool, but I'm not sure I want to invest in making custom tooling or finding an extension for this.</p>
<p>My second brain setup doesn't allow for any sharing or collaboration. For me, that's fine and intentional as this is a private workspace for all areas of my life, and I can use other collaboration tools as needed.</p>
<p>Exporting and migrating Markdown files from Readwise and converting voice notes isn't automatic and introduces friction in the CODE workflow. It's not ideal, but I rarely have an urgent need for specific highlights I haven't imported yet that disrupts my work.</p>
<h2>Valuable constraints</h2>
<p>These limitations are mildly inconvenient, but I'm continuously refining my setup and have full control over it. In fact, I find these limitations worthwhile and even beneficial, as they allow me to keep my setup simple, lightweight, and flexible. Everything is about tradeoffs, and this setup provides immense value to me without requiring much overhead or complexity.</p>
<p>I'm guessing my current setup offers 95% of what I could ever want in my second brain. In fact, the only improvements I have or currently plan to make involve simple adjustments to the folder structures and how I scope/limit my work in progress.</p>
<h2>Recap</h2>
<p>Here's a final overview of my custom second brain setup.</p>
<p>Workflow:</p>
<ul>
<li>Capture highlights and notes from source material (eBooks, articles, videos, podcasts)</li>
<li>Organize with PARA method using folders and files</li>
<li>Distill using Markdown syntax for progressive summarization</li>
<li>Express with new writing and notes for creating other content</li>
</ul>
<p>Tools:</p>
<ul>
<li>Kindle (eBook highlights and notes)</li>
<li>Readwise Reader (article highlights and notes)</li>
<li>Readwise (export highlights and notes to Markdown)</li>
<li>VSCode (creating and editing notes)</li>
<li>Git Journal (creating and editing notes on mobile)</li>
<li>Git (version control)</li>
<li>Version control and automatic file backup services (use multiple for redundancy, privacy, and security)</li>
</ul>
<p>Costs:</p>
<ul>
<li>Readwise and Readwise Reader: $99/year</li>
<li>Cloud storage for file backups: $49/year</li>
<li>Everything else is free and open-source</li>
<li>This setup saves me countless hours per year, while an hour of consulting covers the annual costs</li>
</ul>
<h2>Second brain resources</h2>
<p>There's a growing personal knowledge management community that's focused on building a second brain.</p>
<p>Here are some resources I've gotten a lot of value from:</p>
<ul>
<li><a href="https://fortelabs.com/blog/basboverview/">Building a Second Brain: An Overview</a> article by Tiago Forte (creator of the concept)</li>
<li><a href="https://www.buildingasecondbrain.com/">Building a Second Brain book</a> by Tiago Forte</li>
<li><a href="https://fortelabs.com/blog/para/">The PARA Method: The Simple System for Organizing Your Digital Life in Seconds</a> by Tiago Forte</li>
<li><a href="https://fractalproductivity.substack.com/">Fractal Productivity series</a> by Dennis Nehrenheim M.Sc. (interesting ideas that build on top of PARA)</li>
</ul>
Aquileo | My Custom Second Brain Setup Part 3: Minimalist Productivity2023-04-24T00:00:00Zhttps://luhr.co/blog/2023/04/24/my-custom-second-brain-setup-part-3-minimalist-productivity/<h1>My custom second brain setup, part 3: Minimalist productivity</h1>
<nav aria-labelledby="label-series-posts">
<p id="label-series-posts">Posts in this series:</p>
<ol>
<li>
<a href="https://luhr.co/blog/2023/04/19/my-custom-second-brain-setup-part-1-why-go-custom">Why go custom?</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/21/my-custom-second-brain-setup-part-2-how-it-works">How it works</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/24/my-custom-second-brain-setup-part-3-minimalist-productivity/" aria-current="page">Minimalist productivity (this post)</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/26/my-custom-second-brain-setup-part-4-limitations-as-strengths">Limitations as strengths</a>
</li>
</ol>
</nav>
<p>The real strength of a second brain comes down to a simple folder structure that has done more for my productivity than any app or user interface I've used.</p>
<p>The <a href="https://fortelabs.com/blog/para/">PARA method</a> structures all the information in a second brain in order of actionability.</p>
<p>As a review, my second brain is just a collection of Markdown files that use Git for version control. These files are structured with folders that follow the PARA (Projects, Areas, Resources, Archive) method:</p>
<ol>
<li><code>00-inbox</code>
<ul>
<li>Default for new notes to be processed later</li>
<li><code>scratchpad.md</code> file for random thoughts and reminders</li>
<li>Day-to-day tasks that don't relate to a specific project</li>
</ul>
</li>
<li><code>01-projects</code>
<ul>
<li>Active projects across work and personal life (try to limit to a max of 7)</li>
</ul>
</li>
<li><code>02-areas</code>
<ul>
<li>Ongoing areas of interest or processes with a standard to maintain</li>
</ul>
</li>
<li><code>03-resources</code>
<ul>
<li>Reference information that's not tied to an ongoing area of effort</li>
</ul>
</li>
<li><code>04-archive</code>
<ul>
<li>Information that's no longer relevant or actionable, but can always be promoted again if it's useful</li>
</ul>
</li>
</ol>
<p>My Projects folder represents any current work across my professional and personal life. I try to limit work in progress to an absolute max of 7 projects at once, with 3 or 4 as a more manageable amount.</p>
<p>Projects have all the relevant notes, resources, and tasks in once place to achieve a specific outcome by a particular date. For example, when writing the scripts for my course on LinkedIn Learning, Accessibility-First Design, I was able to reference all the years of my previous work and learnings without having to do much new research as I wrote. This greatly accelerated my writing process by focusing my time on simply curating my already-established thoughts and recommendations instead of starting with a blank script for each topic.</p>
<p>On a day-to-day basis, all other notes go into Areas if they represent topics of ongoing interest (writing, wellbeing, home, travel, etc.) or Resources if the are just useful reference (recipes, bookmarks, learning notes, instructions, etc.).</p>
<p>Anything that's no longer relevant gets demoted to the Archive folder. Life and work seem to have some seasonality to them. Occasionally, archived notes become relevant again and I can easily drag some notes back into a more actionable folder.</p>
<h3>Tasks</h3>
<p>Tasks primarily come out of project scopes and are the most granular and actionable information in a second brain. I either add tasks inline inside of notes where relevant, or can create a dedicated <code>tasks.md</code> file in a project folder with headings for each project scope.</p>
<p>There are also tons of loose tasks that come up in day-to-day life that aren't tied to a specific project (scheduling an appointment, quick things I need to do around the house, etc.). I simply add these to a <code>tasks.md</code> file in my <code>00-inbox</code> folder.</p>
<p>I don't use a separate task manager, and instead leverage Markdown and VSCode to track incomplete tasks from across my entire second brain.</p>
<p>In any file, I can add a simple Markdown task:</p>
<pre class="language-md"><code class="language-md"><span class="token list punctuation">-</span> [ ] Task name
<span class="token list punctuation">-</span> [x] Completed task name</code></pre>
<p>If the task has a deadline, as most tasks should, I created this convention:</p>
<pre class="language-md"><code class="language-md"><span class="token list punctuation">-</span> [ ] YYYY-MM-DD | Task name</code></pre>
<p>Sometimes a task is a nice-to-have, so I mark it as optional with a tilde (<code>~</code>):</p>
<pre class="language-md"><code class="language-md">~ [ ]</code></pre>
<p>Because tasks can appear anywhere, I do a global search in VSCode for the following string: <code>- [ ]</code>. This finds all incomplete tasks across all files. It's intentional that optional tasks with <code>~ [ ]</code> don't appear in this search. I only want to see those when I actively reviewing other information in a project, but the whole point is I'm not committed to them and comfortable never completing them.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/E3kHk1Wc1--320.avif 320w, https://luhr.co/assets/images/generated/E3kHk1Wc1--640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/E3kHk1Wc1--320.webp 320w, https://luhr.co/assets/images/generated/E3kHk1Wc1--640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/E3kHk1Wc1--320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/E3kHk1Wc1--320.jpeg 320w, https://luhr.co/assets/images/generated/E3kHk1Wc1--640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Use <code>Ctrl/Cmd + Shift + F</code> to do a global search in VSCode with the string <code>- [ ]</code></figcaption>
</figure>
<p>I take this one step further and use the "Open in editor" button to create a dedicated tab, which I pin so I can reference it any point.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/_MQUV-7hh--320.avif 320w, https://luhr.co/assets/images/generated/_MQUV-7hh--640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/_MQUV-7hh--320.webp 320w, https://luhr.co/assets/images/generated/_MQUV-7hh--640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/_MQUV-7hh--320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/_MQUV-7hh--320.jpeg 320w, https://luhr.co/assets/images/generated/_MQUV-7hh--640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>Use the "Open in editor" button to create a dedicated search tab</figcaption>
</figure>
<p>This pinned tab shows tasks across all files that represent the work I need to do in every area of my life. From this pinned tab, I can quickly jump into to read the surrounding context or update tasks as needed.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/RpZeYOYxcH-320.avif 320w, https://luhr.co/assets/images/generated/RpZeYOYxcH-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/RpZeYOYxcH-320.webp 320w, https://luhr.co/assets/images/generated/RpZeYOYxcH-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/RpZeYOYxcH-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/RpZeYOYxcH-320.jpeg 320w, https://luhr.co/assets/images/generated/RpZeYOYxcH-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>The pinned search results tab shows tasks across all files</figcaption>
</figure>
<p>If you want to only view tasks at a certain hierarchy, or for a specific project, it's easy to limit the search to a particular folder to create a more focused task list.</p>
<h2>Routine review</h2>
<p>It's useful to routinely review the <code>00-inbox</code> folder to process and organize lose notes into their respective areas, clean up the <code>scratchpad.md</code> file, and get an overview of my work in progress. Review may also involve manually exporting Kindle and Readwise Reader highlights and notes into Markdown files, which I then organize into my second brain as needed. Sometimes, I'll also refine the organization of some folders, and move items into the Archive as needed, but usually this happens in my day-to-day work with my second brain.</p>
<p>While some people have a formal cadence of weekly or monthly reviews, I embrace a just-in-time approach and simply do it whenever it comes to mind or bugs me. This works and hasn't resulted in too much batch processing.</p>
<h2>Calm</h2>
<p>This very straightforward setup requires very little thinking to use and creates very little friction when creating new notes or getting ideas out of my head.</p>
<p>I've found I hold on to fewer recurring worries or reminders in my working memory, and can trust that any task, obligation, or interesting idea is accounted for in my second brain.</p>
<p>I've never been one to have more than a few browser tabs open at once and my capture and organize workflow provides an immediate destination for any interesting articles I want to read later. I find myself needing very few bookmarks since they no longer act as a reading list, and I've actually migrated any remaining browser bookmarks into simple notes in my Resources folder by topic.</p>
<p>This second brain setup also means I also have very few tools and logins to manage, which adds to the calm and reduces context switching. I don't have to maintain my productivity system across multiple sites or apps, so my folder and file structure account for everything holistically.</p>
<p>This simplicity and calm make me feel less overwhelmed, more effective, and more creative than ever.</p>
<h2>Keeping it minimal</h2>
<p>I feel most productive with simple processes and the lightest tools possible.</p>
<p>While in the past I've used task management systems like Asana and created robust dashboards in tools like Notion, the flashy appearance of functionality came with increased overhead that did little to change my actual output. Having all my tasks embedded in the context of all my other knowledge and work in progress is both convenient and easy to survey at a high level.</p>
<p>It feels ridiculous that I put so much thought into some files and folders, but this absurdly simple setup has done more for me than any other tool I've tried.</p>
<p>In the final post in this series, I'll cover the limitations of my custom second brain that are actually some of its main strengths.</p>
<p>Next: <a href="https://luhr.co/blog/2023/04/26/my-custom-second-brain-setup-part-4-limitations-as-strengths">part 4: Limitations as strengths</a>.</p>
Aquileo | My custom second brain setup, part 2: How it works2023-04-21T00:00:00Zhttps://luhr.co/blog/2023/04/21/my-custom-second-brain-setup-part-2-how-it-works/<h1>My custom second brain setup, part 2: How it works</h1>
<nav aria-labelledby="label-series-posts">
<p id="label-series-posts">Posts in this series:</p>
<ol>
<li>
<a href="https://luhr.co/blog/2023/04/19/my-custom-second-brain-setup-part-1-why-go-custom">Why go custom?</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/21/my-custom-second-brain-setup-part-2-how-it-works/" aria-current="page">How it works (this post)</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/24/my-custom-second-brain-setup-part-3-minimalist-productivity">Minimalist productivity</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/26/my-custom-second-brain-setup-part-4-limitations-as-strengths">Limitations as strengths</a>
</li>
</ol>
</nav>
<p>To explain how my custom second brain setup works, I'm going to break down the tools I use for each of the steps in the CODE (Capture, Organize, Distill, Express) workflow.</p>
<p>Brace yourself: the core of my setup is so simple and minimal that you might be disappointed. This simplicity is absolutely intentional and the result of very careful consideration and continuous improvement.</p>
<h2>Tools for Capture</h2>
<h3>Articles</h3>
<p>I use <a href="https://readwise.io/read">Readwise Reader</a> to save articles, subscribe to RSS feeds, and add highlights and notes. <a href="https://readwise.io/">Readwise</a> (the main app) provides a convenient Markdown export that provides all the highlights and notes since the last export.</p>
<h3>Books</h3>
<p>I primarily read eBooks using a Kindle. Even though I enjoy reading paper books more, eBooks are so much more efficient for highlighting and capturing notes. I also use Readwise to export my Kindle highlights into Markdown.</p>
<p>When I do want to make notes or highlights in paper books, I'll read at my computer and transcribe any interesting text as I'm reading. This isn't the most ergonomic workflow, as books rarely stay open when I set them down to type, and transcription is tedious and slower than eBook highlights, so I don't really like this workflow. However, it's necessary for books that are only available in print. I may build a compact bookstand that can accommodate books of any size and holds the pages open. Handheld scanners with a pen form factor are a thing, but I don't know how well these work. That could be a viable solution, though.</p>
<h3>Videos, podcasts, audiobooks, and other sources</h3>
<p>For video and audio content, there's no efficient way (that I'm aware of) to capture snippets of audio as a "highlight" or auto-transcribe a selected portion. It'd be interesting if there are solutions to this, but I don't mind keeping things more manual and low-tech. This little bit of friction is actually a positive, as only the best information feels worth the effort to capture.</p>
<p>I usually watch educational videos on my computer, so I just add notes directly in my second brain as needed.</p>
<p>I'm usually not at my computer when listening to podcasts, so I just pause and make a note to re-listen to the podcast when I'm at my computer. Or better yet, find a transcript that I can save and highlight as if it's an article (going through Readwise Reader and then Readwise for exporting to Markdown). If you have a podcast, please provide transcripts! They're essential for accessibility and benefit anyone who wants to make notes or highlights.</p>
<p>Audiobooks have similar limitations, but I'll usually just get the eBook version if I like the audiobook, and re-read it while making highlights and notes (going through my Kindle and then Readwise for exporting to Markdown).</p>
<h3>Voice notes</h3>
<p>I use the built-in <a href="https://recorder.google.com/about">Recorder app on Android</a> to capture voice notes that auto-generate fairly accurate transcriptions. While this is handy for dictating ideas while I'm driving, for example, it requires some manual migration, and the transcripts aren't as succinct as my written notes or highlights, so they usually require more reworking and trimming to be useful.</p>
<h2>Tools for Organize</h2>
<h3>My second brain: just files and folders</h3>
<p>My second brain is just a collection of Markdown files that use Git for version control. These files are structured with folders that follow the <a href="https://fortelabs.com/blog/para/">PARA (Projects, Areas, Resources, Archive) method</a>:</p>
<ol>
<li><code>00-inbox</code>
<ul>
<li>Default for new notes to be processed later</li>
<li><code>tasks.md</code> for loose, day-to-day tasks not tied to any project</li>
<li><code>scratchpad.md</code> for capturing random thoughts that can be organized and refined later</li>
<li><code>shopping-list.md</code> for groceries or anything I need to buy in town</li>
</ul>
</li>
<li><code>01-projects</code>
<ul>
<li>Active projects across work and personal life (try to limit to a max of 7)</li>
</ul>
</li>
<li><code>02-areas</code>
<ul>
<li>Ongoing areas of interest or processes with a standard to maintain</li>
</ul>
</li>
<li><code>03-resources</code>
<ul>
<li>Reference information that's not tied to an ongoing area of effort</li>
</ul>
</li>
<li><code>04-archive</code>
<ul>
<li>Information that's no longer relevant or actionable, but can always be promoted again if it's useful</li>
</ul>
</li>
</ol>
<h3>Version control and syncing</h3>
<p>With Git for version control, I can easily back up and sync my second brain across many devices. I have full flexibility of using any combination of local storage, automatic file backups, GitHub, GitLab, etc. for safe keeping and redundancy.</p>
<p>I also find the security of version control services like GitHub and GitLab to be much more robust and dependable than self-rolled solutions from productivity apps.</p>
<h3>Editing notes on the computer</h3>
<p>Because my second brain is just a simple version-controlled folder structure, I can use the same tools I use for coding, which really speeds up my productivity.</p>
<p>For me, that's VSCode with a VIM plugin for efficient editing and searching.</p>
<p>Whenever I'm using my computer, I always have an instance of VSCode with my second brain open and at the ready. No logins needed.</p>
<p>At the Markdown file level, I use standard Markdown syntax, with the occasional table. The only custom thing I do is simple frontmatter in each file with created and modified dates:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">---</span>
<span class="token key atrule">created</span><span class="token punctuation">:</span> YYYY<span class="token punctuation">-</span>MM<span class="token punctuation">-</span>DD
<span class="token key atrule">modified</span><span class="token punctuation">:</span> YYYY<span class="token punctuation">-</span>MM<span class="token punctuation">-</span>DD
<span class="token punctuation">---</span></code></pre>
<p>This is easy to add with a Markdown snippet I created in VSCode that I invoke with <code>/top</code> and autocomplete:</p>
<p>In <code>markdown.code-snippets</code>:</p>
<pre class="language-json"><code class="language-json"><span class="token property">"Markdown frontmatter"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"prefix"</span><span class="token operator">:</span> <span class="token string">"/top"</span><span class="token punctuation">,</span>
<span class="token property">"body"</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">"created: $1"</span><span class="token punctuation">,</span>
<span class="token string">"modified: $1"</span><span class="token punctuation">,</span>
<span class="token string">"---"</span><span class="token punctuation">,</span>
<span class="token string">""</span><span class="token punctuation">,</span>
<span class="token string">"# $0"</span>
<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"Default frontmatter"</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre>
<h3>Editing notes on mobile</h3>
<p>I edit notes on mobile using the <a href="https://gitjournal.io/">Git Journal app</a>.</p>
<p>I configured Git Journal to name all new files to <code>YYYY-MM-DD-HH-SS.md</code> by default. Traversing folders in Git Journal isn't the easiest, so by default it stores all new notes in the <code>00-inbox</code> folder, which I can organize later when I'm working on a computer.</p>
<p>Git Journal automatically adds and updates the <code>created</code> and <code>modified</code> timestamps in frontmatter for each file, which matches my manual process.</p>
<p>Git Journal works well overall. Managing folders and files is a little clunky, so I organize them later when I'm at my computer. It does have a custom toolbar with some WYSIWYG options, but I'd really like to have dedicated indent/outdent buttons.</p>
<h2>Tools for Distill</h2>
<p>Progressive summarization is an efficient way to distill highlights and notes down to the most interesting and impactful information. It's an iterative process that involves 3 passes of using text formatting to highlight certain text with increasing levels of emphasis.</p>
<p>In an app like Evernote or Notion, this may involve the following passes:</p>
<ol>
<li>First pass: bold the most important text</li>
<li>Second pass: underline the most important bolded text</li>
<li>Third pass: highlight (add a background color to) the most important underlined and bolded text</li>
</ol>
<p>The result of this third pass is often less than 10% of the original highlights and notes. Since highlights are often 10% or less of the original source information, the output of progressive summarization is the most essential 1% of ideas that offer the majority of value.</p>
<p>Since all my notes are authored in Markdown, I can use its simple syntax for progressive summarization:</p>
<ol>
<li>First pass: <em>italicize</em> with <code>*</code> (<code>*idea to highlight*</code>)</li>
<li>Second pass: <strong>bold</strong> with <code>**</code> (<code>**idea to highlight**</code>)</li>
<li>Third pass: <strong><em>bold and italicize</em></strong> with <code>***</code> (<code>***idea to highlight***</code>)</li>
</ol>
<p>This syntax is easy to type, and just so happens to visually indicate the level of importance with 1, 2, or 3 asterisks (<code>*</code>), even when viewing Markdown as plain text.</p>
<h2>Tools for Express</h2>
<p>The main channels I use to express new ideas and content are my personal blog and my YouTube channel, with some very light posting to social media.</p>
<p>My personal site uses 11ty with Markdown for posts, so I draft all posts in my second brain and copy them into my site's codebase when I'm ready to publish. All I need to do for publishing is add some frontmatter for meta information (title and description), sprinkle in some WebC components if needed, and a published date. Then, I simply commit it up to trigger a new build. It's critical that this process requires only a couple minutes of work to go from finished draft to published post.</p>
<p>For other channels, my second brain has substantially accelerated my learning and creative output with so much valuable information and thoughts to build upon. I can quickly search my notes for ideas across a topic, rework them as an outline for a video or as a quick social post, and then create the final content.</p>
<p>Alongside authoring written content in Markdown, I've experimented with dictating blog post drafts using Recorder for Android or Otter.ai, but this has been less efficient for me. Exporting text from these tools and manually formatting into more structured Markdown content is tedious. And, although I largely have aligned my speaking voice and writing voice over the last few years, my off-the-cuff dictating isn't as well structured and sequenced as my writing, so it requires a lot more editing to get from initial draft to finished post.</p>
<p>I can probably improve on this with more detailed outlines before dictating content, but I'm more likely just to just write directly. I might refocus my dictating experiments into recording podcast episodes or YouTube videos in the future instead.</p>
<h2>All together</h2>
<p>Here are all the tools that compose my second brain setup:</p>
<ul>
<li>Kindle (eBook highlights and notes)</li>
<li><a href="https://readwise.io/read">Readwise Reader</a> (article highlights and notes)</li>
<li><a href="https://readwise.io/">Readwise</a> (export highlights and notes to Markdown)</li>
<li>VSCode (creating and editing notes)</li>
<li><a href="https://recorder.google.com/about">Recorder</a> (Android app for voice recording)</li>
<li><a href="https://gitjournal.io/">Git Journal</a> (creating and editing notes on mobile)</li>
<li>Git (version control)</li>
<li>Git repository hosting and automatic file backup services (use multiple for redundancy, privacy, and security)</li>
</ul>
<p>At its core, my second brain really is just a collection of Markdown files in folders, with my text editor (VSCode) as the primary tool. A key attribute of this setup is I can use any text editor to edit and maintain my second brain, and I can store it anywhere. I can structure everything to suit my needs, and any conventions are purely of my own choosing. For me, this simplicity offers pretty much everything I could want.</p>
<p>In the remaining posts in this series, I'll cover my minimal productivity process using my custom second brain setup, and then wrap things up by covering the limitations of my setup that are actually some of its main strengths.</p>
<p>Next: <a href="https://luhr.co/blog/2023/04/24/my-custom-second-brain-setup-part-3-minimalist-productivity">part 3: Minimalist productivity</a>.</p>
Aquileo | My custom second brain setup, part 1: Why go custom?2023-04-19T00:00:00Zhttps://luhr.co/blog/2023/04/19/my-custom-second-brain-setup-part-1-why-go-custom/<h1>My custom second brain setup, part 1: Why go custom?</h1>
<nav aria-labelledby="label-series-posts">
<p id="label-series-posts">Posts in this series:</p>
<ol>
<li>
<a href="https://luhr.co/blog/2023/04/19/my-custom-second-brain-setup-part-1-why-go-custom/" aria-current="page">Why go custom? (this post)</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/21/my-custom-second-brain-setup-part-2-how-it-works">How it works</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/24/my-custom-second-brain-setup-part-3-minimalist-productivity">Minimalist productivity</a>
</li>
<li>
<a href="https://luhr.co/blog/2023/04/26/my-custom-second-brain-setup-part-4-limitations-as-strengths">Limitations as strengths</a>
</li>
</ol>
</nav>
<p>Building a second brain last year had the biggest impact on my productivity, learning, and organization in my entire career. It led to many of the successes I covered in my <a href="https://luhr.co/blog/2023/02/25/2022-year-in-review/">2022 year in review</a> and provided incredible value in my work, self development, and creative output.</p>
<h2>What is a second brain?</h2>
<p>A <a href="https://fortelabs.com/blog/basboverview/">second brain</a> is a productivity system that gives focus, structure, and actionability to all the information we encounter in our lives.</p>
<p>It acts as a central store for all areas of your professional and personal life. By getting all information out of your head, you can keep your "working memory" clean, which is calming and leads to clearer thinking.</p>
<p>A second brain follows the CODE (Capture, Organize, Distill, Express) workflow:</p>
<ol>
<li>Capture
<ul>
<li>Highlight or take notes on the most relevant, interesting, and impactful information from source materials (articles, books, videos, podcasts, etc.)</li>
<li>Write down any ideas or tasks as you think of them to get them out of your head</li>
</ul>
</li>
<li>Organize
<ul>
<li>Structure this information based on actionability</li>
</ul>
</li>
<li>Distill
<ul>
<li>Use progressive summarization to refine highlights and notes down to the essential details and most novel ideas</li>
</ul>
</li>
<li>Express
<ul>
<li>Recombine existing ideas and generate new ones to create meaningful work</li>
</ul>
</li>
</ol>
<p>My second brain setup needs to account for this full process with as little complexity and overhead as possible.</p>
<p>Most people build their second brain using a productivity/knowledge management app like Evernote, Notion, Obsidian, or Roam, sometimes with multiple layers of other services and integrations that support a complex CODE workflow.</p>
<p>With so many great options, some of which are completely free, why build a custom setup?</p>
<h2>Past pains and lock-in</h2>
<p>I've been burnt by lost data. A few years ago, a collection I maintained with over 15 years of my past writing vanished suddenly from my Google Drive. This happened without a trace, not even appearing in my file history or trash, and could not be recovered. That was a deeply painful experience that discouraged me from writing much at all for nearly 2 years.</p>
<p>I've also lost a lot of time migrating between tools repeatedly due to pricing and plans changing suddenly, companies going out of business or being acquired, and other stressful experiences.</p>
<p>I first drafted my second brain in 2021 using Notion and quickly felt locked in. I was also frustrated by how long the app took to load, poor offline support, and block limits as the pricing structure changed. When I migrated off Notion to my custom setup, the process was tedious. Even going from Notion's flavor of Markdown to standard Markdown required a fair bit of manual conversion and per-file review.</p>
<p>These apps work for many people and not everyone needs to build a custom second brain. Lock-in happens to varying degrees with any paid service. For some people, those tradeoffs are totally worth it. Sometimes a level of lock-in provides even provides convenience within an ecosystem, but it's not a good fit for me.</p>
<h2>Requirements</h2>
<p>My second brain needed to have the following qualities:</p>
<ul>
<li>Quick and easy to access</li>
<li>Simple</li>
<li>Efficient</li>
<li>Lightweight</li>
<li>Secure</li>
<li>Easy to maintain</li>
<li>Independent of specific companies or services</li>
<li>Flexible</li>
<li>Fully works offline</li>
<li>Convenient editing and syncing across my devices</li>
</ul>
<h2>Existing options</h2>
<p>I'm not interested in Evernote, and moved away from Notion.</p>
<p><a href="https://obsidian.md/">Obsidian</a> and <a href="https://roamresearch.com/">Roam</a> are other interesting options that have free plans. They offer intricate relational graphs, backlinks, and other power features are probably great for abstract thinkers, but the interfaces and interactions are definitely more complex as well. They both have mobile apps, which is a plus, but I'm hesitant about long-term lock-in.</p>
<p>If I had to pick an existing tool, I'd probably go with <a href="https://www.dendron.so/">Dendron</a>. It's an impressive open-source project that adds knowledge management features directly into VSCode. Some features even match Obsidian and Roam, such as the relational graphs that connect topics across notes. These features are more complex than what I need, but seem pretty optional. The main downside of Dendron is the lock-in with VSCode and lack of mobile editing. I use VSCode, but have considered using VIM directly in the terminal. So VSCode may not always be my editor, and I need a way to edit my second brain from my phone.</p>
<p>After evaluating all the existing options, I was craving more flexibility, efficiency, and simplicity than what was available.</p>
<h2>Time to go custom</h2>
<p>Often people build custom tools to have features that don't exist, or to support nuanced, intricate workflows. In my case, I actually wanted the opposite: strip out as much complexity, overhead, and outsourcing as possible so I can enjoy a simple, focused, portable second brain setup.</p>
<p>In the next post in this series, I'll detail the tools I use for each of the Capture, Organize, Distill, and Express steps of the CODE workflow. In following posts, I'll then cover my minimal productivity process using my custom second brain setup, and wrap things up by covering the limitations of my setup that are actually some of its main strengths.</p>
<p>Next: <a href="https://luhr.co/blog/2023/04/21/my-custom-second-brain-setup-part-2-how-it-works">part 2: How it works</a>.</p>
Aquileo | My new course: Accessibility-First Design2023-04-17T00:00:00Zhttps://luhr.co/blog/2023/04/17/my-new-course-accessibility-first-design/<h1>My new course: Accessibility-First Design</h1>
<p>My new course, <a href="https://www.linkedin.com/learning/accessibility-first-design">Accessibility-First Design</a>, launched recently on LinkedIn Learning!</p>
<p>Accessibility doesn't compromise design. Instead, it's at the core of successful content and visual design.</p>
<p>This concise course provides tons of tactical skills across content design, visual design, and accessibility testing to make more successful digital products. While it's focused on design, I consider all product work to be a form of design, and every topic has bridging context to development.</p>
<p>I love when my educational content opens up wider conversation. If you take the course, let me know if you have any questions or feedback!</p>
<h2>Core concept: accessibility-first</h2>
<p>As the title reflects, this course focuses on an accessibility-first approach to product design.</p>
<p>Accessibility-first is a term I introduced in my work over the past year that's analogous to mobile-first design.</p>
<p>With mobile-first design, we design for smaller viewports to ensure that the user experience, content, and visual design are all thoughtfully crafted without compromise when there's less space to work with. This makes for a more focused, resilient, and cohesive experience for all users across all screen sizes.</p>
<p>With accessibility-first design, we design with accessibility in mind from the start as a core principle of our work. This spans content design, visual design, and development, with accessibility testing throughout the product development process to ensure we keep our work in an always-working, accessible state. Prioritizing accessibility improves the user experience for everyone.</p>
<h2>Core concept: just-in-time Design</h2>
<p>Just-in-time design is another approach and term I've introduced in my work over the past year. Even the most "Agile" teams often perform upfront design before beginning development, which leads to a lot of rework and waste in the process. Questions, constraints, and even opportunities arise later in the process, which requires repeated design changes or compromises.</p>
<p>With just-in-time design, teams do visual design in parallel with development. Specifically, by waiting for initial content and functionality to be implemented in the browser, teams can start with a working, accessible, unstyled version of their product. This verifies that the overall functionality, content structure, and accessibility are in place and will be protected as the product evolves.</p>
<p>At this point, just enough visual design is layered in to bring polish, branding, and delight to the user experience, but it's built on a working, de-risked foundation.</p>
<p>This is a radical approach, but I've implemented it in my consulting work with my clients' teams with a lot of success. The end result is a more better product across accessibility, user experience, performance, content design, and visual design, while also saving significant time and energy.</p>
<h2>Behind the scenes</h2>
<p>It's surprising how much work and distillation goes into creating just a 1 hour 24 minute course.</p>
<p>LinkedIn Learning first reached out in August of last year to ask if I would like to pitch a course. The course pitch process involved writing a course summary and an outline with learning objectives for each topic. It was interesting to outline the course from a user benefit perspective, and forced me to think about actionability and outcomes for every topic.</p>
<p>My course was greenlit in late September, so I kicked off production in October with a flurry of script writing. With the volume of production work ahead of me, I really depended on time blocking my calendar to guarantee I had consistent hours every week to stay on schedule.</p>
<p>Typically, I just have a simple bulleted list when I record educational content. Writing polished scripts that I would follow word-for-word was a new experience, and forced me to really unify my writing and speaking voices.</p>
<p>Although I do a lot of writing in my career, this also pushed me to produce writing consistently, on-demand, at scale. By mid-November, I finalized scripts for 26 videos with 13,500 words.</p>
<p>From there, the sole focus was creating visual assets. Across each of the 26 videos, I created an ID for every distinct idea that would benefit from a dedicated visual. This created an inventory of over 350 slides I needed to design.</p>
<p>First, I conducted an audit of all the unique user interface situations I cover in the course. This created a checklist for me to build a realistic design system in Figma that had a generic fictional brand, cohesive look, and all the statefulness of an interactive and accessible product. A number of these components also needed both good and bad versions to cover common mistakes.</p>
<p><picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/VVR2CeoTi--320.avif 320w, https://luhr.co/assets/images/generated/VVR2CeoTi--640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/VVR2CeoTi--320.webp 320w, https://luhr.co/assets/images/generated/VVR2CeoTi--640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/VVR2CeoTi--320.jpeg" alt="Inventory of dozens of components and slide assets in Figma." loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/VVR2CeoTi--320.jpeg 320w, https://luhr.co/assets/images/generated/VVR2CeoTi--640.jpeg 640w" sizes="(min-width: 100px)" /></picture></p>
<p>This work was a form of just-in-time design. Because the visual design was driven by the script writing, I was able to design exactly what was needed and create a ton of efficiencies. I was surprised that just 10 distinct user interface patterns had enough accessibility considerations, states, and common mistakes to support hundreds of slides.</p>
<p>With my component warehouse in place, I worked fairly quickly and linearly through the slides. Most of the slides I designed could be adapted for related topics in other videos, so progress accelerated with each chapter I completed.</p>
<p>Working around the holidays and surges in client work, I finalized the slides in mid-January just before traveling to LinkedIn Learning to record onsite.</p>
<p>As someone who records a lot of content at home, the scale and resources of the recording process at LinkedIn Learning really impressed me. Their studio had dozens of soundproof booths with dedicated halves and equipment for producers and instructors to collaborate.</p>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/FBYh3qofxU-320.avif 320w, https://luhr.co/assets/images/generated/FBYh3qofxU-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/FBYh3qofxU-320.webp 320w, https://luhr.co/assets/images/generated/FBYh3qofxU-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/FBYh3qofxU-320.jpeg" alt="Interior of the recording booth with black felt walls, standing desk, camera with teleprompter, lights, and white decorative wave pattern wall." loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/FBYh3qofxU-320.jpeg 320w, https://luhr.co/assets/images/generated/FBYh3qofxU-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>My soundbooth and entire universe for a couple days.</figcaption>
</figure>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/Dx8QGOgw9_-320.avif 320w, https://luhr.co/assets/images/generated/Dx8QGOgw9_-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/Dx8QGOgw9_-320.webp 320w, https://luhr.co/assets/images/generated/Dx8QGOgw9_-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/Dx8QGOgw9_-320.jpeg" alt="" loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/Dx8QGOgw9_-320.jpeg 320w, https://luhr.co/assets/images/generated/Dx8QGOgw9_-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>The studio has dozens of soundbooths for recording many courses in parallel.</figcaption>
</figure>
<figure>
<picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/_f9W517XXs-320.avif 320w, https://luhr.co/assets/images/generated/_f9W517XXs-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/_f9W517XXs-320.webp 320w, https://luhr.co/assets/images/generated/_f9W517XXs-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/_f9W517XXs-320.jpeg" alt="Teleprompter in front of camera lens with 'hi Dave' displayed." loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/_f9W517XXs-320.jpeg 320w, https://luhr.co/assets/images/generated/_f9W517XXs-640.jpeg 640w" sizes="(min-width: 100px)" /></picture>
<figcaption>An accidental homage to HAL-9000.</figcaption>
</figure>
<p>The campus also had dedicated sets and studios for on-camera content and photoshoots, and the scale of the whole operation made it clear how LinkedIn Learning can put out multiple new courses weekly.</p>
<p><picture><source type="image/avif" srcset="https://luhr.co/assets/images/generated/j784-8EmdV-320.avif 320w, https://luhr.co/assets/images/generated/j784-8EmdV-640.avif 640w" sizes="(min-width: 100px)" /><source type="image/webp" srcset="https://luhr.co/assets/images/generated/j784-8EmdV-320.webp 320w, https://luhr.co/assets/images/generated/j784-8EmdV-640.webp 640w" sizes="(min-width: 100px)" /><img src="https://luhr.co/assets/images/generated/j784-8EmdV-320.jpeg" alt="Seating lounge with dimensional felt shapes as colorful wall art and LinkedIn Learning logo." loading="lazy" decoding="async" width="640" height="480" srcset="https://luhr.co/assets/images/generated/j784-8EmdV-320.jpeg 320w, https://luhr.co/assets/images/generated/j784-8EmdV-640.jpeg 640w" sizes="(min-width: 100px)" /></picture></p>
<p>After a couple days recording onsite, the course went into post-production and launched in late March!</p>
<h2>Reflecting on my experience</h2>
<p>This experience made me realize how courses are a strong form of progressive summarization. This course summarizes years of my career and learning, hundreds of hours of production work with thousands of words of writing and hundreds of dedicated visual assets, and distills it all down into less than 90 minutes.</p>
<p>I typically create longer, deep-dive content that could dig into a single technique for 90 minutes, so this format of intensely focused, shorter videos was a good stretch exercise for me.</p>
<p>The production process also created a productive habit in my writing. Since working on the course, I've carried this momentum forward into this year. Writing comes much more easily now, even at scheduled times, and I've found myself wanting to get back into creating video content again after a couple years of being focused on client work.</p>
<p>Feel free to check out <a href="https://www.linkedin.com/learning/accessibility-first-design">Accessibility-First Design on LinkedIn Learning</a>. I hope it provides value for anyone who works on digital products. Let me know your thoughts!</p>
Aquileo | Betting on WebC2023-02-27T00:00:00Zhttps://luhr.co/blog/2023/02/27/betting-on-webc/<h1>Betting on WebC</h1>
<p><a href="https://github.com/11ty/webc">WebC</a> is a new tool by <a href="https://www.11ty.dev/">Eleventy</a> that is one of the most exciting developments in a long time.</p>
<p><a href="https://github.com/11ty/webc">WebC as a standalone tool</a> generates markup for custom elements and Web Components, which unlocks server-rendering for Web Components and brings many quality-of-life features while following web standards.</p>
<p>As someone who most enjoys working in good old HTML, CSS, and vanilla JS, this is a dream. I've used plenty of frontend frameworks throughout my career, but I've always been frustrated by the complexity, bloat, and damage to the user experience they cause. WebC has the potential to become my go-to approach for building websites.</p>
<p><a href="https://www.11ty.dev/docs/languages/webc">WebC as a plugin for Eleventy</a> allows for single-file components and tons of new templating and rendering possibilities. It can be used for layouts, pages, and components, meaning Eleventy sites can now be built entirely with WebC without other templating languages such as Nunjucks or Liquid.</p>
<p>In fact, I recently rebuilt this website using WebC instead of Nunjucks, and the authoring experience was so empowering, simple, and fun. I'm using WebC for layouts (including nested layouts), pages, and components that have fully replaced Nunjucks partials/includes.</p>
<h2>Enhancing Web Components</h2>
<p>Web Components have a strong future, but the main arguments against them have been server-side rendering and the ergonomics. WebC solves this, allowing for progressive enhancement, while also providing rendering flexibility, simple CSS scoping, critical CSS and JS bundling, and more.</p>
<p>Probably my favorite benefit of WebC with Eleventy is the default behavior of simply rendering an HTML-only partial. You simply write markup in a <code>.webc</code> file, and the custom element tag will be replaced with the markup when rendered. This is the main use case for HTML templating, and keeps the DOM clean and simple. I'll dig into and compare all the rendering options WebC offers in a future post, as there are so many useful options available.</p>
<p>As with all projects by Eleventy, performance and web standards are top of mind with WebC. An Eleventy site with WebC won't ship any client-side JavaScript, unless you intentionally author it in a component.
Even if you add scoped CSS to a component, WebC will render it with a custom element tag, but there's no JavaScript needed. This is a major win for performance, web standards, and simplicity.</p>
<h2>Future potential</h2>
<p>WebC unlocks a lot of possibilities for static sites using Eleventy and I'm interested in what's now possible for even more interaction-heavy experiences. I have an ambitious personal project in the works, so I'm going to take things as far as I can with WebC and Eleventy, sharing my learnings and ideas along the way.</p>
<p>Because WebC addresses the limitations of Web Components, I'm optimistic that it can be used to build robust web apps and can gain strong adoption in the community.</p>
<p>I don't care about tech trends, but it'd be great to have a project that's committed web standards and progressive enhancement grow in use to improve the web for everyone.</p>
<h2>Learn more about WebC</h2>
<ul>
<li><a href="https://www.11ty.dev/docs/languages/webc/">WebC Eleventy documentation</a></li>
<li><a href="https://www.youtube.com/watch?v=p0wDUK0Z5Nw">Interactive Progressively-enhanced Web Components with WebC (Eleventy YouTube)</a></li>
<li><a href="https://www.youtube.com/watch?v=X-Bpjrkz-V8">Crash Course in Eleventy’s new WebC Plugin (Eleventy YouTube)</a></li>
<li><a href="https://11ty.webc.fun/">11ty & WebC</a>. This site of useful recipes taught me a lot about how WebC works and all the different rendering options it offers.</li>
</ul>
Aquileo | 2022 year in review2023-02-25T00:00:00Zhttps://luhr.co/blog/2023/02/25/2022-year-in-review/<h1>2022 year in review</h1>
<p>2022 was the best year of my career so far, and one of the best in my personal life.</p>
<h2>Career</h2>
<p>I moved away from full-time employment in the summer of 2020 to be fully independent, and I'm enjoying this phase of my career more than ever. 2022 was a year of focus, and I really overcame lingering burnout that had crept into my life from the previous couple years.</p>
<h3>Consulting</h3>
<p>Independent consulting work was very successful, rewarding, and dependable. My goal was to keep my time and attention focused, so I turned down several consulting and full-time opportunities.</p>
<p>I started 2022 fully booked for the year with my main client, who was incredible to work with throughout the entire year, and I'm rolling into 2023 with the whole year booked with them again.</p>
<p>While I originally began working with this client in November 2021 to improve their product's accessibility, our work together has since evolved to include pretty much all aspects of the product and business.</p>
<p>I was able to work with the team to drastically simplify and redesign the marketing site and product, introduce lean manufacturing principles to improve an intense data workflow at the core of the business, and transition the team from Scrum to <a href="https://basecamp.com/shapeup">Shape Up</a> with much success.</p>
<p>Working with this team has led to many learning opportunities for me. I learned how to code in Python, build a parser from scratch, do web scraping and data transformations at scale, and really embraced Test-Driven Development (TDD).</p>
<h3>Coaching</h3>
<p>In addition to consulting, I took on a new design coaching client who is a solo entrepreneur and a freelancer. While I've done a lot of mentorship in past roles, this was a great experience to focus more on formal coaching, provide advice, and provoke new solutions with less focus on teaching.</p>
<p>While I don't really have the capacity to scale coaching as an offering, I enjoyed the recurring sessions we had for a few months, and it was rewarding to see my client really level up in design skills, UX, and accessibility.</p>
<h3>Teaching</h3>
<p>I wanted to keep my total obligations to a minimum in 2022, so I turned down multiple speaking and course opportunities for most of the year.</p>
<p>In the fall of 2022, however, I was contacted by LinkedIn Learning to basically create any course I wanted. After careful consideration, I felt this opportunity was a good fit and came at the right time, so I kicked off production on a new course on accessible design that is slated for spring 2023.</p>
<p>Although I've been gradually working on a large-scale accessibility course for a couple years, it's always had to fall back in priority to paid consulting work. Having a short-term deadline with a more focused scope was the right level of commitment and led to a very productive few months.</p>
<p>I wrote over 13,000 words for the video scripts between October and November, and spent December and January creating designs for dozens of UI examples and over 300 slides. In late January 2023, I went to LinkedIn Learning's offices to record the course, and it's now in post-production.</p>
<h3>Learning</h3>
<p>As I mentioned with consulting work, I had an opportunity to pick up several new skills in 2022. I've grown to really enjoy working with Python as my second programming language after JavaScript, and it's been quicker to pick up additional languages, such as C# for game development (more on that in the future).</p>
<p>Since I started my career in visual design, my development experience has always been heavily focused on the frontend. In 2022, I got a lot of experience working with APIs and asynchronous code and getting more familiar with backend development which has helped round out my understanding and made me a better developer.</p>
<p>My favorite project of the year was building a custom tokenizer and recursive descent parser (RDP) in both JavaScript and Python to parse a custom syntax for my main client. I followed a TDD workflow to write maybe the most polished code of my career.</p>
<p>TDD has always been interesting to me, but it didn't quite click until this past year. While I'm still learning how to best test integrations (particularly with <a href="https://www.jamesshore.com/v2/projects/nullables">James Shore's nullables technique</a>), it's been confidence-inspiring to code in an always-working state, while improving my understanding of how things work.</p>
<p>Lastly, I finally made the switch to Vim for text editing. Like TDD, I've dabbled with Vim for a while, but didn't fully commit. In the middle of 2022, I tried it again and felt right at home. While I'm still not the most efficient at horizontal movements and more advanced text replacements, most tasks have become muscle memory and really improved the ergonomics of writing and coding.</p>
<h3>Side projects</h3>
<p>As part of my focus for the year, my side projects played less of a role in 2022.</p>
<p>After collaborating weekly with a past colleague on a major product idea for the past 2 years, we decided to wind the project down in mid-2022 to focus on individual interests.</p>
<p>I haven't created content for <a href="https://youtube.com/buildux">Build UX</a> in about a year and a half, as I was staving off long-term burnout. My burnout wasn't specific to content creation, but something had to give. Recently I've been feeling more inspired to create more videos and free educational content, so it's possible this will play more of a role again in 2023.</p>
<p>Although I didn't make formal progress on my larger accessibility course in 2022, creating the smaller course for LinkedIn Learning proved to myself how much I could cover on the topic in the future. This course has definitely been de-prioritized in favor of bringing in consulting income and I'm not sure when or if it'll be my main focus.</p>
<h2>Personal life</h2>
<p>My personal life in 2022 was defined by investing in long-term improvements and simplifying things as much as possible. Overcoming lingering burnout was a huge relief, and I feel like I'm more organized and productive than ever.</p>
<p>The biggest transformation to my organization, learning, and personal growth has been my <a href="https://fortelabs.com/blog/basboverview/">second brain</a>. Throughout 2022, I really increased my non-fiction book and article reading, with tens of thousands of highlights feeding into several hundred notes.</p>
<p>I created several hundred more notes from courses, educational videos, and both professional and creative writing. Building a second brain has really changed the way I work and learn by structuring all my thoughts and interests and giving me a central reference to any important information I need in my career and personal life.</p>
<p>Alongside this, I can't understate how helpful therapy has been for my personal growth and self awareness. I wish therapy was accessible for everyone as a standard wellness practice. I'm glad therapy is being de-stigmatized as something everyone can benefit from, even if you don't feel you're unwell or in crisis.</p>
<h3>Personal learning</h3>
<p>On a lighter note, I really progressed in my mountain biking skills and fitness in 2022. My technique, strength, and cardio were better than ever, and I felt more comfortable on black diamond and pro rating trails as I pushed myself to ride more challenging lines, bigger jumps, and longer sessions.</p>
<p>I also returned to learning Japanese after many years. I studied Japanese throughout college and studied at Doshisha University in Kyoto through the Department of State's Critical Languages Scholarship in 2012. I traveled to Japan in 2016 and 2017, and while a lot of things came back to me after a few days of immersion, my overall speaking skills have really taken a hit. But, I'm now on a 150+ day streak of studying Japanese again, and I feel a lot of my previous knowledge returning. I've been fortunate to have a friend to practice with each week, and it's good to be learning something not directly tied to my professional work.</p>
<h2>Going into 2023</h2>
<p>I don't place much emphasis on New Year's and I'm not a fan of making specific goals or resolutions. Instead, I try to continuously adjust my values and guiding principles to shift my habits and energy in the direction I want.</p>
<p>For 2023, I would like to continue keeping my professional work focused with just my main client, and saying "no" to other opportunities as the default. I'm fortunate to have 2023 already fully booked, and I'm feeling energized with my work more than ever.</p>
<p>I've been getting a lot of ideas for educational content lately, so that will likely play more of a role again this year.</p>
<p>I'm at an inflection point with two side projects that have some real long-term potential, and I know I will need to choose just one to dedicate my time and learning to. It's going to be a difficult call, but I've over-extended myself enough times to know that it's necessary.</p>
<p>In my personal life, I'm excited to keep investing in my personal growth and organization. 2022 very much felt like laying the foundations for future improvements.</p>
<p>I'll hopefully be traveling to Japan in September, which is really motivating me to take my Japanese learning further. I'm also hoping to do a mountain bike race or two, as I miss the personal challenge I used to get from motocross and desert racing when I was younger.</p>
<p>I feel incredibly fortunate to be where I'm at, and more motivated than ever to make the most of it.</p>
Aquileo | Freelancing, consulting, and coaching2022-06-03T00:00:00Zhttps://luhr.co/blog/2022/06/03/freelanching-consulting-and-coaching/<h1>Freelancing, consulting, and coaching</h1>
<p>I recently took on a new client that I've been coaching in design, and it got me thinking about how the terminology of freelancing, consulting, and coaching really does define the engagement and working relationship.</p>
<p>Freelance work is often relegated to production with a set scope and deadline. Whether it's true, being a freelancer seems to have the connotation of someone who is assigned well-defined projects that might have smaller scope or quick turnaround. A freelancer might bill hourly, by the word or deliverable, or have a retainer for consistent production targets. If there's an opportunity for value-based pricing, it's usually for projects that again are well-defined with a known deadline.</p>
<p>In my career, defining myself as a freelancer felt like I was limited by unfair client expectations that I could/should only work this way, which led to lots of price negotiating and clients looking for a discount. If I had wider recommendations around processes or an area I wasn't explicitly assigned to, they weren't really welcomed or treated seriously.</p>
<p>The working relationship as a consultant or coach feels very different. Both these roles have the connotation of outside expertise, a fresh perspective, of someone you bring in to help with hard problems, illuminate opportunities and inefficiencies, and give guidance on how to progress.</p>
<p>This leads to one of my favorite quotes:</p>
<blockquote>
<p>We give added trust to claims made by external sources.</p>
</blockquote>
<p>Actually, I just wrote that. But by framing it as a quote, it seems more objectively and universally true, which proves my point.</p>
<p>What sets apart consulting from coaching in my experience is the level of embeddedness. When I consult for my clients, I strive to basically be a member of the team, and often contribute directly in addition to evaluating, teaching, and advising. With coaching, I'm a bit more removed, asking questions and providing feedback with check-ins or while talking through challenges and recent work. Coaching sessions definitely can involve pairing on things, but it's usually a lighter touch or with the coach acting purely as the navigator.</p>
<p>Consulting and coaching also open the engagement up to high level observations and recommendations about <em>how</em> the work is being done, or if there are opportunities in areas beyond the original focus. I might begin working with a client primarily focused on accessibility, but this can naturally lead to further visual design and performance implications or have wider learnings with how the team is structured and collaborating.</p>
<p>These types of engagements are also better suited for a retainer model or value-based pricing (often at a higher amount), with more flexible scopes and an ongoing timeline.</p>
<p>The connotation of expertise with consulting and coaching is definitely a benefit, but it needs to be handled carefully. The main advantage is clients seek and value your insights and observations, which leads to a much more successful working relationship compared to an engagement where I'm supposed to hand over deliverables without question. This allows me to contribute much more business value, which deserves a higher rate.</p>
<p>There is a risk to this perception, however. Because clients assume consultants have outside expertise, that sometimes means they don't believe there is internal expertise. One of my objectives in working with clients is to ensure that everyone, both leaders and individual contributors, recognize, respect, and praise the expertise across all roles in the team. There may be cultural or process issues that are inhibiting this, but I can use the trust placed in me as a consultant to surface and resolve them.</p>
<p>All this said, it's strange how much these terms define the working relationship, limit or open opportunities, and even affect how much you can charge for the work. As much as I feel that titles and roles are largely superficial and unnecessary, they do really affect the perceived value of our work. Shifting my focus to consulting and coaching over time really did change the dynamics with better work, pay, and impact.</p>
<p>So, if you're a freelancer with clients who don't allow you to bring your full potential, maybe try calling yourself a consultant to switch it up. You work for yourself, so it's not like you need permission.</p>