annualbetaInfrequent musings on web development, books and life2021-12-17T00:00:00-00:00https://annualbeta.com/Søren Birkemeyerpolarbirke@gmx.deThe End is The Beginning is The End2007-09-27T00:00:00-00:00https://annualbeta.com/blog/the-end-is-the-beginning-is-the-end/<p><strong>Web design</strong> has come a long way. We strive to play the game in adherence to Standards, we separate structure from presentation as much as possible and we have taken into our hearts the fact that indeed the Experience is the Product. Or have we?</p>
<p>A core ambition of website designers is to keep visitors on a site, get them to consume the content and perhaps even interact with it. Why then are users still left to themselves after they have done what should bring joy to every site owner: after they have read all of the content? Too often they are fobbed off with just a copyright info, a legal notice or a claim to W3C validity at the end of the page. They are met with bland place holders where they should in fact be rewarded for their effort.</p>
<p>Let us imagine a user that has just devoured all of the beautifully laid out information on a page. He is a most wanted user: he made the effort to scroll down all the way. He might have gotten engaged in an interesting train of thought, or become convinced of a certain product’s advantages – in any case, he is entertained, even excited to a point. Now the user has arrived at the bottom of the page and is looking for more to read, to absorb, to engage with. Ideally, we would want him to stay and navigate to another part of the website. Ideally, he would find recommendations where to go next. Unfortunately for both of us, he is usually faced with nothing helpful and left to himself.</p>
<p>Footers are in serious need of some love. We have to pay more attention to what is going on (literally) “down under”. Just like Europeans acknowledge the existence of Australia but rarely know what is going on there at any given time, footers still are a bit of a hazy area in web design.</p>
<h2>Carpe Pedem</h2>
<p>What are possible solutions then? To be honest, we (as designers) are only bound by our imagination. That is the beauty of it: once footers are understood not as a point of probable departure but as a point of pending decision, the possibilities are endless, as long as they stay in line with the context of the website.</p>
<p>One of the most common uses of the footer area is the placement of a feedback form. Gaining immediate feedback can be immensely valuable, and is most famously used in blogs. Bloggers have almost unanimously included comments at the bottom of every article, but news and corporate sites also are slowly beginning to embrace this opportunity for user feedback.</p>
<p>Other pages, like <a href="http://last.fm/">last.fm</a> or <a href="http://skype.com/">skype.com</a> put almost their complete navigation or sitemaps beneath the content area. Whereas this certainly offers the user ideas about where to go next, the sheer amount of options may cause the opposite effect of what is intended.</p>
<p>E-commerce pages may want to display related products to the one described on the respective page (a feature promoted by Amazon, for example), a link to an order tracking form or to their support area, and possibly customer reviews.</p>
<p>A company providing services might offer links to other services than the one highlighted on the page and showcase excerpts of their portfolio to complement the description of their work.</p>
<p>Footers could also include links to partner organisations, contact information, promotional elements, recent news – again, there are endless possibilities.</p>
<p>Just as long as we comprehend our footers’ potential as beginnings instead of mere endings.</p>
Browsertesting can be fun!2008-08-18T00:00:00-00:00https://annualbeta.com/blog/browsertesting-can-be-fun/<p>Okay, enthusiasm may have gotten the better of me when I was punching in this entry's title. But even when you're doing what every frontend engineer hates by default – testing and retesting your page in the major browsers known to mankind – there are a few tools that can really make things easier for you.</p>
<p>Let us revisit a great talk by Nate Koechley first. I saw him at this year's @media conference where he went and instilled some (needed?) pride into those of us who work the (X)HTML/CSS/JS/DOM/Browser angle of websites. Is frontend engineering a "real" job? Hell yeah! Check out the <a href="http://nate.koechley.com/blog/2008/06/11/slides-professional-frontend-engineering/">slides of his talk</a> on his blog and see how he arrives at the magic number of 672 different situational combinations of website/user environments that we have to prepare and test for.</p>
<p>So what can ease the job with the actual testing work? It goes without saying that you should always develop with at least three to four browsers, namely the current builds of IE7, FF3, Opera and Safari, open next to your IDE. It's miles better to check browser behaviour immediately than finishing off a template for Firefox, opening it in IE and having to start all over again.</p>
<p>I have the advantage of working on a Mac with Windows XP running on a virtual machine, so I can cover these two operating systems and the main browsers with relative ease. But what about testing multiple versions of the same browser?</p>
<h2>Testing multiple Internet Explorer versions in Windows: IE Tester</h2>
<p>Up until recently my favourite tool for testing multiple IEs was the aptly named <a href="http://tredosoft.com/Multiple_IE"> Multiple IE</a>. However, when I ran into problems with testing browser specific styles (via conditional comments) I went looking for an alternative and found <a href="http://www.my-debugbar.com/wiki/IETester/HomePage">IE Tester</a>. This one is working flawlessly so far and even includes a IE8 beta engine for pretesting, if you're so inclined.</p>
<h2>Testing multiple Firefox versions: MultiFirefox</h2>
<p>Since Firefox is Firefox you'd guess it's fairly easy to set up two parallel installations, right? Well, it's not quite that simple, since FF has a tendency to overwrite itself and/or its user profiles when you install a second version.</p>
<p>For Mac users there's a pretty straightforward solution called <a href="http://davemartorana.com/multifirefox/">MultiFirefox</a> that handles profile management for you. Install FF3 and FF2 and the application will take your hand and help you set yourself up.</p>
<p>Windows users have to go through a bit more manual tuning work, which is <a href="http://www.command-tab.com/2008/06/18/how-to-run-firefox-2-and-3-simultaneously/">nicely covered by Command-Tab.com</a>. They also mention MultiFirefox as the tool for MacOS.</p>
Preach What You Practice2008-10-07T00:00:00-00:00https://annualbeta.com/blog/preach-what-you-practice/<p>webfactory has always been about creating highly usable and accessible websites, keeping a close watch on <a href="http://www.webstandards.org/learn/">web standards</a> and the <a href="http://microformats.org/wiki/POSH">POSH paradigm</a>. This September we finally decided it was time to start preaching what we practice.</p>
<p>So you say, „Dude, get real! It’s 2008! Web standards are not only known among the web working crowd, they’re our bread and butter!“. Sadly, there are still websites <a href="http://www.seitenbacher.de/">proving you wrong</a>. There is still need to spread the gospel and point both established and new web workers in the right direction.</p>
<p><img src="https://annualbeta.com/blog/preach-what-you-practice/per-lectures-the-group.jpg" alt=""></p>
<p>On a sunny Saturday afternoon we invited a group of students who are serving apprenticeships as digital media designers like webfactory’s own Søren Birkemeyer into our company's Bistro and set about to highlight the advantages of semantic HTML and web standards. After an initial phase of explaining concepts and best practices, we confronted the group with a real-world task: translating a finished design (a Photoshop file) into HTML and CSS.</p>
<p>With one exception, all attendees had a print-related background in their companies and were rather new to both HTML and CSS. And yet, after only half an hour, people got to grips with our <a href="http://notepad-plus-plus.org/">editor of choice</a> for the workshop and the first discussions arose about correct semantics and markup. „Is this a headline at all?“ „I have ‚by’ and ‚from’ looking special to me in this sentence, but if I emphasize them that’s not what I really want, or is it?“</p>
<p><img src="https://annualbeta.com/blog/preach-what-you-practice/kicker.jpg" alt=""></p>
<p>With regard to the exemplary weather we had been planning for a quite open workshop, hoping for perhaps two hours of concentrated work before the session dissolved around beers and a game or two on our foosball table. Alas, we ended up closing the door behind the last die-hards after seven hours of hard work, coffee and beautiful code.</p>
<p>We are really happy about the turnout and will certainly continue with another workshop in the near future, so stay tuned! After all, who could resist the chance to turn people into POSH-<a href="http://www.randsinrepose.com/archives/2007/11/11/the_nerd_handbook.html">nerds</a>?</p>
wfDevCamp 2008: Work Away From Work2009-01-28T00:00:00-00:00https://annualbeta.com/blog/wfdevcamp-2008-work-away-from-work/<p>Sometimes, special tasks require unusual measures. It was obvious that we needed to move the development of wfDynamic, webfactory’s content management framework, a big step forward. So in December 2008 the webfactory team headed to an apartment in the Austrian mountains for a week of focused, uninterrupted work.</p>
<p><img src="https://annualbeta.com/blog/wfdevcamp-2008-work-away-from-work/200808812-balcony-sunny-pano-enhanced.jpg" alt=""></p>
<p>In the middle of daily routines, it is difficult to bring all the people together that are needed to achieve giant leaps in product development. At a small software development company like webfactory, everyone is working on different projects with different rhythms. There are meetings with customers, impromptu meetings of small sub teams on project-related minutiae and phone calls that need to be handled.</p>
<p>But we needed to get things done: wfDynamic heavily relied on user interface techniques that didn't comply to today's web standards and accessibility guidelines. Replacing these techniques by state-of-the-art alternatives was a job that needed time, focus and every skill we had on the team.</p>
<p>We felt that a week away from the office with the whole team would be the break we needed. Enough distance from „business as usual“ to really be able to zone into product development. The idea of a webfactory DevCamp was born.</p>
<h2>Laying the groundwork</h2>
<p>The first step to take was finding a location. We agreed on a few particular requirements:</p>
<ul>
<li>far enough from home to have a clean cut from everyday life</li>
<li>no distractions (no phone, no emails)</li>
<li>a flexible and inspiring working environment for the team</li>
<li>attractive options for breaks</li>
</ul>
<p>We were very fortunate to find a place that almost perfectly met our requirements in the Austrian Alps. One team member's family had been using a holiday apartment in Westendorf, Tyrol for years. The apartment, situated on a small mountain road, had a good size to accomodate our company of five. The fresh mountain air and great views over the valley from our balcony provided an inspiring background for a concentrated working atmosphere. A ski lift in walking distance and many hiking trails through the snow-covered forest behind the house were just calling for attention whenever we needed a longer break. However, most of the time we were too captivated by our ideas and discussions to even consider going outside.</p>
<h2>The setup</h2>
<p>Westendorf is about 700km south of our office. We decided to hire a van for the trip to foster team communication even on the journey.</p>
<p>On arrival in Westendorf we hit the next „Hofer“ discount market to buy some food and a <a href="http://www.yesss.at/diskont-surfen/home.php">„yesss“ mobile internet flatrate</a> with USB stick. Equipped with the USB stick, a Mac Mini was serving as our router for internet access and also as host for our version control repository. It took a while to set up, but after we got it running it didn’t let us down once.</p>
<h2>Discussions</h2>
<p><img src="https://annualbeta.com/blog/wfdevcamp-2008-work-away-from-work/img_1446.jpg" alt=""></p>
<p>The team that set to work reinventing our product was quite diverse. Besides the more obvious division in software developers and interface designers, there were also subtly different flavours to each member. Analytical minds mixed with visual thinkers, the urge to save time for careful consideration clashed with pragmatic hands-on approaches. The question to what extent we would reuse the old code or start from scratch was another controversial subject.</p>
<p><img src="https://annualbeta.com/blog/wfdevcamp-2008-work-away-from-work/img_4422.jpg" alt=""></p>
<p>All of it led to some very difficult but interesting and productive discussions throughout the week. Incidentally, it took more than half a day before the developers touched the first lines of code. The designers dug into Photoshop to knock up a few visual ideas while the most fundamental code discussions were under way.</p>
<h2>Iterative Improvements</h2>
<p>It was interesting to see how we tended to discard the previous day’s ideas every morning because someone had come up with an even better approach after a break and/or getting a few hours of sleep. At last, Day 4 of our stay saw the first implementations of our new designs.</p>
<h2>Sum of the parts</h2>
<p><img src="https://annualbeta.com/blog/wfdevcamp-2008-work-away-from-work/img_1606.jpg" alt=""></p>
<p>After getting a better idea of the user's flow through and interactions with our application, the designers went back to their wireframes and started implementing them based on growing HTML outputs from the development faction. It worked quite smoothly and we had a rudimentary system up and running on the last day. With a lot of work left ahead, but a solid new code foundation under our belts, we returned home in high spirits.</p>
<h2>Conclusion</h2>
<p><img src="https://annualbeta.com/blog/wfdevcamp-2008-work-away-from-work/P1010739.jpg" alt=""></p>
<p>The webfactory DevCamp exceeded all our expectations. The development results were fabulous and we are absolutely convinced that we could never have achieved anything remotely similar during regular hours at the office. The event was also a great team building experience - shared memories of our day out (braving the slopes on sleighs) and of a few quite effective discussions during the shorter hikes around the house top the charts next to the energetic and productive atmosphere during development.</p>
<p>We will definitely do this again!</p>
8 Golden Rules for Apprentices2010-04-21T00:00:00-00:00https://annualbeta.com/blog/8-golden-rules-for-apprentices/<p>Last week, we finally found a second apprentice at <a href="https://www.webfactory.de/">webfactory</a> and completed our team lineup for 2010. Simon Mönch will be joining us from May 1st to learn the craft of software development and Jessica Lazarus starts her apprenticeship for digital media design on August 1st.</p>
<p>Cheekily, we're using this as an excuse to formulate a set of guidelines for apprentices, but of course the following concepts apply to everyone on the team.</p>
<ol>
<li>
<p><strong>Look, Listen & Learn</strong><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">There's a lot to learn! Look around and listen to what the other team members do and say. Try to follow up on concepts and resources (articles, books, videos, ..) that get mentioned.</small></p>
</li>
<li>
<p><strong>Be curious and enthusiastic</strong><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">We're all of us grinning (slightly mad) and on our toes most of the time, because the web is such a fascinating environment with so many changes and improvements every day. Try and add your enthusiasm to the mix, and stay ahead of trends in your main areas of interest.</small></p>
</li>
<li>
<p><strong>Be punctual</strong><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">Being on time in the morning and with your deadlines (if you get assigned any) is a sign of professionalism and shows your respect for the team and the work we do.</small></p>
</li>
<li>
<p><strong>Be organised</strong><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">Being organised doesn't only mean to try and keep a clean desk. You also have to organise your files, scribbles and, most of all, thoughts. If you have a problem, try to solve it yourself first and, if that fails, think about what exactly you need to know to be able to continue on your own. We call this the art of asking good questions.</small></p>
</li>
<li>
<p><strong>Set yourself goals you can achieve</strong><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">Apart from completing the tasks you are given within our range of projects, it helps to set yourself learning goals. Which skill do you want to aquire next? Take notes about what helps you to learn or work faster, and what doesn't.</small></p>
</li>
<li>
<p><strong>Stay focused</strong><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">Work hard to stay on top of your tasks. This doesn't mean you need to put in overtime everyday, but you need to focus on what you are doing, pay attention to feedback and keep distractions to a minimum.</small></p>
</li>
<li>
<p><strong>Lessons learned</strong><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">At the end of each week, take a little break and ask yourself "What was the single most important thing I learned this week?". Write one or two (not more!) sentences about it into a dedicated notebook.</small></p>
</li>
<li>
<p><strong>Don't be afraid to ask</strong><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">Finally, talk to us. There are no bad or stupid questions – in fact, the most stupid question is the one you didn't ask. Open up and consider the critique you're getting. If you have a strong opinion about something that differs from what other team members say, don't lecture them but engage in a healthy discussion. Often, both sides can gain from this.</small></p>
</li>
</ol>
Inspiration break at ADC Summit 20102010-05-20T00:00:00-00:00https://annualbeta.com/blog/inspiration-break-at-adc-summit-2010/<p>From time to time, it's good to raise your head over the rim of your cubicle and take a look around. In a daring attempt to fulfill my desire for an overdue escape from day-to-day work, I ventured to Frankfurt and this year's ADC Summit Expo with projekt-pr's Rüdiger Hahn and Anke Schöneweiß, a freelance graphic designer. 11.000 square metres worth of design samples were waiting for us on this sunny Saturday.</p>
<p>While most of the exhibited ideas were well executed, it was only a handful that really caught my attention, and none did quite match my favourite from 2009. Still, a few caused a good laugh, while others delivered their message with such force that it took a while to digest the visual impact and sometimes gruelling information.</p>
<p>I took a few pictures of questionable quality on my iPhone before its battery died on me (that is to say, "died.. again" — great fun, but a different story):</p>
<p><img src="https://annualbeta.com/blog/inspiration-break-at-adc-summit-2010/kimjongil.png" alt="The designers use text colour to create the shape of a pair of hands gripping prison bars"></p>
<p><img src="https://annualbeta.com/blog/inspiration-break-at-adc-summit-2010/jeep.png" alt="A visceral interpretation of "No limits": a graffiti spray-on Jeep silhouette is seen driving out of a billboard frame"></p>
<p>Thankfully, these two were among my overall favourites. I absolutely love the simple and yet striking typographic visualisation in the first one, where the text doesn't need any accompanying image but becomes the image itself.</p>
<p>Jeep (or their agency) comes around with another strong punchline, "No limits.", and underlines it in a wonderful way. It is a reminder to think outside the box more often and even more unconventionally, and duly noted.</p>
Behind the curtain: preparing an interface for themes with rgba goodness2010-10-18T00:00:00-00:00https://annualbeta.com/blog/behind-the-curtain-preparing-an-interface-for-themes-with-rgba-goodness/<p>This is a peak under the hood of an ongoing project.</p>
<p>Encouraged by the widespread enthusiasm regarding CSS3 and its adoption across all browsers and platforms we encountered during this year's <a href="http://fronteers.nl/congres/2010">fronteers conference</a>, we decided to especially put rgba to work for us. The following screenshot shows different color versions of the same part of the interface. However, all that was changed is the background-colour – the elements comprising the interface are layered on top and use varying opacity values on their black or white backgrounds.</p>
<p><img src="https://annualbeta.com/blog/behind-the-curtain-preparing-an-interface-for-themes-with-rgba-goodness/css3layering.png" alt=""></p>
<p>The resulting interface will always have a pretty harmonious colour scheme and is adaptable with the change of just a single value: the overall background-colour (or -image). The fallback solution for browsers lacking rgba support will of course have to be carefully tweaked so it still delivers a satisfying experience without all effects.</p>
<p>We'll talk more about CSS3 and its massive advantages in a follow-up post as the project speeds by the next milestones.</p>
<p><strong>Update 12.04.2011:</strong> The approach can work really well, but in our case there were too many subtly different display elements to make it work flawlessly. In the end we backtracked from using rgba transparency and rather turned out two finely tuned skins that don't require workarounds for older, challenged browsers. The customer didn't have much use for the broad palette of different skins we had envisioned, either.</p>
<p>That being said, I'm still looking forward to a project where this might be put to more efficient use.</p>
Rands In Repose: The Art of Not2010-11-13T00:00:00-00:00https://annualbeta.com/blog/rands-in-repose-the-art-of-not/<p>There have been a great number of articles and opinons about Instagram lately (if you missed them, try google), but Rands’ <a href="http://www.randsinrepose.com/archives/2010/11/12/the_art_of_not.html">“The Art of Not”</a> — as usual — comes out near the top and is definitely worth sharing.</p>
<p>On a sidenote, Instagram has done two things to me. It has managed to rekindle my love of photography paired with a device that is always ‘there’ instead of longing for my dSLR which rarely accompanies me (this is a good thing). On the other hand it has me itching to sell my iPhone 3GS early and spend money on basically the camera of an iPhone 4 (this is bad).</p>
<p>Ironically this ties in well with the title Rands chose for his article: using Instagram is the art of happily not missing your dSLR, but also of not getting too tempted to upgrade your phone.</p>
New Year’s Resolutions: make more, do more, get more.2012-01-01T00:00:00-00:00https://annualbeta.com/blog/new-years-resolutions-make-more-do-more-get-more/<p>My resolutions for 2012 revolve around a single, common theme: more. I want to attain more of as much as possible, including</p>
<ul>
<li>more fitness</li>
<li>more creative output</li>
<li>more craftsmanship*</li>
<li>more fun</li>
<li>more traveling</li>
</ul>
<p>The overall mantra this year will be: don’t think too long about anything, just do it. I’m starting with a photo book documenting my 2010 journey to the north cape and the Lofoten islands.</p>
<p>Off to work!</p>
<p><small class="u-block margin-top u-background u-text-smaller padding-half">*craftsmanship: I will strive to hone and expand my existing skills in web front-end development and photography, with a bit of (copy-)writing thrown in on the side.</small></p>
LESS Quicktips: escaping / (slash) in shorthand2014-04-02T00:00:00-00:00https://annualbeta.com/blog/less-quicktips-escaping-slash-in-shorthand/<p>There are some CSS shorthand declarations that require the use of <code>/</code> which LESS may interpret as the math operator for a division (depending on the values in your declaration). Examples for this are:</p>
<pre class="language-less"><code class="language-less"><span class="token selector">.selector</span> <span class="token punctuation">{</span><br> <span class="token property">font</span><span class="token punctuation">:</span> font<span class="token operator">-</span>style font<span class="token operator">-</span>variant font<span class="token operator">-</span>weight font<span class="token operator">-</span>size<span class="token operator">/</span>line<span class="token operator">-</span>height font<span class="token operator">-</span>family<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>and</p>
<pre class="language-less"><code class="language-less"><span class="token selector">.selector</span> <span class="token punctuation">{</span><br> <span class="token property">background</span><span class="token punctuation">:</span> background<span class="token operator">-</span>color background<span class="token operator">-</span>image background<span class="token operator">-</span>position<span class="token operator">/</span>background<span class="token operator">-</span>size background<span class="token operator">-</span>repeat background<span class="token operator">-</span>attachment<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>I ran into the latter use case while defining multiple backgrounds in shorthand. You can escape the slash in LESS using <code>e('/')</code>, e.g.</p>
<pre class="language-less"><code class="language-less"><span class="token selector">.selector</span> <span class="token punctuation">{</span><br> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span>"../images/bg.png"<span class="token punctuation">)</span></span> center 300px <span class="token function">e</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">)</span> 70% no<span class="token operator">-</span>repeat<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>Alternatively, there is the option to enable <a href="http://lesscss.org/usage/#command-line-usage-strict-math">"strict math" in LESS</a>; when strict math is enabled, all math operations must be wrapped in parenthesis (10px / 5px) which causes overhead there, but solves the slash ambiguity for shorthand.</p>
<p>Choose your poison!</p>
<p>PS: Obviously you can also avoid this by not using shorthand for cases like font and background.</p>
Responsive Email Resources2014-04-25T00:00:00-00:00https://annualbeta.com/blog/responsive-email-resources/<p>Responsive email is a rapidly growing field; if you need to get up to speed with current developments, check out <strong><a href="http://responsiveemailresources.com/">Responsive Email Resources</a></strong> which has a good overview of approaches and patterns for all domains from planning to implementation.</p>
Practical ARIA examples2014-05-06T00:00:00-00:00https://annualbeta.com/blog/practical-aria-examples/<p><a href="http://heydonworks.com/practical_aria_examples/">Practical ARIA Examples</a> has some handy snippets and approaches, e.g. for accessible hamburger menu icon, tab navigation and alerts (among others).</p>
IE8 Failsheet for Responsive Websites2014-06-02T00:00:00-00:00https://annualbeta.com/blog/ie8-failsheet-for-responsive-websites/<p>If - like me - you're working on a responsive website project which still has IE8 in the browser matrix, you may find my failsheet helpful during development. It simply lists all the techniques that IE8 does not support natively and keeps you on your toes when you're about to get into a CSS3 feature flow extravaganza.</p>
<p>It doesn't say anything about possible fallbacks and/or polyfills, those are left to your judgment. Just keep<br>
in mind that it's often possible to use :first-child instead of :last-child or get approval for a rendering without text-shadow in IE8 from both design and client, and I suggest to try those avenues before you venture down the Selectivizr, CSS3PIE, respond.js etc. route (which each come with their own pitfalls).</p>
<p>Enjoy and please feel free to comment about stuff I missed or which you think is unnecessary!</p>
<p><a href="https://annualbeta.com/blog/ie8-failsheet-for-responsive-websites/ie8-failsheet_20140602-sb.pdf">Download Failsheet (PDF, 135kb)</a></p>
Quickly test your HiDPI media queries on a 1x PC or Mac2014-06-30T00:00:00-00:00https://annualbeta.com/blog/quickly-test-your-hidpi-media-queries-on-a-1x-pc-or-mac/<p>If you want to do a quick check whether your high resolution media queries are working, but you don't have a retina tablet at hand, here's a nice little trick for you:</p>
<ol>
<li>Open Firefox</li>
<li>Go to about:config (and sign with your blood that you know what you're doing and will be super careful)</li>
<li>Search for "PixelsPerPx"</li>
<li>Change the value to "2" for a 2x resolution simulation</li>
</ol>
<p><img src="https://annualbeta.com/blog/quickly-test-your-hidpi-media-queries-on-a-1x-pc-or-mac/quickly-test-your-hidpi-media-queries-on-a-1x-pc-or-mac-1.jpg" alt=""></p>
<p>Firefox will now render everything at twice its size (2 screenpixels for every 1 CSS pixel, if you will). You can verify it's working with something like</p>
<pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">-webkit-min-device-pixel-ratio</span><span class="token punctuation">:</span> 1.25<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token property">min-resolution</span><span class="token punctuation">:</span> 120dpi<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br> <span class="token selector">body </span><span class="token punctuation">{</span><br> <span class="token property">background-color</span><span class="token punctuation">:</span> pink<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>That's it, Happy testing!</p>
Vertical centering of containers with variable max- but defined min-height2014-07-01T00:00:00-00:00https://annualbeta.com/blog/vertical-centering-of-containers-with-variable-max-but-defined-min-height/<p><strong>tl;dr:</strong> vertical centering is still lots of fun if your use case includes containers of variable height, but with a set min-height. If you have no min-height, you can pick either way and you're done.</p>
<p>CSS (and browser support in 2014) offers two possible solutions, but each comes with its own drawback:</p>
<ul>
<li>Use <code>display: table;</code><br>
<small class="u-inline-block margin-top-fourth">Using table-display for vertical centering will get you far, but not past Firefox who will not use min-height on tables or CSS tables.</small><br>
<small class="u-inline-block margin-top-fourth margin-bottom-half">Check the <a href="http://codepen.io/polarbirke/pen/Amhlw/">CodePen for display: table;</a></small></li>
<li>Use <code>display: flex;</code><br>
<small class="u-inline-block margin-top-fourth">Using flexbox for vertical centering will get you far, but not past IE11 who will not use min-height on elements with display: flex.</small><br>
<small class="u-inline-block margin-top-fourth">Check the <a href="http://codepen.io/polarbirke/pen/uayoc">CodePen for display: flex;</a></small></li>
</ul>
<p>My solution: Use flexbox where possible (feature detection) with table-display as fallback; also use table-display for IE11 despite successful flexbox detection.</p>
<p>Pro tip: you can target IE11 (and IE10) using a MS specific media-query, since conditional comments are no longer supported by IE10+. Here's the (slighhtly hacky, but still nicer than UA sniffing) snippet:</p>
<pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@media</span> all <span class="token operator">and</span> <span class="token punctuation">(</span><span class="token property">-ms-high-contrast</span><span class="token punctuation">:</span> none<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token property">-ms-high-contrast</span><span class="token punctuation">:</span> active<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br> <span class="token comment">/* IE10+ CSS styles go here */</span><br><span class="token punctuation">}</span></code></pre>
<p>src: <a href="http://philipnewcomer.net/2014/04/target-internet-explorer-10-11-css/">http://philipnewcomer.net/2014/04/target-internet-explorer-10-11-css/</a></p>
1px hairline CSS borders on HiDPI screens2014-07-04T00:00:00-00:00https://annualbeta.com/blog/1px-hairline-css-borders-on-hidpi-screens/<p>Supposedly we've come to grips with CSS pixels and device pixels by now and gotten past our <a href="http://alistapart.com/article/a-pixel-identity-crisis/">Pixel Identity Crisis</a>. <a href="https://css-tricks.com/snippets/css/retina-display-media-query/">Retina Display Media Queries</a> are all good and dandy, but there is something that remains tricky: CSS borders.</p>
<p>Imagine having a great and esteemed brand, a hotshot designer and a bunch of subtle lines in your interface. What are you going to do? Right, you'll use borders.</p>
<pre class="language-less"><code class="language-less"><span class="token selector">.my-element</span> <span class="token punctuation">{</span><br> <span class="token property">border</span><span class="token punctuation">:</span> 1px solid #f5f5f5<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>That looks nice until you check your project on a device with a HiDPI screen. Even though you may professionally ignore the fact that your borders look slightly less subtle, your hotshot designer will probably be less sloppy and check it out in Photoshop. After all, you're tasked to deliver a shiny interface that pulls out all the stops for your client.</p>
<p><img src="https://annualbeta.com/blog/1px-hairline-css-borders-on-hidpi-screens/how-to-1px-hairline-css-borders-on-hidpi-screens-1.jpg" alt="A magnified screenhot of two lines, one is twice as wide as the other" title="Not so thin: the border looks fat on HiDPI screens"></p>
<p>So what happened? Thanks to the way CSS pixels are treated on HiDPI screens, your border was blown up to twice its size and you're now stuck with an interesting challenge. "Is it possible to define 0.5px borders for retina screens?" you will inevitably ask. The answer is: no(t yet). It'll work only in Firefox and Safari 8 (introduced in OS X Yosemite).</p>
<p>There are workarounds using <code>border-image</code> and/or multiple background images. I have opted for a third approach: <code>transform(scale)</code>. Below is my documented LESS mixin stack:</p>
<pre class="language-less"><code class="language-less"><span class="token comment">/** Mixin stack for hairline borders on HiDPI screens<br>* [1] Sets a standard border for all devices<br>* [2] Matches devices with a HiDPI screen resolution<br>* [3] Creates a pseudo-element before the element that has the border<br>* [4] Resets the border on the original element<br>* [5] Sets the desired border on the pseudo-element<br>* [6] Positions the pseudo-element absolutely<br>* [7] Positions the original element relatively if is not yet positioned (set to false if<br> your original element is already positioned)<br>* [8] Scales the pseudo-element up to twice the size of the original element via width and height<br>* [9] Scales the pseudo-element back down to the correct size via CSS Transform<br> > This is where the magic happens: the border is scaled down to 0.5 CSS pixels which will render <br> as 1 device pixel on HiDPI screens<br>*/</span><br><br><span class="token selector">.border-hidpi-base(<span class="token variable">@positionRelative</span>)</span> <span class="token punctuation">{</span><br> <span class="token atrule">@media <span class="token punctuation">(</span>@hidpi<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">// [2] </span><br> .<span class="token function">position-relative</span><span class="token punctuation">(</span><span class="token variable">@positionRelative</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// [7]</span><br> <br> <span class="token selector">&:before</span> <span class="token punctuation">{</span> <span class="token comment">// [3] </span><br> .<span class="token function">transform</span><span class="token punctuation">(</span><span class="token function">scale</span><span class="token punctuation">(</span>0.5<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// [9]</span><br> .<span class="token function">transform-origin</span><span class="token punctuation">(</span>0 0<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <br> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">""</span><span class="token punctuation">;</span><br> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span> <span class="token comment">// [6]</span><br> <span class="token property">top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br> <span class="token property">left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br> <span class="token property">width</span><span class="token punctuation">:</span> 200%<span class="token punctuation">;</span> <span class="token comment">// [8]</span><br> <span class="token property">height</span><span class="token punctuation">:</span> 200%<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.border-hidpi(<span class="token variable">@side</span>, <span class="token variable">@color</span>, <span class="token variable">@positionRelative</span>: true) when (<span class="token variable">@side</span> = all)</span> <span class="token punctuation">{</span><br> <span class="token mixin-usage function">.border-hidpi-base</span><span class="token punctuation">(</span><span class="token variable">@positionRelative</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token property">border</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span> <span class="token comment">// [1]</span><br><br> <span class="token atrule">@media <span class="token punctuation">(</span>@hidpi<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br> <span class="token property">border-width</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token comment">// [4]</span><br><br> <span class="token selector">&:before</span> <span class="token punctuation">{</span><br> <span class="token property">border</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span> <span class="token comment">// [5]</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.border-hidpi(<span class="token variable">@side</span>, <span class="token variable">@color</span>, <span class="token variable">@positionRelative</span>: true) when (<span class="token variable">@side</span> = top)</span> <span class="token punctuation">{</span><br> <span class="token mixin-usage function">.border-hidpi-base</span><span class="token punctuation">(</span><span class="token variable">@positionRelative</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token property">border-top</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span><br><br> <span class="token atrule">@media <span class="token punctuation">(</span>@hidpi<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br> <span class="token property">border-top-width</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><br> <span class="token selector">&:before</span> <span class="token punctuation">{</span><br> <span class="token property">border-top</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.border-hidpi(<span class="token variable">@side</span>, <span class="token variable">@color</span>, <span class="token variable">@positionRelative</span>: true) when (<span class="token variable">@side</span> = right)</span> <span class="token punctuation">{</span><br> <span class="token mixin-usage function">.border-hidpi-base</span><span class="token punctuation">(</span><span class="token variable">@positionRelative</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token property">border-right</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span> <br><br> <span class="token atrule">@media <span class="token punctuation">(</span>@hidpi<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br> <span class="token property">border-right-width</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><br> <span class="token selector">&:before</span> <span class="token punctuation">{</span><br> <span class="token property">border-right</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.border-hidpi(<span class="token variable">@side</span>, <span class="token variable">@color</span>, <span class="token variable">@positionRelative</span>: true) when (<span class="token variable">@side</span> = bottom)</span> <span class="token punctuation">{</span><br> <span class="token mixin-usage function">.border-hidpi-base</span><span class="token punctuation">(</span><span class="token variable">@positionRelative</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token property">border-bottom</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span><br><br> <span class="token atrule">@media <span class="token punctuation">(</span>@hidpi<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br> <span class="token property">border-bottom-width</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><br> <span class="token selector">&:before</span> <span class="token punctuation">{</span><br> <span class="token property">border-bottom</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span> <br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.border-hidpi(<span class="token variable">@side</span>, <span class="token variable">@color</span>, <span class="token variable">@positionRelative</span>: true) when (<span class="token variable">@side</span> = left)</span> <span class="token punctuation">{</span><br> <span class="token mixin-usage function">.border-hidpi-base</span><span class="token punctuation">(</span><span class="token variable">@positionRelative</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token property">border-left</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span><br><br> <span class="token atrule">@media <span class="token punctuation">(</span>@hidpi<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br> <span class="token property">border-left-width</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><br> <span class="token selector">&:before</span> <span class="token punctuation">{</span><br> <span class="token property">border-left</span><span class="token punctuation">:</span> 1px solid <span class="token variable">@color</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span> <br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token comment">/** Helper mixin in case you need to reset the border */</span><br><span class="token selector">.border-hidpi-reset()</span> <span class="token punctuation">{</span><br> <span class="token property">border-width</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><br> <span class="token atrule">@media <span class="token punctuation">(</span>@hidpi<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br> <span class="token selector">&:before</span> <span class="token punctuation">{</span><br> <span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br> <span class="token punctuation">}</span> <br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token comment">/** Helper mixin for relative positioning if variable is set when invoking the caller mixin */</span><br><span class="token selector">.position-relative(<span class="token variable">@positionRelative</span>) when (<span class="token variable">@positionRelative</span> = true)</span> <span class="token punctuation">{</span><br> <span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The "magic" part is that <code>transform</code> scales down the element after the 1px CSS border has been calculated (and no matter how much you blow up your width and height, the border is still 1px wide).</p>
<p>You can check out a demo here: <a href="http://codepen.io/polarbirke/pen/dlyvF">http://codepen.io/polarbirke/pen/dlyvF</a></p>
<p>So far, I've used this technique with great success. Try it out if you like (and make your designer happy)!</p>
"The" Android Browser2014-07-17T00:00:00-00:00https://annualbeta.com/blog/the-android-browser/<p>A round-up of mostly everything you never wanted to know about browser fragmentation on Android.</p>
<blockquote>
<p>If somebody tells you they tested their website on Android, laugh evilly and show them this slidedeck.</p>
<p><cite><a href="https://slides.com/html5test/the-android-browser#/">The Android Browser • A presentation by HTML5Test</a></cite></p>
</blockquote>
<p>Enjoy!</p>
Responsive images cook book2014-07-24T00:00:00-00:00https://annualbeta.com/blog/responsive-images-cook-book/<p>Andreas Bovens has published a the comprehensive round-up of use cases and example code for "responsive images" at Dev.Opera:</p>
<p><strong><a href="https://dev.opera.com/articles/responsive-images/">Dev.Opera — Responsive Images: Use Cases and Documented Code Snippets to Get You Started</a></strong></p>
<p>You should also read Eric Portis' in-depth article about <a href="https://ericportis.com/posts/2014/srcset-sizes/">srcset & sizes</a>. Filament Group's <a href="http://scottjehl.github.io/picturefill/">picturefill</a> will be familiar, but I'll link it for the sake of completeness.</p>
<p><strong>Update August 2014:</strong> here's a behind-the-scenes summary by Yoav Weiss, who's is working on the implementation: <a href="https://dev.opera.com/articles/native-responsive-images/">https://dev.opera.com/articles/native-responsive-images/</a></p>
<p><strong>Update October 2015:</strong> Jake Archibald published a nicely annotated overview of usecases and appropriate techniques: <a href="https://jakearchibald.com/2015/anatomy-of-responsive-images/">https://jakearchibald.com/2015/anatomy-of-responsive-images/</a></p>
Grunticon + svgmin + PNG fallbacks2014-09-04T00:00:00-00:00https://annualbeta.com/blog/grunticon-svgmin-png-fallbacks/<p>If you're using <a href="https://github.com/filamentgroup/grunticon">filamentgroup's grunticon</a> to inline SVG icons in your workflow and you're minifying them with <a href="https://code.google.com/archive/p/svgmin/">svgmin</a> before you're running Grunticon (as you should), you may run into broken PNG fallback images that are output by Grunticon.</p>
<p><strong>The bug:</strong> the PNGs contain a big blob of SVG text instead of the image.</p>
<p><strong>The reason:</strong> By default, svgmin removes the XML encoding header <code><?xml version="1.0" encoding="utf-8"?></code>. Without this, Grunticon's Phantom renderer doesn't know what to do and decides to put the SVG text into the PNGs.</p>
<p><strong>The fix:</strong> You need to configure svgmin with</p>
<pre class="language-js"><code class="language-js">svgmin<span class="token operator">:</span> <span class="token punctuation">{</span><br> options<span class="token operator">:</span> <span class="token punctuation">{</span><br> plugins<span class="token operator">:</span> <span class="token punctuation">[</span><br> <span class="token punctuation">{</span><br> removeXMLProcInst<span class="token operator">:</span> <span class="token boolean">false</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">]</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> dist<span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token operator">...</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>This stops svgmin from removing the encoding header and everything works as intended.</p>
LESS Extend in Responsive Projects2014-09-19T00:00:00-00:00https://annualbeta.com/blog/less-extend-in-responsive-projects/<p><strong>tl;dr:</strong> you can't extend a selector with a class that has been defined in a different <code>@media</code> scope from the one you are currently in.</p>
<p>In case you're wondering why your <code>&:extend(.my-super-class)</code> statements seem to work only when they feel like it: it might be because you're unwittingly leaving their <code>@media</code> scope.</p>
<p>Example: extending a class that has been defined outside any media query will not work if you're trying it in a LESS file that is referenced inside a media query for large screens.</p>
<p>There's documentation here: <a href="http://lesscss.org/features/#extend-feature-scoping-extend-inside-media">Language Features | EXTEND | Less.js</a></p>
<p>Hopefully this saves us a few head aches!</p>
Estimated number of global internet users has surpassed 3bil2014-11-19T00:00:00-00:00https://annualbeta.com/blog/estimated-number-of-global-internet-users-has-surpassed-3bil/<blockquote>
<p>Around 40% of the world population has an internet connection today. In 1995, it was less than 1%. The number of internet users has increased tenfold from 1999 to 2013. The first billion was reached in 2005. The second billion in 2010. The third billion in 2014.</p>
<p><cite>Internet Live Stats</cite></p>
</blockquote>
<p>Check <a href="http://www.internetlivestats.com/internet-users/">http://www.internetlivestats.com/internet-users/</a> for more stats. Unsurprisingly, China has the most active internet users, but surprisingly Nigeria has the highest (relative) growth rate, followed by India and Russia.</p>
Textarea loses current value in Firefox when cloned with jQuery2014-12-19T00:00:00-00:00https://annualbeta.com/blog/textarea-loses-current-value-in-firefox-when-cloned-with-jquery/<p>This bug has actually existed for about 5 years, but for various reasons it has not been fixed by the jQuery team. The official bug ticket is here: <a href="http://bugs.jquery.com/ticket/3016">jQuery Ticket #3016 [closed bug: patchwelcome]</a> and it is still a valid bug in Firefox 34.0.5.</p>
<p>I know that cloning a textarea is a super edge case, but if you happen to have to do it for some reason and your placeholders disappear in Firefox (which happened to me), it's because of this bug. I have put together an example on CodePen:</p>
<p class="codepen" data-height="520" data-theme-id="dark" data-default-tab="result" data-user="polarbirke" data-slug-hash="bNExRj" style="height: 615px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Text area loses current value when cloned in Firefox">
<span>See the Pen <a href="https://codepen.io/polarbirke/pen/bNExRj/">
Text area loses current value when cloned in Firefox</a> by polarbirke (<a href="https://codepen.io/polarbirke">@polarbirke</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>
<p>Happy debugging!</p>
What's up with AngularJS?2015-01-14T00:00:00-00:00https://annualbeta.com/blog/whats-up-with-angularjs/<p>I've been thinking about looking into Angular for a while now. I know a few people who swear by it, but, coincidentally, they're all backend developers (PHP/Symfony2). I actually do not know any frontend developers who actively use Angular. It's curious that this seems to be exactly the same situation that prompted ppk to write a rather lengthy <a href="http://www.quirksmode.org/blog/archives/2015/01/the_problem_wit.html">opinion piece about Angular</a> (well worth a read, I think, even if your mileage may vary). In fact, he felt compelled to say that "If one is uncharitably inclined, one could describe it as a front-end framework by non-front-enders for non-front-enders" about Angular 1.x’s suitability for modern web development.</p>
<p>I would love to hear about lessons learned and a few personal impressions to gain a broader view. Are the mentioned performance issues as severe as indicated? Is it really that mobile-unfriendly? Please do share your experiences <a href="http://twitter.com/polarbirke">@polarbirke</a>.</p>
<p><strong>Update January 15:</strong> ppk reacted to the huge amount of feedback with a <a href="http://www.quirksmode.org/blog/archives/2015/01/angular_and_tem.html">follow-up article</a> about his reservations regarding AngularJS.</p>
<p><strong>Update January 30:</strong> Jeremy Keith also chimed in with his column about <a href="https://adactio.com/journal/8245">Angular Momentum</a>.</p>
The almost sentient CSS grid system2015-01-15T00:00:00-00:00https://annualbeta.com/blog/the-almost-sentient-css-grid-system/<p>I have been working with grids for a long time now, and there is one challenge that pops up in every project: how many items do you put on a given row. Think, for example, of a row of news or product teasers. You may decide to go for 3 teasers per row, evenly spaced, for a certain breakpoint and move it up to 4 teasers per row on bigger screens. "That'll do fine", you'll say and deliver it to the client. A few days later she calls you and says "Hey, I really love the layout, but on some pages it looks so empty. Especially the homepage." Umm, what? So you go and check it out and voila: the client only put two news items on the homepage and they fill only half the available space in your bigger breakpoint. Before you call her back and tell her to just write more content - or before you start writing some Javascript or server-side logic that counts the news items and adds/removes grid classes to adjust the layout - what if, what if it was possible to make the grid system a bit smarter and handle this with pure CSS?</p>
<p>I saw someone mention a clever technique of chaining different nth-child selectors to achieve a kind of "nth child of m siblings" selection logic. I played around with it this morning and here's what I got:</p>
<p>If you want to have three evenly spaced items in a row, you can target them like so:</p>
<pre class="language-scss"><code class="language-scss">.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span><span class="token punctuation">,</span><br>.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token selector">~ .gs-column </span><span class="token punctuation">{</span><br> <span class="token property">width</span><span class="token punctuation">:</span> 33.333333%<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>"Target the first child of three total children as well as its siblings."</p>
<p>Moving up to four and five evenly spaced items is easily done:</p>
<pre class="language-scss"><code class="language-scss"><span class="token comment">/* 4 Teasers */</span><br>.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>4<span class="token punctuation">)</span><span class="token punctuation">,</span><br>.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>4<span class="token punctuation">)</span> <span class="token selector">~ .gs-column </span><span class="token punctuation">{</span><br> <span class="token property">width</span><span class="token punctuation">:</span> 25%<span class="token punctuation">;</span> <br><span class="token punctuation">}</span><br><br><span class="token comment">/* 5 Teasers */</span><br>.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>5<span class="token punctuation">)</span><span class="token punctuation">,</span><br>.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>5<span class="token punctuation">)</span> <span class="token selector">~ .gs-column </span><span class="token punctuation">{</span><br> <span class="token property">width</span><span class="token punctuation">:</span> 20%<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>Obviously this is really specific, but you can both make it more generic and create more complex rules with only minor tweaks. Say you want to target groups of 5, 8, 11, 14, ... items so that the first two share a row and are both 50% wide while all other items fill out the following rows in threes:</p>
<pre class="language-scss"><code class="language-scss">.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token property">first-child</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>3n+5<span class="token punctuation">)</span><span class="token punctuation">,</span><br>.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token property">first-child</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>3n+5<span class="token punctuation">)</span> <span class="token selector">~ .gs-column </span><span class="token punctuation">{</span><br> <span class="token property">width</span><span class="token punctuation">:</span> 50%<span class="token punctuation">;</span> <br><span class="token punctuation">}</span><br><br>.<span class="token property">gs-column</span><span class="token punctuation">:</span><span class="token property">first-child</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>3n+5<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token function">nth-last-child</span><span class="token punctuation">(</span>5<span class="token punctuation">)</span> <span class="token selector">+ .gs-column ~ .gs-column </span><span class="token punctuation">{</span><br> <span class="token property">width</span><span class="token punctuation">:</span> 33.333333%<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>If you create similar rules for different total numbers, you will end up with pleasing layouts where all rows are filled out all the time. <strong>No more empty spaces, no more need for placeholders.</strong></p>
<p><strong>An almost sentient CSS grid system.</strong></p>
<p>You can check it out and play around with it here: <a href="http://codepen.io/polarbirke/pen/NPpyGb">http://codepen.io/polarbirke/pen/NPpyGb</a></p>
<p>DISCLAIMER: This is just a cool CSS experiment to play around with, I wouldn't recommend to use it in production without lots of testing and thinking about fallback handling for older browsers.</p>
AEM (CQ 5.6) Front-end Workflow for LESS updates2015-03-27T00:00:00-00:00https://annualbeta.com/blog/aem-cq-5-6-front-end-workflow-for-less-updates/<p>I've recently joined a project where all front-end work is done directy in the CQ trunk instead of using a stand-alone approach with Handlebars as I was used to. What surprised me is the need to deploy everything if you want to see your CSS (LESS) or JS changes applied to your locally served website. Especially if you're involved in mainly developing a new theme using a fixed set of existing components, it becomes important to be able to refresh and check your (visual) changes quickly and often.</p>
<h2>The task</h2>
<p>What we need, then, is a quick way to update the static files without runing a <code>mvn install</code> every time.</p>
<h2>Possible solutions</h2>
<p>I have heard about and tried to use the internal VCS of CQ (vault) for this, but apparently using both vault and svn (or any other external VCS) together will result in plenty of funky conflicts (I never got far enough to see any, though). The other issue is that I failed to get vlt running on my Windows machine in the way that I wanted.<br>
The other way would be to develop with CRXDE but either I don't understand how that is meant to work or it really is a somewhat sub-optimal experience (I want to use my favourite IDE!).</p>
<h2>My approach</h2>
<p>Here's what I came up with: I use a really simple Gulp task to</p>
<ul>
<li>watch for changes in my LESS files</li>
<li>push the changed files to jcr_root with a curl command</li>
<li>refresh the local website using livereload with chrome plugin</li>
</ul>
<p>Here's the repo: <a href="https://github.com/polarbirke/aem-gulp-workflow">polarbirke/aem-gulp-workflow · GitHub</a></p>
<p>I'd love to hear your feedback about this — did I miss something obvious? Should we work on a Grunt plugin? Does it even work for you at all?</p>
The plural of Chrome is Chromia2015-05-11T00:00:00-00:00https://annualbeta.com/blog/the-plural-of-chrome-is-chromia/<p>ppk (Peter-Paul Koch, <a href="http://quirksmode.org/">http://quirksmode.org</a>) gave the second talk of day 1 at beyond tellerrand 2015 and completely nerded out at least half the audience with his topic of browser fragmentation on Android.</p>
<p>His talk is the distilled version of his book <a href="https://shop.smashingmagazine.com/mobile-web-handbook.html">“The Mobile Web Handbook”</a> and if you’re working on mobile websites I highly recommend you read the book or at least watch the video of his presentation (which will be made available a few weeks after the conference) or at the very least check his <a href="http://quirksmode.org/presentations/Spring2015/chromia_bt.pdf">slides</a> after reading this very article. You can also <a href="https://vimeo.com/138004183">watch the talk on vimeo</a>.</p>
<p>Having said that, here’s the hyper-distilled tl;dr version.</p>
<h2>Android is <s>differentiated</s> fragmented</h2>
<p>When Google offered device vendors their mobile OS for free (to get more data, of course), everybody in need of a platform that could compete with Apple jumped at the chance. The result: Sony, LG, htc, Samsung etc. all used Android and all looked and worked.. pretty much the same.</p>
<p><img src="https://annualbeta.com/blog/the-plural-of-chrome-is-chromia/android-phones-all-have-the-same-ui.jpg" alt="" title="Image copyright: Peter-Paul Koch"></p>
<p>Their marketing departments obviously hated this, so Google (in the spirit of making everybody happy and spreading their OS as widely as possible) allowed differentiation of both the Android OS and its browsers.</p>
<p>Yes, <em>browsers</em>, plural. Because while Android comes with a pre-installed default browser - usually based<br>
off Android WebKit at first, Chromium (the open-source base of Chrome) later – since a change in license agreements in Android 4.4, device vendors are also required to install Google Chrome (but not to make it the default). Wait, what?</p>
<h2>Chrome is not Chrome</h2>
<p>In the sense of differentiation (see above), device vendors started to toggle some of the ~120 switches in Webkit, later Chromium, to make <em>their</em> Android browser a bit more special than the other device vendors’ browser. They also don’t update their browser as often, which means that while Google Chrome (and the underlying Chromium) may already be at version 42, many users browsing on Android will actually be using Chromium 33, or 30, or 34, or 28. ppk tries to make sense of the fragmentation by calling Chromium on htc “HTC Chromium”, on Sony “Sony Chromium” and so on.</p>
<p>If you have a masochistic streak and want to see how bad the fragmentation is, have a look at this screenshot and also his slides.</p>
<p><img src="https://annualbeta.com/blog/the-plural-of-chrome-is-chromia/chromia-shares.jpg" alt="" title="Image copyright: Peter-Paul Koch"></p>
<h2>What does this actually mean?</h2>
<p>In the end, it is important to know about the fragmentation for two reasons: First, it’s going to get worse, not better. And second, there are a few differences that one needs to be aware of during development. Possible “wtf?!” moments include different support for <code><input type=”datetime”></code> (works in phones from htc, Sony, LG; doesn’t work in phones from Google, Xiaomi, or with Cyanogen mod) and <code>position: fixed;</code> (just don’t use it for mobile, ever). It might not be that many, but it’s always better to be aware of them than to spend hours trying to figure out why one Android phone works and another doesn’t.</p>
<h2>Oh, one more thing.</h2>
<p>Google Chrome on iOS is not Google Chrome, either. Apple does not allow other vendors to install their rendering engine on iOS, so what Google Chrome on iOS actually is, is a Google Chrome app that is running an iOS webview ~…(“Safari”) inside.</p>
<p>The mobile web? It's complicated.</p>
Please Update Picturefill (JS polyfill for responsive images)2015-06-01T00:00:00-00:00https://annualbeta.com/blog/please-update-picturefill-js-polyfill-for-responsive-images/<h2>All hands on deck!</h2>
<p>Older versions of Picturefill can net you broken images in both Microsoft Edge and Webkit Nightly. There’s already <a href="https://bugs.webkit.org/show_bug.cgi?id=144095">an issue logged for the problem</a>.</p>
<h2>Why is that an issue?</h2>
<p>The community is afraid that browser vendors like Microsoft will renege on their pledge to implement the Responsive Images spec because the impact of having no images shown is too big.</p>
<p>Find more details here: <a href="http://alistapart.com/blog/post/picturefill-upgrade">Picturefill Me In · An A List Apart Blog Post</a> and here: <a href="https://css-tricks.com/please-update-picturefill/">https://css-tricks.com/please-update-picturefill/</a></p>
<p>(And update picturefill to version 2.3.1 or newer.)</p>
Living Styleguide and automated visual tests with DSS and Galen2015-06-02T00:00:00-00:00https://annualbeta.com/blog/living-styleguide-and-automated-visual-tests-with-dss-and-galen/<p><strong>tl;dr:</strong> My team was tasked to convert a client’s white label platform from a desktop-first, fixed width layout to a fully responsive one and also to improve frontend code quality throughout the project. A living styleguide and automated visual regression tests helped us to meet our goals.</p><p></p>
<p>If you are only interested in our setup, skip to <a href="#setup">Setup</a> – if not, bear with us while we set the stage.</p>
<h2>Context</h2>
<p>We recently took over a project that is at heart a white label component library built on CQ (AEM) and used to power a great number of brand, sub-brand and campaign websites for a big industrial company. The project was originally started by a competitor and then taken over by various agency teams before it finally landed on our desks. As is common in large projects, the code had grown organically and we found many different flavors and best practices (some already outdated) when we started a high-level inventory.</p>
<p>Our first task was to implement a new campaign website for a sub-brand: in practice, we “only” had to create a new responsive theme, working exclusively in the LESS files. We immediately pinpointed two major challenges for us:</p>
<ol>
<li>We didn’t know the HTML markup for the components</li>
<li>We didn’t yet understand the LESS architecture (such as it was)</li>
</ol>
<p>The HTML markup was (and still is) suffering from a couple of drawbacks. It was conceived without the requirements of a responsive website in mind and as such sometimes lacking containers or class hooks that would have made our life easier. On the other side, AEM adds a plethora of containers and classes that we didn’t really need and much less dared to lean on.</p>
<p>The LESS files were a diverse bunch; hardly commented, rarely using mixins, sometimes using variables. The responsiveness that had already been implemented was based on a set of 17 distinct media queries mostly going from xx pixels to yy pixels and following a desktop-down approach.</p>
<h2>Goals</h2>
<p>Our code inventory led us to define clear goals that we wanted to work towards during our campaign project:</p>
<ol>
<li>Improve the LESS code quality
<ol>
<li>Move to a mobile first approach with less media queries</li>
<li>Let smart mixins do the hard work for us, i.e.
<ol>
<li>Introduce a mixin-based grid system for layout tasks</li>
<li>Introduce a mixin-based typography pattern system</li>
<li>Set up variables in a way that many future theme adjustments could be managed by tweaking variables, not CSS declarations</li>
</ol>
</li>
</ol>
</li>
<li>Facilitate maintenance
<ol>
<li>Comment liberally</li>
<li>Make the code more readable by agreeing on and following a code style</li>
<li>Create a living styleguide so components can be viewed in isolation and without CQ or existing demo content</li>
<li>Set up a system for automated visual tests as a first line of defense against regression bugs</li>
</ol>
</li>
</ol>
<h2 id="setup">Setup</h2>
<h3>Living Styleguide</h3>
<p>We agreed that a living styleguide would be just the thing to help us with both our challenges and our goals:</p>
<ul>
<li>We would get to work closely with the HTML and be able to see and play around with it in an isolated environment without need for client content</li>
<li>We would be able to refactor the LESS components one by one and track our progress by looking at the list of components in the styleguide</li>
</ul>
<p>After reviewing a few of the many, many solutions, we settled for <a href="https://github.com/DSSWG/DSS">DSS</a> (Documented Stylesheets). Most styleguide tools parse certain comment syntax in the referenced stylesheet files to create the styleguide HTML, and DSS is no exception. What made us choose DSS over the alternatives is the fact that the parser makes the data available as <code>JSON</code> and lets you do with it whatever you want. We use Handlebars to loop through our <code>JSON</code> and output an <code>index.html</code> with a list of our components as well as a <code>componentName.html</code> for each component.</p>
<p>We also extended the generic parser (includes component name, description, example markup and a rendered example of the component) with custom parsers:</p>
<ul>
<li>A “mobile first” tag is added to the component if the responsive approach was converted</li>
<li>A list of configurable visual parameters can be added as a quick reference for designers that need to adapt the theme</li>
<li>Mixins can be documented with a list of components that use them so it is easy to keep track of possible side effects</li>
</ul>
<p>The living styleguide is part of our project’s Grunt task workflow and can be automatically updated after every change to any LESS file (this makes sense when we’re working against the styleguide but can be omitted if we chose to work against a local CQ server and want to save grunt execution time).</p>
<p><img src="https://annualbeta.com/blog/living-styleguide-and-automated-visual-tests-with-dss-and-galen/documented-component-less-file.jpg" alt="A documented component LESS file" title="A documented component LESS file"></p>
<p><img src="https://annualbeta.com/blog/living-styleguide-and-automated-visual-tests-with-dss-and-galen/component-in-living-styleguide.jpg" alt="The component in the living styleguide" title="The component in the living styleguide"></p>
<h3>Automated visual tests</h3>
<p>Once we had the styleguide up and running with the first components, we looked for a way to set up visual tests and quickly settled for the <a href="http://galenframework.com/">Galen framework</a>. Galen is developed and used by eBay, so we hoped that it would be actively maintained for quite a while yet. The premise of Galen is simple: you define a number of test specs and test cases using a kind of pseudo-english syntax and then run them. Feedback is given on the command line and as a generated HTML report including screenshots.</p>
<p>We run Galen as part of our Grunt task workflows and have tasks for running the complete test suite as well as only a single component test case. Similar to automatic updates to the styleguide, it can make sense to have Galen react to any change or to only run the test suite a few times a day.</p>
<p><img src="https://annualbeta.com/blog/living-styleguide-and-automated-visual-tests-with-dss-and-galen/galen-test-spec.jpg" alt="Galen test spec using pseudo-english specifying the expected visual results" title="Galen test spec using pseudo-english specifying the expected visual results"></p>
<p><img src="https://annualbeta.com/blog/living-styleguide-and-automated-visual-tests-with-dss-and-galen/galen-test-case.jpg" alt="Galen test case specifying the browser, breakpoints and URL to test against" title="Galen test case specifying the browser, breakpoints and URL to test against"></p>
<p><img src="https://annualbeta.com/blog/living-styleguide-and-automated-visual-tests-with-dss-and-galen/galen-test-report.jpg" alt="Galen HTML test report overview" title="Galen HTML test report overview"></p>
<p><img src="https://annualbeta.com/blog/living-styleguide-and-automated-visual-tests-with-dss-and-galen/galen-test-report-drilldown.jpg" alt="Galen test report drilldown for a specific test case and breakpoint" title="Galen test report drilldown for a specific test case and breakpoint"></p>
<h2>Results & Lessons Learned</h2>
<p>Our decision to invest in a living styleguide and visual tests although they had to be retrofitted to an existing project has paid off tremendously:</p>
<ul>
<li>It was quite liberating to simply work on components after having had trouble getting demo content</li>
<li>It was also easy to keep track of our progress in various stages of the refactoring process and watching the styleguide grow gave us great visual feedback and a warm, fuzzy feeling of achievement</li>
<li>Running Galen and visual tests gave us great confidence and kept us sane when we were tweaking things in central mixins</li>
</ul>
<p>However, we also encountered a couple of new challenges that we will need to work around or fix in the future:</p>
<ul>
<li>The additional effort needed to document the LESS code and write test specs was not included in the original project estimation and schedule because both preceded our decisions</li>
<li>Especially the creation of test cases suffered from our lack of time (sounds familiar?)</li>
<li>Many components are heavily nested and have contextual styling quirks (i.e. <em>download</em> in <em>textimage</em> in <em>accordion</em> in <em>article</em>); this is very hard to document via comments in LESS files without creating a lot of bloat</li>
</ul>
<h2>Vision</h2>
<p>We want to develop and maintain a micro-framework of helpful tools for frontend development in CQ (AEM) projects. The framework will be based on node.js and Grunt and be available to coworkers as a (company internal) repo. We are looking to open source our work as soon as possible, if time and project constraints permit it — but your feedback is appreciated <em>right now</em>! Have you tried similar approaches? Does our solution make sense? Please share your thoughts!</p>
How Sappu helped SapientNitro to break boundaries at beyond tellerrand2015-06-18T00:00:00-00:00https://annualbeta.com/blog/how-sappu-helped-sapientnitro-to-break-boundaries-at-beyond-tellerrand/<h2>tl;dr</h2>
<p><strong>This is the tale of the truly collaborative effort behind "Sappu's Lost Memories", a project where every team member added a twist to the plot and helped to expand a simple idea into the complex storyscape we presented at btconf Düsseldorf 2015.</strong></p>
<h2>Brief</h2>
<p>All stories, however boundary-breaking, must begin somewhere. Sappu’s story started because the hiring team once again wanted Sapient to be present at the beyond tellerrand conference in Düsseldorf, an event with a focus on creative web design and development that has earned a high reputation in Europe for inspiring content that often broadens the audience’s horizon in unexpected ways. Sapient has been a long-time sponsor and already used the event as a stage for employer branding in past years. Traditionally, the hiring team would prepare a quiz (with support from Experience Technology) that tested participants’ frontend savvy. While the best answers were rewarded with prices, it was also a fun way to collect contact details for the company's Talent Relationship Pool. This year, however, we agreed during the kick-off meeting that we could and should raise the bar to better represent Sapient’s unique strengths.</p>
<h2>Conception</h2>
<p>We quickly came up with the two ideas that would define the whole project:</p>
<ul>
<li>Showcase our skills in and with innovative iBeacon technology</li>
<li>Build native apps to enable the tech, but also to put Sapient on participants’ home screens</li>
</ul>
<p>It didn’t take us long to flesh them out into the concept of a beacon-based scavenger hunt around the conference venue: Participants would basically download our app, search for beacons and answer quiz questions at every beacon they found.</p>
<h2>Collaborative evolution</h2>
<p>With roughly four weeks to go until the conference, we started to assemble a team for the project. While our app developers began to set up the apps and beacon integration, a few others sat down to think about how the scavenger hunt might work and look on mobile screens. We explored a few directions for the interface and got excited about silly ideas like sonar animations and submarine sounds. In the end, we didn’t really settle for any of them, but the most notable result of this initial brainstorming session was the idea to reward participants with the chance to enter a raffle. Entry to the raffle would be limited by a combination lock that could only be unlocked with the right code. To generate the code, every quiz answer would return a one-digit number to the participant so they could finish the game with a number code of four to six digits, depending on the amount of beacons we wanted to hide.</p>
<p>We pitched our idea to the rest of the team and talked about our vision of locking the lottery wheel with a cable lock in order to further gamify the raffle participation. Feedback was good and we discussed the impact a bit until suddenly someone suggested changing the cable lock to an Arduino or Raspberry Pi robot with a key pad. Everybody loved how this would add another twist to the story and we agreed to make it happen if we could.</p>
<p>At this point we took the rough concept to our UX and visual design people who focused on adding a few bells and whistles to our very basic ideas for the user experience. We agreed that the game should be open to more than only frontend developers and that the quiz questions should be answerable by a broader spectrum of the audience. Our creatives were quick to create a flow chart for the app experience and wireframes for the different screens we would have to implement. They also came up with a little robot icon in the process:</p>
<p><img src="https://annualbeta.com/blog/how-sappu-helped-sapientnitro-to-break-boundaries-at-beyond-tellerrand/robot.png" alt="A robot made out of red rectangles with an antenna on its head and the Sapient logo on its breast"></p>
<p>It was gratifying to see our ideas take shape and get a face. We were certain that the right backstory would help us to transform the still rather dry and technical beacon hunt into a fun and memorable experience. We played around with digital production domains like UX, Design, Development and Management and tried to connect them to gaming metaphors where players might enter different virtual worlds (“World of UX”, “Planet Management”) based on both their and our beacons’ physical location in the conference venue. While that would probably also have worked out, we kept coming back to the quirky little robot icon and eventually decided to make the robot a person and put him in the middle of our story. Sappu was born and we had finally found our story’s protagonist. The individual pieces of our plot started falling into place almost automatically: Sappu was promoted to the whimsical position of Chief Digital Delivery Assistant and we imagined him to be equipped with memory nodes for the different skills he would need. Suddenly we had a reason for our beacons – they were distress beacons in case Sappu lost his memory nodes. We decided we wanted Sappu to talk directly to our participants because he needed them for a crowdsourced memory node rescue mission. He could then reward them for their help by handing out a secret code that would enable them to join the otherwise inaccessible raffle.</p>
<p>While some of us were trying to put this to work by creating the interface copy and intro text, our UX team came up with yet another layer that we could apply to the story. Couldn’t we take up the earlier idea to build an Arduino robot and build him out as an actual robot look-alike who would greet the participants in the physical space at our booth and validate their secret code? Of course we could. We quickly debated whether this should be Sappu himself but settled for creating a colleague of his instead – Cistro.</p>
<h2>Implementation</h2>
<p>While Sappu’s story was growing in complexity and vitality, our developers got busy with actually implementing the game. Due to responsibilities in other teams and projects it was hard for anyone to finish the app alone, so in the end everybody pitched in where and as much as they could. Both the iOS and the Android code ended up changing steward no less than three times.</p>
<p>We decided early on that we wanted the native apps to handle the beacon logic (a technical necessity) but that we wanted to implement the actual game’s screens as WebViews in HTML, CSS and JavaScript instead of doing everything natively. We hit a few downsides of this decision later on (browser history management being one of them) but we also unknowingly hit a jackpot because the WebViews enabled us to adjust our beacon sensitivity on location in Düsseldorf (more about this later).</p>
<p>The frontend devs took up the job of writing the JavaScript logic that would accept and evaluate the beacon data from the native apps, translate it into a game state and display the correct screens while participants progressed through the scavenger hunt.</p>
<p>During a short review of how it felt to use the app we decided to add “more Sappu”. Our designer was subsequently asked to create a set off different Sappu SVG icons (in different emotional states) and accompanying storyboards for little animation sequences. We implemented them in JavaScript so we could trigger and pause them from outside as needed (we tried velocity.js but eventually chose GSAP to power the animations).</p>
<p>We were soon able to put the WebViews on a Sapient server, reference them in the native apps and stage the first test runs on devices from our device library. When people started walking around the office to look for hidden beacons with Sappu on their screens, we felt we had hit another important milestone.</p>
<p><video controls="" preload="metadata">
<source type="video/mp4" src="https://annualbeta.com/blog/how-sappu-helped-sapientnitro-to-break-boundaries-at-beyond-tellerrand/bt2.mp4">
Your browser does not support playing HTML5 video. You can <a href="https://annualbeta.com/blog/how-sappu-helped-sapientnitro-to-break-boundaries-at-beyond-tellerrand/bt2.mp4" download="">download a copy of the video file</a> instead.
</video></p>
<p>It was time to switch gears and move on to the physical space and Cistro. We went down to Team Black’s innovation lab, reserved a work station and had soon ordered the missing materials (a small display, a keypad, a wooden box that would serve as lottery wheel/Cistro’s torso). Our Team Robot soon had programmed an Arduino to accept and evaluate a four digit code and start up a servo motor if the code checked out. They then conceived that Cistro should rotate his head and reveal the lottery deposit slot for participants with the right code. It took some duct tape and tiny ball bearings until the head would move smoothly, but they got there in the end. After Cistro was dressed in red foil with the Sapient logo on his breast, Team Robot declared him ready for the show.</p>
<p><img src="https://annualbeta.com/blog/how-sappu-helped-sapientnitro-to-break-boundaries-at-beyond-tellerrand/sappu-in-assembly.jpg" alt="Three photos of a wooden box with a number keypad being assembled into a robot-like contraption" title="Cistro is being assembled in the workshop"></p>
<p>Meanwhile, our test runs had led us to discover a disconcerting truth: the beacons were working fine, but every device reacted with different sensitivity to the beacon signals. We wanted the apps to “find” a beacon once a participant entered a 10-15m radius around the hidden beacon. The apps reported the beacons’ signal strength as a value to our JavaScript and the JavaScript decided whether the necessary threshold was reached. The challenge: iPhones have a different sensitivity compared to Android phones. Not only that, but different Android phones also vary greatly in what signal strength they actually receive from the beacons. We started to think about a device-specific adjustment table but had to abandon the idea due to time constraints. Note: If you require homogenous values across many devices for accurate indoor navigation, there is probably no way around it. In the end we elevated the signal strength for Android devices to bring them to the level of iPhone sensitivity and accepted the issue that some Androids would make the game really easy (Samsung S4 Mini) whereas some Android owners would have to play on “Hard as Hell” difficulty (apologies to all 1+ One owners). In any case, having the final threshold defined in our ftp-hosted JavaScript proved to be a godsend because we could hide the beacons on site in Düsseldorf, test the difficulty and adjust our median required sensitivity on the fly – which was good, because due to building safety restrictions some beacons ended up quite a ways from the main walkways. If the value had been part of our native app logic, we would never have been able to make the necessary adjustments and update the apps in the app stores in time (we were only able to hide the beacons one day before the conference).</p>
<p><img src="https://annualbeta.com/blog/how-sappu-helped-sapientnitro-to-break-boundaries-at-beyond-tellerrand/sappus-lost-memories-v1-submitted.png" alt="A screenshot of the Apple AppStore "My Apps" dashboard" title="Screenshot of our Apple AppStore submission"></p>
<p>Up to this point we had had fantastic momentum and a lot of fun, so when Apple rejected our iOS app (“Missing meta data: please provide a video showing app functionality”) we had a little shock moment. We rallied and resubmitted, but were rejected again (“Video needs to be exactly 750 x 1334 pixels in size”) and again (“Video must be recorded at 30fps, yours is 60fps”) and, finally, again:</p>
<blockquote>
<p>Your app includes a contest but it does not:</p>
<ul>
<li>Include official rules for the sweepstake, which is required</li>
<li>Indicate that Apple is not a sponsor or involved in the activity in any manner</li>
</ul>
</blockquote>
<p>In hindsight, it seems trivial to clearly research all applicable requirements for a successful submittal, but we didn’t – and we suffered for it. Even a last minute effort by the combined force of the legal team (who helped us to write up a revised and approved T&C text) and another submittal for expedited review would not be able to save us: we were not able to launch the iOS app in time for the conference. Ironically, we created a paper-based fallback quiz for iPhone owners and went to Düsseldorf with only the Android app.</p>
<p><img src="https://annualbeta.com/blog/how-sappu-helped-sapientnitro-to-break-boundaries-at-beyond-tellerrand/sappu-5.jpg" alt="A photo of the completed robot; the wooden box was painted red and a pair of eyes attached to the head." title="Cistro is waiting for the conference to start"></p>
<h2>Launch</h2>
<p>Despite this rather significant drawback, we received a lot of positive feedback from visitors to our booth. Instead of making fun of us for not having an iOS app, our plight made for an excellent conversation starter on AppStore reviews, the battle of web vs. native and many other topics. We were very happy to see that people were genuinely interested in our capabilities from mobile development to beacon technology and that we could make them curious about Sapient as a company (we had the only booth with a robot!).</p>
<p>At the end of the conference we presented three happy winners with their raffle prices, and the hiring team with a lot of interesting contacts (11x the amount we generated in 2014) for their talent pool.</p>
<p><img src="https://annualbeta.com/blog/how-sappu-helped-sapientnitro-to-break-boundaries-at-beyond-tellerrand/sappu-in-action.jpg" alt="Two photos of the robot, one with its head open, showing raffle submissions" title="Participants had a lot of fun interacting with Cistro"></p>
<h2>Lessons learned</h2>
<p>Sappu’s success despite the iOS failure is a great return, but there are a few other learnings that I would like to emphasize. Here is a list of my personal take-aways for future projects:</p>
<p>Get the legal department involved at the start, not the end.</p>
<ul>
<li>Plan extra time for Apple’s AppStore reviews, then double it. Then double it again.</li>
<li>Stay in a loosely explorative mind throughout the whole project, you never know when the next idea will take shape.</li>
<li>As clichéd as it sounds: a truly collaborative effort results in a product that is more than just the sum of its parts.</li>
<li>If you hit the right story (or organizing idea), things may start to fall into place almost magically.</li>
<li>iBeacons are a fantastic technology at heart, but your results will vary across the diverse device landscape. A lot.</li>
<li>Once your projects cross over into physical spaces, never go anywhere without duct tape.</li>
<li>It is easier to motivate people by silently bringing them coffee instead of telling them to work harder.</li>
</ul>
<h2>Thanks</h2>
<p>My heartfelt thanks go out to everybody who made this project possible and that includes our staffing partners and many other teams who allowed the following people to donate time and bring Sappu to life.</p>
<p>In accidental order:</p>
<ul>
<li><strong>Michaela Schwarz</strong> got the ball rolling and defended our analogue flank (flyers, contact forms)</li>
<li><strong>Holger Hellinger</strong> provided oversight and the original beacon idea</li>
<li><strong>Florian Feiler</strong> came up with the Arduino idea and provided feedback throughout</li>
<li><strong>Patrick Sbrzesny</strong> came up with the idea to put a combination lock on the box</li>
<li><strong>Sara Ziskovic</strong> got involved with UX, design and went manual in Team Robot</li>
<li><strong>Felix Hofschulte</strong> carried his share of the UX work and created the robot icons</li>
<li><strong>Philipp Hammerschmidt</strong> was the mechanic in Team Robot and responsible for the Arduino</li>
<li><strong>Annick Querfeld</strong> wrangled HTML, CSS and JavaScript and got it all animated</li>
<li><strong>Dominik Pich</strong> set up the iOS app and beacon logic</li>
<li><strong>Stefan Jager</strong> took up the iOS torch and finished the app</li>
<li><strong>Jens Kreuels</strong> had the ungrateful task of dealing with Apple’s AppStore reviews</li>
<li><strong>Adalbert Schanowski</strong> got the Android App started</li>
<li><strong>Raanan Nevet</strong> came, saw, rolled up his sleeves and finished for Team Android</li>
<li><strong>Markus Ruhl</strong> jumped in very last-minute and provided legal support for our terms & conditions</li>
<li>…and, finally, <em>I</em> tried to manage things without getting in the way</li>
</ul>
How to recover momentum scrolling behaviour on Windows Phone2015-06-30T00:00:00-00:00https://annualbeta.com/blog/how-to-recover-momentum-scrolling-behaviour-on-windows-phone/<p>You're probably familiar with <code>-webkit-overflow-scrolling: touch;</code> as a way to restore momentum scrolling (sometimes also called inertia scrolling) on iOS devices. <a href="https://css-tricks.com/snippets/css/momentum-scrolling-on-ios-overflow-elements/">Chris Coyier has an example page</a> if you want to refresh your memory.</p>
<p>Now here's the thing: Windows Phone has similar scrolling behaviour which also is not "on" by default if you declare <code>overflow: scroll;</code> on a container. You need to toggle it specifically, so where you put <code>-webkit-overflow-scrolling: touch;</code> for iOS, you can also put <code>-ms-overflow-style: none !important;</code> right next to it. Here's a short StackOverflow discussion about the fix: <a href="http://stackoverflow.com/a/19918539">html5 - Div overflow scrolling when -ms-viewport is specified? - Stack Overflow</a></p>
<p>Happy overflowing!</p>
Take a breath2016-09-27T00:00:00-00:00https://annualbeta.com/blog/take-a-breath/<p>I have been building web things for more than ten years. I was there when <a href="https://en.wikipedia.org/wiki/Designing_with_Web_Standards">Zeldman told us</a> to drop <code><table></code> and start using this thing called "CSS". I don't actually feel <em>that</em> old (or wise) yet, but where wisdom remains elusive, I most certainly am developing a whole lot of grumpiness — and a very ambiguous love/hate relationship with the speed at which the web is growing.</p>
<h2>The web is great</h2>
<p>I love many things about working on the web as a frontend developer: The freedom of choice, the boundless opportunities for learning, the open-mindedness, the glorious selflessness and generosity of open source. There is something new every week, something great every month, and inspiring thinking is published not daily or hourly, but probably every milisecond.</p>
<p>On the other hand, it's this sheer mass, breadth and speed of evolution that threatens to leave me winded whenever I open Twitter or look at the massive amount of unread items in my feed reader (yes, some people still use those).</p>
<h2>A simple definition of fulfillment</h2>
<p>A recent trip to Bologna to the fifth and, sadly, last instalment of the <a href="https://2016.fromthefront.it/">From the Front</a> conference provided some much needed insights. One quote from <a href="https://twitter.com/lyzadanger">Lyza Danger Gardner's</a> talk "Everyone else is so clever" really resonated with me:</p>
<blockquote>
<p>"The trick is to seek out work that makes you feel both competent and challenged at the same time."</p>
<p><cite>Lyza Danger Gardner</cite></p>
</blockquote>
<p>It actually seems to be quite simple when you look at it this way. Stop worrying about what everybody else is doing; focus on the task at hand and figure out the best way to go about it. <em>My</em> best way might differ from yours, but that's okay. <em>My</em> best way is shaped so that it let's me feel competent, but also encourages me to try out something new if I think it appropriate.</p>
<h2>Programming sucks</h2>
<p>Two days ago I chanced upon an article on Mashable that I remember from when it was published in 2014 but had since forgotten. If you're a programmer and have never read <a href="http://mashable.com/2014/04/30/programming-sucks/">"Programming Isn't Manual Labor, But It Still Sucks"</a>, then do yourself a favor and read it. It will make you laugh and discharge some of the pressure you may be feeling.</p>
<p>Some key points are:</p>
<ul>
<li>All programming teams are constructed by and of crazy people</li>
<li>All code is bad</li>
<li>A lot of work is done on the Internet and the Internet is its own special hellscape</li>
</ul>
<h2>One big mess</h2>
<p>Coincidentally, <a href="https://twitter.com/ppk">ppk</a> published a great piece titled <a href="http://www.quirksmode.org/blog/archives/2016/09/web_development.html">"Web development as a hack of hacks"</a> today. The article consists of cherry-picked answers from a <a href="https://news.ycombinator.com/item?id=12477190">Hacker News thread</a> of the same name and ppk's opinionated commentary. I wholeheartedly subscribe to his perspective of the current state of frontend development.</p>
<p>It's a big mess right now, but it's also a great kind of mess. There's hope that the current burst of innovation in the frontend scene is indeed our Cambrian explosion that leads not to more or better frameworks, but to more and better standards.</p>
<h2>Focus</h2>
<p>For me, the key takeaways are to slow down once in a while and focus on fundamentals, not implementations; to brush up on <a href="https://addyosmani.com/resources/essentialjsdesignpatterns/book/">Javascript Design Patterns</a> and read up on <a href="https://rachelandrew.co.uk/archives/2016/09/13/why-there-is-no-css4-explaining-css-levels/">new CSS specifications</a> that have reached Candidate Recommendation.</p>
<p>And most importantly, to take a breath, look at the task before me and make a measured decision on how to get it built.</p>
Flexbox, Firefox and the button element2016-11-02T00:00:00-00:00https://annualbeta.com/blog/flexbox-firefox-and-the-button-element/<h2>Useful knowledge about flexbox</h2>
<p>When an element receives <code>display: flex;</code>, all children will be set to block-level. This is not an issue if the <code>flexbox</code> implementation is complete, but it's good to know about this little quirk.</p>
<h2>Beware of the <code><button></code> element</h2>
<p>Interestingly, Firefox has not implemented <code>flexbox</code> for <code><button></code> in the same way as Webkit or Blink – yet. In Mozilla's implementation, the <em>inline-block-like</em> display behavior of <code><button></code> cannot be changed via CSS <code>display</code>. Consequently, if you set a <code><button></code> to <code>display: flex;</code> to vertically center, say, button text and an icon, it will look great in Chrome, but broken in Firefox: text and icon wrap because the CSS renderer sees the <code>display: flex;</code> and sets all children to block-level, <em>even though</em> the button does not actually become a <code>flexbox</code> container.</p>
<p><strong>The good news:</strong> This is easily fixable by adding a <code><div></code> that wraps the items inside the button and applying the <code>flexbox</code> styles on it.</p>
<p>What's more, Mozilla has classified this behavior as a bug and is actively working on it (current status: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=984869">RESOLVED FIXED in Firefox 52</a>).</p>
A love letter to the CSS :not() pseudo-class2017-11-24T00:00:00-00:00https://annualbeta.com/blog/a-love-letter-to-the-css-not-pseudo-class/<p><strong>tl;dr:</strong> The use of <code>:not([class])</code> as an enhancement for <abbr title="Hypertext Markup Language">HTML</abbr> element selectors in <abbr title="Inverted Triangle CSS">ITCSS</abbr>' "elements" layer cancels the need for overriding rules in more specific layers.</p>
<p><img src="https://annualbeta.com/blog/a-love-letter-to-the-css-not-pseudo-class/bildschirmfoto-2017-11-25-um-18.14.01.png" alt="A screenshot of CSS code in an editor, showing selectors like h1:not([class])" title="The CSS pseudo-class in action. Please note: The screenshot shows compiled CSS for clarity; we actually make heavy use of SCSS features like mixins and nesting to keep project code DRY and maintainable."></p>
<p>When it comes to <abbr title="Cascadading Style Sheets">CSS</abbr> and code management, I have long since made the move to <abbr title="Block Element Modifier">BEM</abbr> and <abbr title="Inverted Triangle CSS">ITCSS</abbr> with a smattering of Functional CSS. If you haven't heard of one or any of those, have a look at the following articles (familiarity with ITCSS at least is sort of required for the rest of this post):</p>
<p><strong>BEM:</strong></p>
<ul>
<li><a href="https://en.bem.info/methodology/quick-start/">BEM Quick start</a></li>
<li><a href="https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/">MindBEMding – getting your head ’round BEM syntax</a></li>
<li><a href="http://www.didoo.net/to-bem-or-not-to-bem/">to bem or not to bem – a series of interviews on BEM methodology</a></li>
</ul>
<p><strong>ITCSS:</strong></p>
<ul>
<li><a href="https://www.hongkiat.com/blog/inverted-triangle-css-web-development/">Intro to ITCSS for Web Developers</a></li>
</ul>
<p><strong>Functional CSS:</strong></p>
<ul>
<li><a href="https://medium.com/@cole_peters/building-and-shipping-functional-css-4f29b947bcb9">Building and shipping functional CSS</a></li>
<li><a href="http://mrmrs.github.io/writing/2016/03/24/scalable-css/">CSS and Scalability</a></li>
</ul>
<p>Done? Splendid. Onwards!</p>
<p>As you (now) know, ITCSS was created by <a href="https://twitter.com/csswizardry">Harry Roberts</a> and is essentially a system that helps you to embrace the cascade (the "C" in CSS) and avoid writing convoluted selectors with ever increasing specificity (often referred to as "specificity hell" or "specificity wars") as projects and/or teams grow. ITCSS solves this by organizing your styles in layers from very generic to very specific. My typical setup for a <abbr title="Sassy CSS, a syntax for Snytactically Awesome Style Sheets (Sass)">SCSS</abbr>-based project follows Harry's layer suggestions quite closely:</p>
<ul>
<li><strong>Settings</strong> – Variables for breakpoints, colors, font sizes, …</li>
<li><strong>Tools</strong> - Mixins & functions</li>
<li><strong>Generic</strong> - Baseline rules like normalize/reset and box-sizing</li>
<li><strong>Elements</strong> - Baseline rules for HTML elements, a bit like normalize++</li>
<li><strong>Objects</strong> - Abstractions and patterns like media object, grids etc.</li>
<li><strong>Components</strong> - Discrete parts of the UI like buttons and and links, but also composites like accordions, main navigation, page header etc.</li>
<li><strong>Trumps</strong> - Very specific overrides, mostly functional helper classes for layout (margins, paddings)</li>
</ul>
<p>Among those, the "elements" layer is dedicated to styling pure HTML element selectors, while the code in the following layers uses classes exclusively (HTML element selectors should be avoided here to keep specificity low).</p>
<p>ITCSS helps me to write styles <em>with</em> the cascade, where I embrace the fact that rules of the same specificity in a lower layer – aka "later" in a concatenated production file - will overwrite earlier rules. However, I make an effort to use overwrites sparsely and avoid complexity (or, as many CSS developers have come to call it, "dark magic").</p>
<p>The <code>:not()</code> pseudo-class is the newest tool in my belt for this endeavor. Without it, I used to declare rules for properties like <code>font-size</code>, <code>line-height</code>, <code>color</code>, <code>font-weight</code> and especially <code>margin</code> in the "elements" layer (i.e. for headings or lists), only to reset them later in the "components" layer for BEM-elements like <code>news__title</code> or <code>accordion__header</code> that use an appropriate HTML element and not a <code><div></code>. This could be a heading, its level depending on the document outline (i.e. <code><h3></code>), but it would not necessarily <em>look like a third-level heading</em>.</p>
<p>"Then why do you style classless HTML elements in the first place?", you ask? Excellent question! Let me digress a little.</p>
<p>I mostly work with content-heavy websites that are powered by a content management system (CMS). Content editors (the people) usually work with so-called <abbr title="What You See Is What You Get">WYSIWYG</abbr> editors (the software) that support text formatting from <em>italic</em> and <strong>bold</strong> to inserting different types of headlines, lists, tables, links, etc. If you have worked with WYSIWYG editors before, you will know that the good ones produce well-formed, semantic HTML like <code><ul><li>…</li></ul></code> for unordered lists or <code><h2>…</h2></code> for sub-headings – HTML without classes.</p>
<p>Obviously, this WYSIWYG content needs to look just as right and correspond to the project's design guidelines as more "handcrafted" components like news teasers or <abbr title="Frequently Asked Questions">FAQ</abbr> accordions, where the content is directly retrieved from a database and marked up in a view template by a developer.</p>
<p>This poses a dilemma: It's necessary to style classless HTML elements so WYSIWYG content looks great, but at the same time I don't relish the thought of resetting <code>margin</code> or <code>list-style</code> for every non-content kind of <code><ul></code> I want to use in my templates (i.e. for navigation or a tabs component). In the past, I solved this by wrapping any WYSIWYG content in a <code><div class="wysiwyg">…</div></code> and used the selector to apply my styles <em>contextually</em>, like so:</p>
<pre class="language-scss"><code class="language-scss"><span class="token selector">.wysiwyg </span><span class="token punctuation">{</span><br> <span class="token selector">h2 </span><span class="token punctuation">{</span> … <span class="token punctuation">}</span><br> <span class="token selector">ul </span><span class="token punctuation">{</span> … <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Note: This kind of rule nesting is a feature provided by SCSS.</span></code></pre>
<p>The above is a perfectly valid method, but it never felt <em>right</em>. What if I have a design for lists or headings that applies to a majority of all lists or headings with only a few exceptions? The contextual approach requires me to create an additional class with the same styles I used for the <code>.wysiwyg</code>-selector that can be applied to handcrafted components like <code>news__title</code>. Again, this is a perfectly valid technique which can be made more maintainable by including a Sass mixin in both use cases (which in turn negates the necessity of a specific class). And still I believed there had to be an easier way that required less code. And there is!</p>
<h2>Enter the <code>:not()</code> pseudo-class</h2>
<p>From <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:not">MDN web docs</a>:</p>
<blockquote>
<p>The <code>:not()</code> CSS pseudo-class represents elements that do not match a list of selectors. Since it prevents specific items from being selected, it is known as the <em>negation pseudo-class</em>.</p>
</blockquote>
<p>I do not know why it took me so long to realize this, but I can specifically target HTML elements without a class by simply applying <code>:not([class])</code> to them.</p>
<p>Here's a simple example of how this looks in a current project:</p>
<pre class="language-scss"><code class="language-scss"><span class="token property">h2</span><span class="token punctuation">:</span><span class="token function">not</span><span class="token punctuation">(</span>[class]<span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token property">color</span><span class="token punctuation">:</span> #555555<span class="token punctuation">;</span><br> <span class="token property">font-size</span><span class="token punctuation">:</span> 24px<span class="token punctuation">;</span><br> <span class="token property">font-weight</span><span class="token punctuation">:</span> 300<span class="token punctuation">;</span><br> <span class="token property">line-height</span><span class="token punctuation">:</span> 1.2<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>And a more complex one:</p>
<pre class="language-scss"><code class="language-scss"><span class="token comment">// Due to design and implementation choices, I can reset all my</span><br><span class="token comment">// unordered lists to a common baseline</span><br><span class="token selector">ul </span><span class="token punctuation">{</span><br> <span class="token property">list-style</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br> <span class="token property">margin-top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br> <span class="token property">margin-bottom</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br> <span class="token property">margin-left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br> <span class="token property">padding-left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// All classless unordered lists have an orange bullet and</span><br><span class="token comment">// are slightly spaced (by applying a margin-top to</span><br><span class="token comment">// consecutive list items)</span><br><span class="token property">ul</span><span class="token punctuation">:</span><span class="token function">not</span><span class="token punctuation">(</span>[class]<span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token selector">li </span><span class="token punctuation">{</span><br> <span class="token property">line-height</span><span class="token punctuation">:</span> 1.375<span class="token punctuation">;</span><br> <span class="token property">padding-left</span><span class="token punctuation">:</span> 1.15em<span class="token punctuation">;</span><br> <span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span><br><br> <span class="token selector"><span class="token parent important">&</span>::before </span><span class="token punctuation">{</span><br> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">'•'</span><span class="token punctuation">;</span><br> <span class="token property">color</span><span class="token punctuation">:</span> #ff8c00<span class="token punctuation">;</span><br> <span class="token property">font-size</span><span class="token punctuation">:</span> 1.5em<span class="token punctuation">;</span><br> <span class="token property">line-height</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span><br> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br> <span class="token property">top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br> <span class="token property">left</span><span class="token punctuation">:</span> .1em<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br><br> <span class="token selector">+ li </span><span class="token punctuation">{</span><br> <span class="token property">margin-top</span><span class="token punctuation">:</span> 5px<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>Suddenly, all my problems vanish into thin air:</p>
<ul>
<li>I can style WYSIWYG content without a contextual class</li>
<li>I do not have to overwrite any unwanted rules for my handcrafted components with classes</li>
<li>I can benefit from my generic rules for HTML elements whenever I want simply by <em>omitting</em> to apply a class to them</li>
</ul>
<p>I have used this approach for the better part of three months now and not encountered any downsides yet.</p>
<p>What do you think?</p>
The AI Revolution: The Road to Superintelligence2018-02-01T00:00:00-00:00https://annualbeta.com/bookmarks/the-ai-revolution-the-road-to-superintelligence/<p>Tim Urban's famous primer on all things artificial intelligence, probable societal upheaval and the possibility of humanity's impending demise.</p>
Legends of the Ancient Web2018-02-06T00:00:00-00:00https://annualbeta.com/bookmarks/legends-of-the-ancient-web/<p>Maciej Cegłowski likens the history of the internet to that of radio and points to episodes of radio's past that we should avoid repeating with the web.</p>
China's Surveillance State Should Scare Everyone2018-02-08T00:00:00-00:00https://annualbeta.com/bookmarks/chinas-surveillance-state-should-scare-everyone/<p>Imagine a society in which your government keeps a score about your behaviour both online and offline. Then consider that China is not only imagining this, they're nearly there.</p>
Everything Easy is Hard Again2018-02-10T00:00:00-00:00https://annualbeta.com/bookmarks/everything-easy-is-hard-again/<p>Frank Chimero contemplates the state of web (frontend) development and why it baffles him.</p>
Inside the Two Years That Shook Facebook—and the World2018-02-14T00:00:00-00:00https://annualbeta.com/bookmarks/inside-the-two-years-that-shook-facebookand-the-wor/<p>How a confused, defensive social media giant steered itself into a disaster, and how Mark Zuckerberg is trying to fix it all.</p>
Finding the Exhaust Ports2018-02-15T00:00:00-00:00https://annualbeta.com/bookmarks/finding-the-exhaust-ports/<p>Jon Gold tries to make sense of his dwindling techno-optimism in the messy reality we got instead of the utopia we were hoping for.</p>
On Weaponised Design2018-02-17T00:00:00-00:00https://annualbeta.com/bookmarks/on-weaponised-design/<p>Tactical Tech is researching how well-intended but ill-considered design choices lead to hurtful outcomes.</p>
Design’s Lost Generation2018-02-20T00:00:00-00:00https://annualbeta.com/bookmarks/designs-lost-generation/<p>Mike Monteiro asks designers to aim for a professional level of accountability, because <em>"Amateur hour is over."</em> and <em>"We moved too fast and broke too many things."</em> This is an excellent follow-up read to <a href="https://ourdataourselves.tacticaltech.org/posts/30-on-weaponised-design/">On Weaponised Design</a>.</p>
Weeknote #12019-03-31T00:00:00-00:00https://annualbeta.com/blog/weeknote-1/<p>Weeknotes seem to be a thing currently. <a href="https://www.dertagundich.de/kategorie/allwoechentlich-belangloses/">Martin</a> has been doing it for ages, and I've always been jealous of his discipline. Now <a href="https://paulrobertlloyd.com/2019/03/weeknotes_11">Paul</a>, <a href="https://daverupert.com/2019/02/weeknotes-3/">Dave</a>, <a href="https://www.baldurbjarnason.com/2019/03/18/weeknote-1/">Baldur</a> and probably many others have started and I thoroughly enjoy reading them. I started to wonder (not for the first time) if I can also make this a regular thing. Part of my Sunday routine, maybe.</p>
<p>So, without further ado: The last week was too short, a bit on the stressful side – but also not too bad. I predict that this will be true for a lot of weeks.</p>
<h2>Work</h2>
<p>With our two main project coordinators on vacation and sick leave respectively, I felt the need to assume a lot of their duties on top of mine. That includes taking over the bi-weekly phone calls with our focus project's client as well as triaging all the incoming customer requests via our issue-tracking-system. Turns out this is a <em>lot</em> of work and I'm glad that I usually don't notice any of it. I also got to fix a few minor bugs, which felt good. Apart from them being there in the first place, of course.</p>
<p>We also hosted eight bright and enthusiastic girls on Thursday for this year's "<a href="https://www.girls-day.de/Daten-Fakten/Das-ist-der-Girls-Day/Ein-Zukunftstag-fuer-Maedchen/english">Girls'Day</a>", a nationwide activity where "technical" companies offer a full day of looking behind the scenes to girls of 10 years upwards. <a href="https://www.webfactory.de/ueber-uns/#lukas">Lukas</a> and I explained a bit about web development and then quickly let them loose on the excellent <a href="https://code.org/hourofcode/overview">Hour of Code activities from code.org</a>. The biggest part of the day was dedicated to free-form projects, where the girls went through ideation, conceptualisation, storyboarding, logo design all the way to early HTML prototypes. The list of projects included</p>
<ul>
<li>an e-commerce store that sells everything (apart from illegal stuff!)</li>
<li>a variation of the hangman game</li>
<li>a website for an animal shelter with little CVs for each animal looking for adoption</li>
<li>a restaurant finder for healthy fast food with intricate filter mechanisms</li>
<li>an adventure game situated in an aquatic world where you solve quests to gain new outfits</li>
</ul>
<p>At the end of the afternoon, the girls made us very happy when they concluded that they would really like to do this every day – even after we explained that we usually don't have as much chocolate nor order pizza for lunch.</p>
<h2>Reading</h2>
<p>I am pretty sure that reading is the secret of my mental resilience. It helps me to switch off completely from whatever is going on, so it was quite awesome that I had plenty to devour this week. I am currently on a young adult (YA) fantasy fiction streak, mostly because I enjoy reading everything <a href="https://brandonsanderson.com/">Brandon Sanderson</a> puts to paper and I have read every book except his YA endeavours. So, yeah. I finished "Skyward" (which was fantastic!) on Friday last week, gave myself a 48-hour-lockout and started into "Steelheart", the first installment of his "Reckoners" trilogy on Monday. By now, I'm almost finished with the second book ("Firefight") and a bit sad that there's only one more to go.</p>
<p>As usual, I accompany my fiction reading with plenty of non-fiction, mostly from around the internets. Here is a selection of articles that I believe are worth remembering:</p>
<p>Malte Ubl explained how his team <a href="https://2019.jsconf.eu/news/how-we-built-the-fastest-conference-website-in-the-world/">built the fastest conference website in the world</a>. Even though AMP plays a part (I'm not sold on AMP), there are a lot of things that apply to any project. I enjoyed reading about the decisions they made and the trade-offs they considered worthwile.</p>
<p><a href="https://jakearchibald.com/2019/f1-perf/">Jake Archibald reviewed the performance of formula one websites</a>. It's a hilarious read as well as a good primer on performance reviews. What stuck with me the most was his epiphany that "[…] none of the teams used any of the big modern frameworks. They're mostly Wordpress & Drupal, with a lot of jQuery. It makes me feel like I've been in a bubble in terms of the technologies that make up the bulk of the web." Thank you, Jake. Yes, there are a lot of people building with old, tested and boring tech. We're here, and we're many!</p>
<p>A few other people have published similar thoughts this week. <a href="https://www.baldurbjarnason.com/2019/03/24/weeknote-2---web-development-mistakes-mary-sues-and-icy-spring/">Baldur contemplated The Ease of Big Mistakes</a> in his second weeknote, warning that "Once you have a build system and your basic web application scaffolding up, huge, project-crippling mistakes come almost naturally [note: if you're not paying attention while building your client-side web application]". Chris Coyier wrote a long, art-directed column called "<a href="https://css-tricks.com/simple-boring/">Simple & Boring</a>" and linked to more great posts about this topic which I won't repeat here.</p>
<p>I also stumbled on a few great fringe articles. Nellie Bowles wrote a piece for the New York Times about how <a href="https://www.nytimes.com/2019/03/23/sunday-review/human-contact-luxury-screens.html">Human Contact Is Now A Luxury Good</a>. She points out that "In Silicon Valley, time on screens is increasingly seen as unhealthy. Here, the popular elementary school is the local Waldorf School, which promises a back-to-nature, nearly screen-free education. So as wealthy kids are growing up with less screen time, poor kids are growing up with more. How comfortable someone is with human engagement could become a new class marker." I have no idea how this relates to Germany, but it got me thinking.</p>
<p>John Harris presents a bleak opinion on the web's future in The Guardian's <a href="https://www.theguardian.com/commentisfree/2019/mar/25/cold-war-digital-china-facebook-mark-zuckerberg">The global battle for the internet is just starting</a>. He warns that while we may currently raise an eyebrow at China's overt surveillance and social ranking systems, there are signs that the western giants (namely Google and Facebook) are going in the exact same direction.</p>
<p>Last but not least, there's A. Jesse Jiryu Davis on OneZero with a report on <a href="https://onezero.medium.com/ctrl-alt-delete-the-planned-obsolescence-of-old-coders-9c5f440ee68">The obsolescence of old coders</a>. This hits a bit too close to home. At 37, I am part of <em>the old guard</em> whether I want to or not. I have spent quite a bit of time wondering whether I will still be building websites at 40. Or 50. Will "simple & boring" tech still measure up then? Will I be able to keep up, if not? So much to think about.</p>
<h2>Miscellanea</h2>
<p>I went to the cinema to watch <a href="https://www.nationalgeographic.com/films/free-solo/">Free Solo</a> on Sunday. Such a great movie! Even though it was filmed in 2016/2017 and I knew the outcome, the last third of the movie (the actual climb) was exceptionally thrilling. Highly recommended!</p>
<p>One of our skylights spontaneously sprung a leak on Monday and it started raining on the couch. Only it wasn't spontaneous at all, considering the time it must have taken for the frame to rot through this far with slow seepage only. The roofers found that all skylights across the building share the same fate and need to be replaced. For now, ours is sealed with a red plastic sheet and duct tape. A good time to be renting, I guess.</p>
<p>This concludes week #1. Looking back, I'm not sure I'll be able to be this elaborate every week. We'll see.</p>
Weeknote #22019-04-07T00:00:00-00:00https://annualbeta.com/blog/weeknote-2/<p>Some week. I ranked it at 2.5 out of 5 stars in our weekly team retro, which is pretty low for me. <a href="https://www.webfactory.de/blog/unser-buerohund-experiment">Our dog</a> is not having a good time. His heart condition (three degenerated and leaky valves) seems to be worsening which is why we scheduled an electrocardiogram for tomorrow. He also started limping heavily on Wednesday; we waited a bit, hoping he had only bumped his leg against something, but it didn't get better. The vet diagnosed an acute bout of arthritis which we've now added to the-list-of-longterm-diseases-to-manage. Poor little fur toad.</p>
<p>In other news, we bought a car yesterday. I found an offer for a black 2015 Seat Leon online on Monday and liked it at first glance. We went to have a look-see and drive-around, found nothing amiss and signed a pre-purchase contract on the spot. This is a <em>dramatic</em> improvement from 2014, when I fretted over car offers for weeks and spent three sleepless nights doubting myself before finally buying the VW Golf IV we had reserved. I actually enjoyed the process this time – maybe I'm growing up after all.</p>
<h2>Work</h2>
<p>I reverted to my usual front-end duties with only a bit of hand-over support for our returning project coordinator. The week was still stressful; there's the infinite backlog to consider, with too many paused tasks lurking in the shadow of the "more important" ones. I need to get back on top of my Kanban game. I'm looking at you, next week!</p>
<p>Speaking of which, I also have to face up to a promise past me made, and deliver at least a summary for a talk I'm supposed to give in …18 days? Where has the time gone?!</p>
<p>Here's two code things I enjoyed: I improved on the web components fallback after switching <a href="https://www.g-ba.de/">www.g-ba.de</a> from <a href="https://symfony.com/doc/current/templating/hinclude.html">Symfony's documented recipe for client-side inclusion</a> to a custom one using <a href="https://github.com/gustafnk/h-include">h-include</a>. The reason for the switch was that I need valid HTML to pass WCAG 2.1 and the documented solution with its <code><hx:include></code> syntax doesn't pass (but web components do!).</p>
<p>The other thing? I published our current <a href="https://www.npmjs.com/package/webfactory-disclosure">disclosure</a> (read more/read less) boilerplate script to npm. It's rough, it's not well documented, it's untested – but it's there. Naturally, <a href="https://andy-bell.design/wrote/a-progressive-disclosure-component/">a great write-up on accessible disclosure patterns</a> came out just this week, so I'm looking forward to improving our script in the near future.</p>
<h2>Reading</h2>
<p>Huh. I didn't read as much this week. "Firefight" is finished and "Calamity" (Reckoners #3) sits at 60%. I might finish it tonight, we'll see.</p>
<p>On the other hand, the web was a gold mine for excellent articles and thought pieces this week:</p>
<p>Jason Hoffman (famous for his deep dives into the <a href="https://thehistoryoftheweb.com/">History of the Web</a>) let loose with his <a href="https://css-tricks.com/yet-another-javascript-framework/">Yet Another JavaScript Framework</a>, an absolutely fascinating insight into the history of Javascript frameworks, decisions made in the late 2000s and their repercussions for the present.</p>
<p><a href="https://alistapart.com/article/responsible-javascript-part-1">Responsible JavaScript: Part 1</a> by Jeremy Wagner is a thoughtful examination of how Javascript <em>could/should</em> be used contrasted with how it is actually used. It follows a bit in the "simple & boring" vein of last week, but it is by no means a condemnation of Javascript. Same theme, albeit a bit on the rant-y side: Owen Williams' <a href="https://char.gd/blog/2019/you-dont-need-that-hipster-web-framework">You probably don't need that hip web framework</a>. But hey, <em>I</em> liked it and this is my blog, so I can link to it.</p>
<p>Josh Bader showcases how we can <a href="https://css-tricks.com/css-variables-calc-rgb-enforcing-high-contrast-colors/">use CSS Variables + calc() + rgb() to enforce high contrast colors</a>. I love this kind of mash-up thinking. It's great to see what is possible using standard CSS features with only a slight dose of Javascript.</p>
<p>I was nodding rigorously while reading <a href="https://www.theguardian.com/info/2019/apr/04/revisiting-the-rendering-tier">Revisiting the rendering tier</a>, an excellent post by the team at The Guardian about the challenges every (larger) project faces with their CSS codebase as well as their past and current approaches to solve them. Spoiler: they make an excellent argument for server-side rendered CSS-in-JS components.</p>
<p><strong>tl;dr:</strong> If you read only one article this weekend, make it this one. ☝️</p>
Weeknote #32019-04-14T00:00:00-00:00https://annualbeta.com/blog/weeknote-3/<p>Whoa. Weeknote number three – I seem to be on a streak here, although I still have doubts about its longevity.</p>
<p>This week was <em>turbulent</em>. It started out great with a visit at the vet's where Obi-Wau was diagnosed with "no change" in his heart condition, meaning: no further deterioration of his heart valves in the last couple of months. Awesome! He's obviously still sick but at least the meds seem to have stabilized the status quo. We spent a lot of time playing outside and running off-leash to celebrate. 🐶🎉</p>
<p>A warm and sunny spring has come, left and returned this week. <a href="https://www.jourmany.de/reiseziele/kirschbluete-bonn.html">Bonn's cherry blossom</a> is in full bloom by now and the annual maelstrom of tourists waltzes through the narrow Altstadt alleys. Temperatures dropped to 0°C yesterday and we even had a bit of snow, which probably means that the blossom will not be very long-lived this year.</p>
<p>Even though most of my time and energy was consumed by work, I found a bit of time to improve the partners display on <a href="https://www.black-cab-cologne.de/">Black Cab Cologne</a>. I really want to get back to tinkering with <a href="https://getkirby.com/">Kirby 3.0</a> for my complete, multi-language rewrite of <a href="https://www.jourmany.de/">Jourmany</a>. Alas, it's mostly content from here on out and there's a lot of that. 😬</p>
<h2>Work</h2>
<p>My work week was dominated by our big focus project, to the point where I cancelled my attendance at the Chamber of Commerce's examining board for trainees. We've felt under pressure from our client for weeks to forecast and commit to a launch deadline, which was supposed to be next Tuesday. I spent most of my available time and effort on closing two pull requests (the last outstanding features), while the rest of the team was focused on preparing content migration workflows and creating launch checklists. We really doubled down on this project, updated our client contact in daily phone calls on Tuesday, Wednesday and Thursday and reaffirmed the launch deadline on Thursday. Then Friday came along and our contact's boss called to inform us they had to postpone the launch due to missing internal approvals. Apparently, there is a two-tiered approval process in place that no one was consciously aware of, as well as a spontaneous need for some content restructuring and subsequent new approvals.</p>
<p>On the upside, I did fix a few things to keep me happy <strong>and</strong> carved out some time for <a href="https://dermeier.github.io/portfolio-website">Lukas</a> to assist him with an accessible form he's building for the <a href="https://www.solidaritaetskorps.de/">European Solidarity Corps</a>.</p>
<p>TIL: Do not think that just because modern browsers support CSS flexbox for media type <code>screen</code> they will support it for <code>print</code>. <em>Heavy sigh.</em> I totally ran into this with both eyes open, having read <a href="https://www.smashingmagazine.com/2018/05/print-stylesheets-in-2018/#browser-support">Rachel's warning</a>. Go me.</p>
<h2>Reading</h2>
<p>I feel like I've read too much this week, especially in preparation for my upcoming UX talk on passwords. Now my mind is broke. Most of my reading was of the technical-articles-on-screen type, but I did also finish <a href="https://www.goodreads.com/book/show/15704486-calamity">"Calamity" [Reckoners #3]</a>. The very satisfying ending makes for a great trilogy. Speaking of trilogies, <a href="https://www.goodreads.com/book/show/38099642-holy-sister">"Holy Sister" [Book of the Ancestor #3]</a> came out and I'm about a hundred pages in. I'm taking this one slow because I want to enjoy it. And I'm reading it on paper!</p>
<p>Here's an incomplete and unsorted list of web things I read:</p>
<p><a href="https://alistapart.com/article/nothing-fails-like-success/">Nothing Fails Like Success</a> by Jeffrey Zeldman is a must-read. He compares the current state of venture capital backed companies to a family that loaned from the Mob and now has to pay the price. Mike Davidson (former VP of Design at Twitter) responded with a longish <a href="https://twitter.com/mikeindustries/status/1117177717575667713">twitter thread</a>.</p>
<p><a href="https://css-tricks.com/accessibility-events/">Accessibility Events</a> by Mat Marquis is one of many great pieces that tackle the toxic new "feature" that is Apple's accessibility events. Let us not repeat our past mistakes and say "no" to this.</p>
<p>Then there was Unlike Kinds who actually suffer from <a href="https://unlikekinds.com/article/google-amp-page-speed"><em>lower</em> page speed since they transitioned to Google's AMP</a>. Their post is especially great if you – like me – are very sceptic of AMP and the intentions behind it.</p>
<p>Addy Osmani published <a href="https://addyosmani.com/blog/lazy-loading/">Native image lazy-loading for the web!</a>, a superb resource for the up-and-coming <code>loading</code> attribute which brings native <code><img></code> and <code><iframe></code> lazy-loading to the web. Rejoice! I can't wait to add this to our <a href="https://github.com/webfactory/WebfactoryResponsiveImageBundle">WebfactoryResponsiveImageBundle</a>.</p>
<p>If you need a reminder why you might only be <em>temporarily</em> abled, <a href="https://alistapart.com/article/accessibility-for-vestibular/">Accessibility for Vestibular Disorders: How My Temporary Disability Changed My Perspective</a> is for you. I read it and although I knew most of the points Facundo Corradini makes, I still felt I knew them a bit better afterwards.</p>
<p>I probably shouldn't have squeezed it in this week, but I also read the <a href="https://css-tricks.com/an-introduction-to-web-components/">five-part series on Web Components</a> on CSS-Tricks. Good stuff! I really need to find some time to dig into this exciting not-really-new-anymore part of modern frontend choices.</p>
<p>Also on CSS-Tricks: Chris' musings on <a href="https://css-tricks.com/decaying-sites/">the fact that even websites age</a>. This ties in to a comment on Dave Rupert's <a href="https://daverupert.com/2019/04/some-unsolicited-blogging-advice/">unsolicited blogging advice</a> where Evan Travers explained how he uses a changelog at the bottom of his <a href="http://evantravers.com/articles/2019/04/03/my-keyboard-setup/#references">posts</a> to make edits and updates transparent. This is really neat! I want to do that and it should be possible, somehow, as each post here lives in a markdown-ish <code>.txt</code> file that is already under version control.</p>
<p>I also saved about 20-ish new bookmarks to my "security" folder during yesterday's research for my talk about the UX of passwords. More on that soon.</p>
<p>Did I mention my mind is broke?</p>
On Tinkering2019-08-31T00:00:00-00:00https://annualbeta.com/blog/on-tinkering/<p>I love tinkering with my side projects. Like tree-rings, my different technological choices show what interested me at the time, what I thought would fit the projects needs best or what I felt comfortable working with.</p>
<h2>A static journey</h2>
<p>Take <a href="https://www.jourmany.de/">Jourmany</a>, for example: developed in 2015 and launched in May 2016, this project marked my first foray into static site generators (SSG). I absolutely loved the idea of SSGs! I could build a complete website and totally get away without knowing web servers, server-side languages or databases. This left me free to focus on what I love: details and performance. I tried to make Jourmany the fastest I could make it. Web fonts loading with <a href="https://www.zachleat.com/web/comprehensive-webfonts/#fout-class">FOUT and class</a>. Responsive images, lazy-loaded. GZIP, http2, Resource Hints. I was slightly hampered by my hosting provider, but <a href="https://developers.google.com/speed/pagespeed/insights/?hl=de&url=https%3A%2F%2Fwww.jourmany.de%2Freiseziele%2Fsiebengebirge.html">Lighthouse is still looking okay</a> three years later. I was also able to look into details like adjusting the hero headline's size according to character count (shorter destinations are displayed in bigger type, compare <a href="https://www.jourmany.de/reiseziele/sylt.html">Sylt</a> and <a href="https://www.jourmany.de/reiseziele/elbsandsteingebirge.html">Elbe Sandstone Mountains</a>), or loading Youtube scripts and videos on user interaction only (this not only helps with performance, it's also more privacy conscious).</p>
<p>I chose <a href="https://gruntjs.com/">Grunt</a> and <a href="https://annualbeta.com/blog/on-tinkering/assemble.io">Assemble</a> as my "tech stack" at the time – both of which are not quite dead yet, but probably a lot more niche today. It all worked well; my only gripe was that creating new content (destinations) was a cumbersome process, because I had made quite a mess of the way my JSON data files and handlebars templates worked together. I began to wish for a bit more comfort.</p>
<p>A few months later, I came across <a href="https://getkirby.com/">Kirby</a>, a PHP-based static file CMS. It seemed a great fit for my static needs: still no database overhead, but easier content creation via an admin panel. I chose this blog as my test case and built the site with the Kirby Starterkit. The "design", if you can call it that, was implemented with pre-fabricated patterns from <a href="http://tachyons.io/">http://tachyons.io/</a>, one of many emergent functional CSS libraries. I really liked Kirby, and have used it since then to build a <a href="https://www.black-cab-cologne.de/">business website</a> for a friend – and a complete (not yet launched) rewrite of Jourmany. I'm a lot less certain about doing <em>everything</em> with functional CSS, but it was a fun experiment.</p>
<h2>The new shiny: Eleventy</h2>
<p>Come 2018, a new shiny thing started making the rounds on Twitter and the blogs I frequent: <a href="https://11ty.io/">Eleventy</a>, "a simpler static site generator". What can I say? I looked at the <a href="https://www.11ty.io/docs/">docs</a>, I read blog posts about it. I got excited. It still took a good, long while (and a vacation), but I'm happy to inform you: annualbeta is now powered by Eleventy and (continuously) deployed on <a href="https://www.netlify.com/">Netlify</a>. 🎉</p>
<p>I had a lot of fun making this version of the website. Eleventy is almost two years old by now, and documentation, features and plugins are quite mature. I chose <a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> as my templating engine, because its syntax is very similar to <a href="https://twig.symfony.com/">Twig</a>, which we use at <a href="https://www.webfactory.de/">webfactory</a> for all our Symfony projects. The most tedious work was to convert all my content from markdown-ish <code>.txt</code> files sprinkled with <a href="https://getkirby.com/docs/reference/text/kirbytags">KirbyText</a>, HTML and tachyons classes to pure markdown.</p>
<h2>Joining the Indieweb</h2>
<p>The <a href="https://indieweb.org/">Indieweb</a> movement has been gaining a lot of momentum this year (in my filter bubble). I guess I've always been part of it since I've "owned" my blog content since the beginning, but <a href="https://beyondtellerrand.com/events/dusseldorf-2019/speakers/tantek-celik">Tantek Çelik's talk</a> at this year's beyond tellerand (Dusseldorf edition) inspired me to do more. I wanted to support what he called "interactions between websites", something the Indieweb made possible via the creation – and <a href="https://www.w3.org/TR/webmention/">standardization</a>! – of <a href="https://indieweb.org/Webmention">Webmention</a>.</p>
<blockquote>
<p><strong>Your content is yours</strong><br>
When you post something on the web, it should belong to you, not a <a href="https://indieweb.org/silo">corporation</a>. Too many companies have gone out of business and <a href="https://indieweb.org/site-deaths">lost all of their users’ data</a>. By joining the IndieWeb, your content stays yours and in your control.</p>
<p><cite><a href="https://indieweb.org/">Indieweb.org</a></cite></p>
</blockquote>
<p>Because Eleventy uses plain Javascript under the hood, I could <s>write my own</s> use and build upon other people's plugins and filters. One resource I kept going back to was Max Böck's <a href="https://mxb.dev/blog/using-webmentions-on-static-sites/">Static Indieweb pt2: Using Webmentions</a>. He has open-sourced his Eleventy implementation of Webmention and kindly documented how to use it. For now, <a href="#webmentions">my implementation</a> of Webmentions is a basic syndication of Twitter likes, mentions and reposts, but I'll probably expand it soon.</p>
<p>While I was poking around open-source Eleventy projects, I also copied and modified the super practical <a href="https://github.com/andybelldesign/hylia/blob/master/src/transforms/parse-transform.js">parse transforms</a> I found in Andy Bell's <a href="https://hylia.website/">Hylia Starterkit</a>. Thanks to him, my images are now wrapped in <code><figure></code> tags with <code><figcaption></code> for captions , and blog post subheadings have anchors so it's possible to link to specific sections. 👍</p>
<h2>Tinkering is learning</h2>
<p>I love discovering new perspectives and learning new tricks from other people. Tinkering with my own little projects is how I can do that without pressure or fear of responsiblity. The only real constraint I set for myself is that any existing <a href="https://www.w3.org/Provider/Style/URI">URLs don't change</a> (I have actually broken my feed URL, but I know all of my 3 followers personally, so I'm okay).</p>
<p>Turning everything on its head has been great. I have already learned so much stuff! And there's still a ton of things on my wishlist:</p>
<ul>
<li>I want to implement responsive images with different sizes and webp support and lazy-load them, <a href="https://web.dev/native-lazy-loading">2019-style</a>. 📷</li>
<li>I want to inline my static assets because they're so small (CSS: 4.6kb, JS: 3.3kb) that it'll probably make sense. ⚡️</li>
<li>I want to try Remy Sharp's solution for <a href="https://remysharp.com/2019/06/18/send-outgoing-webmentions">sending Webmentions to other websites</a> when I link to them (my current implementation is still only a one-way street). 🔄</li>
<li>I want to experiment with Zach Leatherman's <a href="https://www.zachleat.com/web/featherweight-facepile/">avatar local cache</a>. Did I mention he's the guy behind Eleventy? Because he is. 🙏</li>
<li>I want to find a nice web font and load it extremely efficiently, maybe fiddle around with subsetting (at the time of writing, I use system fonts). 🔤</li>
<li>I want to fetch commits per page and display a changelog on my blog posts (in case I ever update them). 📋</li>
<li>I want to return to Max Böck and <s>steal</s> copy his ingenious <a href="https://mxb.dev/blog/indieweb-link-sharing/">sharing technique</a>. 😎</li>
<li>I want to add a Serviceworker to this site, probably with <a href="https://okitavera.me/article/turn-your-eleventy-into-offline-first-pwa/">Nanda Oktavera's eleventy-plugin-pwa</a> because she has already done the hard work. 👏</li>
</ul>
<p>Phew. I guess I'll never be done! But this is exactly what I love about working on the web: everything is constantly evolving and changing (hopefully for the better).</p>
A changelog for my blog posts2019-09-04T00:00:00-00:00https://annualbeta.com/blog/a-changelog-for-my-blog-posts/<p>The idea to append a list of updates and errata (in the form of commit messages) to my blog posts has been bouncing around my head for a few months now. It started when I saw <a href="https://twitter.com/evantravers">Evan Travers'</a> comment to Dave Rupert's <a href="https://daverupert.com/2019/04/some-unsolicited-blogging-advice/">Some unsolicited blogging advice</a>:</p>
<blockquote>
<p><span class="u-text-smaller u-text-muted">[…]</span> I've been thinking a lot about how we could reflect a changelog on our blogposts, just to give us permission to adjust and post while "not totally ready".</p>
<p>I'm doing a really simple <code>git log</code> at the bottom of my static site pages for this. It's given me a lot of freedom to get posts out of my perfection clog that was keeping everything in drafts.</p>
<p><cite>Evan Travers</cite></p>
</blockquote>
<p>I strongly agree with the sentiment (my drafts folder is full of unfinished, not-deemed-perfect-enough ideas). Plus, this looked like a nice project to dig into, especially now that I'm running Eleventy and feel somewhat comfortable poking around the engine. All posts are created from markdown files under version control, so it should be possible.</p>
<p>My plan was deceptively simple:</p>
<ol>
<li>Grab a <code>git log</code> and transform it to JSON (so I can consume it easily with Javascript).</li>
<li>Write an Eleventy filter that checks every page's source URL for a match with my commits-as-JSON (I just learned this technique when I implemented webmentions).</li>
<li>Output a list of commits containing the subject line and a timestamp.</li>
</ol>
<p>As all plans do, this one quickly hit a few snags when it met reality.</p>
<h2>Git log to JSON</h2>
<p>So I wanted to get my commit history as JSON. At first I tried to find if I could use someone else's work. A Google search returned a few npm packages that claimed to convert a git log to JSON, but it turned out my requirements were a bit, shall we say, off the beaten path. None of them included references of the changed files which I needed to map commits to blog posts.</p>
<p>The good news: I learned that you can actually use git's in-built <code>--pretty=format:<string></code> to format <code>git log</code> into JSON. There is a list of placeholders that allow you to specify which information you want to log (all explained in the <a href="https://git-scm.com/docs/pretty-formats">official git docs</a>) as well as ones that expand to single literal characters, like <code>%n</code> for newline. So you can do this:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> log -1 --pretty<span class="token operator">=</span>format:<span class="token string">'{%n "commit_hash": "%H",%n "date": "%aD",%n "subject": "%s",%n "author": "%aN" %n}'</span></code></pre>
<p>and get nice, clean JSON:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br> <span class="token property">"commit_hash"</span><span class="token operator">:</span> <span class="token string">"3fe019607382e28a39117b8fda5e436beb3afb5f"</span><span class="token punctuation">,</span><br> <span class="token property">"date"</span><span class="token operator">:</span> <span class="token string">"Tue, 3 Sep 2019 20:20:11 +0200"</span><span class="token punctuation">,</span><br> <span class="token property">"subject"</span><span class="token operator">:</span> <span class="token string">"Add changelog to blog posts"</span><span class="token punctuation">,</span><br> <span class="token property">"author"</span><span class="token operator">:</span> <span class="token string">"Søren Birkemeyer"</span><br><span class="token punctuation">}</span></code></pre>
<p>However, it turns out there is <strong>no placeholder for files</strong>. D'uh. I returned to Google and found <a href="https://git-scm.com/docs/git-log#Documentation/git-log.txt---name-only">docs</a> for <code>git log --name-only</code> and <code>git log --name-status</code>, the latter being my saviour. It outputs a commit with a list of files and status changes. Here's the same commit from before returned by <code>git log -1 --name-status</code>:</p>
<pre class="language-git"><code class="language-git">Author: Søren Birkemeyer <author@mailhost.com><br>Date: Tue Sep 3 20:20:11 2019 +0200<br><br> Add changelog to blog posts<br><br> Display a list of commit messages (subject lines only) at the bottom of<br> blog posts if the commit(s) modified the source markdown file.<br><br>M .eleventy.js<br>M .gitignore<br>A _cache/gitlog.json<br>A git-log-merge.sh<br>A git-log-namestatus.sh<br>A git-log-pretty.sh<br>M gulpfile.js<br>M src/_assets/scss/06_trumps/_text.scss<br>A src/_data/commits.js<br>M src/_includes/layouts/blogpost.njk<br>A src/_includes/snippets/changelog.njk</code></pre>
<p>If you look closely, you may be able to see where this is heading. When I searched for "git log --name-status to json" I struck gold with a <a href="https://gist.github.com/textarcana/1306223">Gist by Noah Sussman</a>. Apparently, he had been working on the same problem and solved it with shell scripts and some Perl voodoo I only half understand – which didn't matter, because the result was exactly what I needed. 🎉</p>
<h2>Perl in a shell</h2>
<p>Riffing off his work, I created three shell scripts:</p>
<h3 class="h3 margin-top-oneandhalf"><code>git-log-pretty.sh</code></h3>
<p>collects the commit data I can get via format placeholders and writes a JSON file to a temp folder. <div class="u-text-muted u-text-smaller margin-top-fourth"><strong>Note:</strong> I decided to use the un-sanitized commit subject <code>%s</code> because I want my changelog to look nice. Because I sometimes use double quotes in commit messages and they would invalidate the JSON, I grabbed an escape-hack (the <code>ßßß</code>) from a <a href="https://gist.github.com/textarcana/1306223#gistcomment-1768173">comment by Alexandre Baizeau</a> and hope that'll be enough.</div></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> log <span class="token punctuation">\</span><br> --pretty<span class="token operator">=</span>format:<span class="token string">'{%n ßßßhashßßß: ßßß%hßßß,%n ßßßauthorßßß: ßßß%aN <%aE>ßßß,%n ßßßdateßßß: ßßß%cIßßß,%n ßßßsubjectßßß: ßßß%sßßß %n},'</span> <span class="token punctuation">\</span><br> <span class="token variable">$@</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/"/<span class="token entity" title="\\">\\</span>"/g'</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/ßßß/"/g'</span> <span class="token operator">|</span> <span class="token punctuation">\</span><br> perl -pe <span class="token string">'BEGIN{print "["}; END{print "]<span class="token entity" title="\n">\n</span>"}'</span> <span class="token operator">|</span> <span class="token punctuation">\</span><br> perl -pe <span class="token string">'s/},]/}]/'</span> <span class="token operator">></span> ./_tmp/git-log.json<br></code></pre>
<h3 class="h3 padding-top-half"><code>git-log-namestatus.sh</code></h3>
<p>outputs an array of changed files and their status for each commit into a second JSON file. The commits are made identifiable by <code>--format='%h'</code> which translates to the commit hash.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> log <span class="token punctuation">\</span><br> --name-status <span class="token punctuation">\</span><br> --format<span class="token operator">=</span><span class="token string">'%h'</span> <span class="token punctuation">\</span><br> <span class="token variable">$@</span> <span class="token operator">|</span> <span class="token punctuation">\</span><br> perl -lawne <span class="token string">'<br> if (defined <span class="token variable">$F</span>[1]) {<br> print qq# {"status": "<span class="token variable">$F</span>[0]", "path": "<span class="token variable">$F</span>[1]"},#<br> } elsif (defined <span class="token variable">$F</span>[0]) {<br> print qq#],<span class="token entity" title="\n">\n</span>"<span class="token variable">$F</span>[0]": [#<br> };<br> END{print qq#],#}'</span> <span class="token operator">|</span> <span class="token punctuation">\</span><br> <span class="token function">tail</span> -n +2 <span class="token operator">|</span> <span class="token punctuation">\</span><br> perl -wpe <span class="token string">'BEGIN{print "{"}; END{print "}"}'</span> <span class="token operator">|</span> <span class="token punctuation">\</span><br> <span class="token function">tr</span> <span class="token string">'<span class="token entity" title="\n">\n</span>'</span> <span class="token string">' '</span> <span class="token operator">|</span> <span class="token punctuation">\</span><br> perl -wpe <span class="token string">'s#(]|}),\s*(]|})#<span class="token variable">$1</span><span class="token variable">$2</span>#g'</span> <span class="token operator">|</span> <span class="token punctuation">\</span><br> perl -wpe <span class="token string">'s#,\s*?}<span class="token variable">$#</span>}#'</span> <span class="token operator">></span> ./_tmp/git-name-status.json<br></code></pre>
<h3 class="h3 padding-top-half"><code>git-log-merge.sh</code></h3>
<p>joins the two files' contents via the unique commit hashes.</p>
<pre class="language-bash"><code class="language-bash">jq --slurp <span class="token string">'.[1] as <span class="token variable">$logstat</span> | .[0] | map(.files = <span class="token variable">$logstat</span>[.hash])'</span> ./_tmp/git-log.json ./_tmp/git-name-status.json <span class="token operator">></span> ./_cache/gitlog.json</code></pre>
<p>As I said, I don't speak Perl – but through trial and error I was able to make the few cosmetic changes I needed. There is probably a much nicer way to do this without <code>sed</code>, or with <code>awk</code> or some clever Node.js streams that avoid the creation of temporary files altogether. Do you have a better solution? <a href="https://twitter.com/polarbirke">Tell me</a>, or even better: blog about it!</p>
<p>Commits in my merged JSON log look like this now:</p>
<pre class="language-bash"><code class="language-bash"><span class="token punctuation">{</span><br> <span class="token string">"hash"</span><span class="token builtin class-name">:</span> <span class="token string">"3fe0196"</span>,<br> <span class="token string">"author"</span><span class="token builtin class-name">:</span> <span class="token string">"Søren Birkemeyer <author@mailhost.com>"</span>,<br> <span class="token string">"date"</span><span class="token builtin class-name">:</span> <span class="token string">"2019-09-03T20:20:11+02:00"</span>,<br> <span class="token string">"subject"</span><span class="token builtin class-name">:</span> <span class="token string">"Add changelog to blog posts"</span>,<br> <span class="token string">"files"</span><span class="token builtin class-name">:</span> <span class="token punctuation">[</span><br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"M"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">".eleventy.js"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"M"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">".gitignore"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"A"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"_cache/gitlog.json"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"A"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"git-log-merge.sh"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"A"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"git-log-namestatus.sh"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"A"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"git-log-pretty.sh"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"M"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"gulpfile.js"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"M"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"src/_assets/scss/06_trumps/_text.scss"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"A"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"src/_data/commits.js"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"M"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"src/_includes/layouts/blogpost.njk"</span><br> <span class="token punctuation">}</span>,<br> <span class="token punctuation">{</span><br> <span class="token string">"status"</span><span class="token builtin class-name">:</span> <span class="token string">"A"</span>,<br> <span class="token string">"path"</span><span class="token builtin class-name">:</span> <span class="token string">"src/_includes/snippets/changelog.njk"</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">]</span><br> <span class="token punctuation">}</span></code></pre>
<p>I already use Gulp to compile my static assets (CSS/JS) so it seemed reasonable to add a (synchronous) series of tasks to execute my shell scripts. For this first iteration I plan to run these locally and commit the generated JSON. Believe me, the irony of committing a git log is not lost on me. 🤦♂️ It's a clutch until I find the time to test if and how I can run the scripts during a Netlify deployment.</p>
<p>Let's rather look at step two: processing the changelog with Eleventy.</p>
<h2>Filter commits by changed files and status "modified"</h2>
<p>I had decided to treat the changelog like my webmentions data and store it in a global <code>_cache</code> folder, so I needed a <a href="https://www.11ty.io/docs/data-js/">Javascript data file</a> to extract it and make it available to Eleventy.</p>
<p>Here's my <code>_data/commits.js</code> which exposes the changelog as a global <code>commits</code> variable I can use in my templates:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> <span class="token constant">CACHE_DIR</span> <span class="token operator">=</span> <span class="token string">'_cache'</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">readFromCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> filePath <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CACHE_DIR</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/gitlog.json</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>filePath<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> cacheFile <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span>filePath<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">return</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>cacheFile<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token keyword">return</span> <span class="token string">'{[]}'</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> <span class="token function">readFromCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre>
<p>Now I needed to figure out how to get only the commits I was interested in. Eleventy has a nifty way to <a href="https://www.11ty.io/docs/filters/">configure filter extensions for templates</a> which let you manipulate data at build time. I wrote down what I wanted to achieve: "Get a list of commits for every blog post that changed the source markdown file with a status of modified." I was specifically interested in the "modified" status because I didn't want the publish commit (status "added") to appear in my <em>change</em>log.</p>
<p>I tried a long time to write the filter with <code>Array.prototype.filter()</code>, but the deeply-nested array/object structure baffled me. In the end, I stuck to the tools I know and wrote two loops that check each file in each commit for a URL and status match:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// Git Commit Filter</span><br>eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span><span class="token string">'changelogForUrl'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">commits<span class="token punctuation">,</span> url</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">let</span> changelog <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br> commits<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">commit</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> commit<span class="token punctuation">.</span>files<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">file</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>file<span class="token punctuation">.</span>path <span class="token operator">===</span> url <span class="token operator">&&</span> file<span class="token punctuation">.</span>status <span class="token operator">===</span> <span class="token string">'M'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> changelog<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>commit<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token keyword">return</span> changelog<span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Do you know how to refactor this with <code>.filter()</code>? Perhaps with <code>.some()</code> and a callback? <a href="https://twitter.com/polarbirke">I'd be happy to learn</a>!</p>
<h2>Render the changelog</h2>
<p>All that was left to do was to actually show the changelog at the bottom of my blog posts. Because <code>page.inputPath</code> returned a string starting with a <code>./</code> I constructed the url to the markdown file by hand. Here's a slimmed down version of my <code>changelog.njk</code> partial:</p>
<pre class="language-twig"><code class="language-twig"><span class="token tag"><span class="token ld"><span class="token punctuation">{%-</span> <span class="token keyword">set</span></span> <span class="token property">url</span> <span class="token operator">=</span> <span class="token string"><span class="token punctuation">'</span>src<span class="token punctuation">'</span></span> <span class="token operator">~</span> <span class="token property">page</span><span class="token punctuation">.</span><span class="token property">url</span> <span class="token operator">~</span> <span class="token string"><span class="token punctuation">'</span>index.md<span class="token punctuation">'</span></span> <span class="token operator">|</span> <span class="token property">url</span> <span class="token rd"><span class="token punctuation">-%}</span></span></span><br><span class="token tag"><span class="token ld"><span class="token punctuation">{%-</span> <span class="token keyword">set</span></span> <span class="token property">changelog</span> <span class="token operator">=</span> <span class="token property">commits</span> <span class="token operator">|</span> <span class="token property">changelogForUrl</span><span class="token punctuation">(</span><span class="token property">url</span><span class="token punctuation">)</span> <span class="token rd"><span class="token punctuation">-%}</span></span></span><br><br><span class="token tag"><span class="token ld"><span class="token punctuation">{%</span> <span class="token keyword">if</span></span> <span class="token property">changelog</span> <span class="token operator">|</span> <span class="token property">length</span> <span class="token rd"><span class="token punctuation">%}</span></span></span><br><span class="token other"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>changelog<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> Changelog<br> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span><span class="token punctuation">></span></span></span><br> <span class="token tag"><span class="token ld"><span class="token punctuation">{%</span> <span class="token keyword">for</span></span> <span class="token property">commit</span> <span class="token operator">in</span> <span class="token property">changelog</span> <span class="token rd"><span class="token punctuation">%}</span></span></span><br> <span class="token other"><li id="</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">commit</span><span class="token punctuation">.</span><span class="token property">hash</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">"></span><br> <span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">commit</span><span class="token punctuation">.</span><span class="token property">date</span> <span class="token operator">|</span> <span class="token property">dateFromTimestamp</span> <span class="token operator">|</span> <span class="token property">readableDateShort</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><br> <span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">commit</span><span class="token punctuation">.</span><span class="token property">subject</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><br> <span class="token other"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span></span><br> <span class="token tag"><span class="token ld"><span class="token punctuation">{%</span> <span class="token keyword">endfor</span></span> <span class="token rd"><span class="token punctuation">%}</span></span></span><br> <span class="token other"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></span><br><span class="token tag"><span class="token ld"><span class="token punctuation">{%</span> <span class="token keyword">endif</span></span> <span class="token rd"><span class="token punctuation">%}</span></span></span></code></pre>
<p>I added the partial to my blog post template and voilà! I had my <a href="https://annualbeta.com/blog/on-tinkering/#changelog">very first changelog</a>.</p>
<p><img src="https://annualbeta.com/blog/a-changelog-for-my-blog-posts/on-tinkering-changelog.png" alt="A screenshot of the changelog for my blog post "On Tinkering" showing one entry from September 02, 2019: "Fix link to 11ty.io"" title="Fortunately I had a real correction to test with (I had forgotten to add https:// to a URL)."></p>
<p>So there it is. I will probably add links to the commits on Github once I am done deciding whether I want to switch the repo to public. Also, there's a lot of room for improvement; I definitely plan to revisit the shell scripts and corresponding Gulp tasks – and maybe I will also get to learn a few new tricks from replies to this blog post?</p>
<p>I'd love that. 👨💻</p>
Posting comments2019-09-15T00:00:00-00:00https://annualbeta.com/blog/posting-comments/<p>Two of the things I like most about the IndieWeb are the principles of ownership and connections. I really like the independence that posting on my own website gives me from any of the big silos and their <a href="https://indieweb.org/site-deaths">(possibly) impending shutdowns</a> (you never know, right?). I also like Webmentions: they're this crazy hybrid of oldschool blogs and pingbacks mashed up with the modern reality of social media.</p>
<h2>A POSSE of responses</h2>
<p>When I relaunched this site with Eleventy, I added basic Webmention support via <a href="https://webmention.io/">Webmention.io</a>. I'm also using <a href="https://brid.gy/">Bridgy</a> to check social networks (just Twitter, for now) for posts containing links to my blog posts and send those as webmentions.</p>
<p>A typical IndieWeb pattern is POSSE – "<em>P</em>ublish on your <em>O</em>wn <em>S</em>ite, <em>S</em>yndicate <em>E</em>lsewhere". Twitter is a great syndication silo: I can compose a quick summary or excerpt of the longform article, add the URL and fire it off (automatically or manually, whichever is preferable). Replies to this tweet are then collected by Bridgy and sent back to my site. So even though a discussion may take place on Twitter, I can archive and publish that same discussion in the context of my original article.</p>
<h2>A "post a comment" button on a static website?</h2>
<p>But something was still missing. How could I connect with visitors that find their way here via different means, like a Google search or a link on someone else's blog? They might never come across my tweet and not know about the magic of webmentions. Then I came across <a href="https://mxstbr.com/thoughts/css-in-js/">an article by Max Stoiber</a> (covering an unrelated but polarising topic), read it and discovered his brilliantly simple answer to my question: he had added a link pointing to a <a href="https://developer.twitter.com/en/docs/twitter-for-websites/tweet-button/guides/web-intent.html">Twitter Web Intent URL</a>! Maybe this was obvious to you from the beginning, but I didn't think of it.</p>
<p>It works like this: Web intents take query parameters, so one can use the <code>url</code> parameter to add the URL of a blog post. Clicking the link will then open a form where one can compose a new tweet with the URL already added. Here's how that looks in my Nunjucks template:</p>
<pre class="language-html"><code class="language-html">{%- set absoluteUrl -%}{{ page.url | url | absoluteUrl(metadata.url) }}{%- endset -%}<br><br><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">=</span><span class="token punctuation">"</span>https://twitter.com/intent/tweet/?url={{ absoluteUrl | urlencode }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Post a comment<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span><br></code></pre>
<p>Because this is a statically generated site, new webmentions are only fetched from the API and appended as HTML comments during my build process. It would absolutely be possible to fetch and render them dynamically with some client-side Javascript, but I find the thought of not-instant updates strangely endearing. I'm instead using <a href="https://ifttt.com/">IFTTT</a> to trigger hourly builds via a Netlify build hook (it's a <a href="https://www.11ty.io/docs/quicktips/netlify-ifttt/">recipe from the official Eleventy docs</a>).</p>
<p>And that's it! A comment function via Twitter, Bridgy, <span>Webmention.io</span>, Netlify and IFTTT. It's a bit circuitous, certainly, but has the added benefit of exposing any comment to the wider twitterverse and a potentially much larger audience.</p>
<p>I like it. 👍</p>
Why availability matters2019-11-04T10:19:01.428-00:00https://annualbeta.com/bookmarks/why-availability-matters/<blockquote>
<p>It's not 1% of people who always can't see your site and 99% of people who always can. It's 1% of visits.</p>
</blockquote>
<p>Stuart Langridge on the myth of '1% of users have JavaScript turned off'. (via <a href="https://twitter.com/adambsilver">@adambsilver</a>)</p>
Jeff Bezos's Master Plan - The Atlantic2019-11-08T15:05:50.178-00:00https://annualbeta.com/bookmarks/jeff-bezoss-master-plan-the-atlantic/<p>All of this confidence in Bezos's company has made him a singular figure in the culture, which, at times, regards him as a flesh-and-blood Picard. If 'Democracy dies in darkness'-the motto of the Bezos-era Washington Post-then he is the rescuer of the light, the hero who reversed the terminal decline of Woodward and Bernstein's old broadsheet.</p>
Everything is Amazing, But Nothing is Ours - alexdanco.com2019-11-10T12:14:59.723-00:00https://annualbeta.com/bookmarks/everything-is-amazing-but-nothing-is-ours-alexdancocom/<p>Wow, Alex is really hitting all the chords here. I love the way he links the decline of file managers as an entry point to our digital lives to the 10000-foot-view of <em>things</em> versus <em>services</em>.</p>
<blockquote>
<p>The constraints of mobile, plus a new generation of users that've never really known life without the internet, meant the benefits of skeuomorphism were no longer worth the cost. Ditching it as a philosophy, both in design and in function, freed us to go out and reinvent everything as a service. Abstract everything away into databases, links and logic, and provide it as a consumer service with all the topology and complexity hidden out of sight.</p>
</blockquote>
<p>He then goes on to point out the downside of our current 'everything is a service'-model:</p>
<blockquote>
<p>Worlds of scarcity are made out of things. Worlds of abundance are made out of dependencies. That's the software playbook: find a system made of costly, redundant objects; and rearrange it into a fast, frictionless system made of logical dependencies. The delta in performance is irresistible, and dependencies are a compelling building block: they seem like just a piece of logic, with no cost and no friction. But they absolutely have a cost: the cost is complexity, outsourced agency, and brittleness. The cost of ownership is up front and visible; the cost of access is back-dated and hidden.</p>
</blockquote>
<p>Last month's shutdown of Yahoo Groups (Yahoo is winding down its Yahoo Groups site after 18 years of running. As of October 28, users will no longer be able to post new content to the site, and on December 14 Yahoo will <em>permanently delete</em> all previously posted content) is once again a reminder that everything we don't (physically) own can be taken away by the real owners at any time. (via <a href="https://twitter.com/justmarkup">@justmarkup</a>)</p>
Computer Files Are Going Extinct - OneZero2019-11-10T18:58:54.004-00:00https://annualbeta.com/bookmarks/computer-files-are-going-extinct-onezero/<p>This is the article by Simon Pitt that inspired Alex Danco to write 'Everything is amazing, but nothing is ours'.</p>
<blockquote>
<p>The file has been replaced with the platform, the service, the ecosystem. This is not to say that I'm proposing we lead an uprising against services. You can't halt progress by clogging the internet pipes. I say this to mourn the loss of the innocence we had before capitalism inevitably invaded the internet. When we create now, our creations are part of an enormous system. Our contributions a tiny speck in an elastic database cluster. Rather than buying and collecting music, videos, or other cultural artifacts, we are exposed to the power hose: all culture, raging over us, for $12.99 a month (or $15.99 for HD) as long as we keep up our payments like good economic entities. When we stop paying, we're left with nothing. No files. The service is revoked.</p>
</blockquote>
<p>(via <a href="https://twitter.com/alex_danco">@alex_danco</a>)</p>
Teaching CSS | CSS-Tricks2019-11-19T08:05:54.333-00:00https://annualbeta.com/bookmarks/teaching-css-or-csstricks/<blockquote>
<p>As I started my CSS layout journey with a backdrop of people complaining about Netscape 4, I now continue against a backdrop of people whining about IE11. As our industry grows up, I would love to see us leaving these complaints behind. I think that this starts with us teaching CSS as a robust language, one which has been designed to allow us to present information to multiple environments, to many different people, via a sea of ever-changing devices.</p>
</blockquote>
<p>Rachel Andrew is absolutely right here. She also touches on a lot of the things happening with CSS right now (two-value <code>display</code>, moving away from physical dimensions like <code>top</code> and <code>bottom</code> to flexible ones like <code>start</code> and <code>end</code> that work in different writing modes, etc.). As is usual with her writing, this is well worth a read. (via <a href="https://twitter.com/hankchizljaw">@hankchizljaw</a>)</p>
Design Tip: Never Use Black by Ian Storm Taylor2019-12-26T13:35:26.417-00:00https://annualbeta.com/bookmarks/design-tip-never-use-black-by-ian-storm-taylor/<blockquote>
<p>Bottom line is: when you find <code>#000000</code> in your color picker, ask yourself if you really want pure black. You're probably better off with something more natural. And if you're feeling adventurous, try staying away from the left edge of the color picker altogether.</p>
</blockquote>
<p>A helpful reminder that we rarely encounter pure black in the real world, which is why the use of toned semi-blacks makes interfaces feel more 'natural'.</p>
The Google Analytics Setup I Use on Every Site I Build - Philip Walton2019-12-26T13:40:07.276-00:00https://annualbeta.com/bookmarks/the-google-analytics-setup-i-use-on-every-site-i-build-philip-walton/<blockquote>
<p>Google Analytics is a powerful yet quite complicated tool. And unfortunately, the truth is most people who use it don't reap its full benefits.</p>
</blockquote>
<p>Good advice from Philip Walton on getting the most out of Google Analytics. The information is from 2017 so some might be out-of-date by now, but a good chunk of it should stand the test of time for a few years.</p>
Aerotwist - Pixels are expensive2019-12-26T15:45:13.760-00:00https://annualbeta.com/bookmarks/aerotwist-pixels-are-expensive/<blockquote>
<p>How pixels get onto your users' screens is something you should know about. Not for the sake of knowing, but because in order to be effective as a modern web developer you're going to need to optimize for it.</p>
</blockquote>
<p>Totally agree.</p>
<p>Disclaimer: Post is from 2014 and archived because I really liked it back then, not because it's 100% accurate for 2020.</p>
'How to Make a Performance Budget' an article by Dan Mall2019-12-26T15:47:07.814-00:00https://annualbeta.com/bookmarks/how-to-make-a-performance-budget-an-article-by-dan-mall/<blockquote>
<p>The main reason to create a performance budget is to have a tangible starting point for conversation around a web page or website. It shouldn't act as gospel, but it's a thing you can measure against. It's your frame of reference.</p>
</blockquote>
Rendering Performance | Web Fundamentals | Google Developers2019-12-26T15:49:11.920-00:00https://annualbeta.com/bookmarks/rendering-performance-web-fundamentals-google-developers/<blockquote>
<p>To write performant sites and apps you need to understand how HTML, JavaScript and CSS is handled by the browser, and ensure that the code you write (and the other 3rd party code you include) runs as efficiently as possible.</p>
</blockquote>
<p>Understanding the render pipeline certainly helps!</p>
How To Write Fast, Memory-Efficient JavaScript - Smashing Magazine2019-12-26T15:56:19.801-00:00https://annualbeta.com/bookmarks/how-to-write-fast-memoryefficient-javascript-smashing-magazine/<blockquote>
<p>As we've seen, there are many hidden performance gotchas in the world of JavaScript engines, and no silver bullet available to improve performance.</p>
</blockquote>
<p>Is there a one true way to write efficient JavaScript? Of course not. As usual, it depends. You can only strive to understand the trade-offs to make informed decisions.</p>
<blockquote>
<p>Measure It. Understand it. Fix it. Rinse and repeat.</p>
</blockquote>
WPO Stats2019-12-26T15:59:29.191-00:00https://annualbeta.com/bookmarks/wpo-stats/<p>A list of curated case studies and experiments demonstrating the impact of web performance optimization (WPO) on user experience and business metrics.</p>
Smaller, Faster Websites - performance, responsive web design - Bocoup2019-12-26T16:13:44.904-00:00https://annualbeta.com/bookmarks/smaller-faster-websites-performance-responsive-web-design-bocoup/<blockquote>
<p>It's a table. It keeps your coffee off the floor.</p>
</blockquote>
<p>This quote alone makes Mat's conference talk transcript worth bookmarking. However, he also drops a ton of basic web performance knowledge, so⦠yeah. Good read. (via <a href="https://twitter.com/wilto">@wilto</a>)</p>
The 'Average Page' is a myth - igvita.com2019-12-26T21:56:01.921-00:00https://annualbeta.com/bookmarks/the-average-page-is-a-myth-igvitacom/<p>(via <a href="https://twitter.com/igrigorik">@igrigorik</a>)</p>
A Comprehensive Guide to Font Loading Strategies-zachleat.com2019-12-26T22:00:16.361-00:00https://annualbeta.com/bookmarks/a-comprehensive-guide-to-font-loading-strategieszachleatcom/<p>Zach Leatherman has an unhealthy obsession with web fonts, but his guide is as comprehensive as it is helpful: massively.</p>
Understanding Brotli's Potential - The Akamai Blog2019-12-26T22:04:03.594-00:00https://annualbeta.com/bookmarks/understanding-brotlis-potential-the-akamai-blog/<blockquote>
<p>Brotli isn't quite the revolutionary jump forward that a technology such as HTTP/2 is, but we should welcome every chance to save bytes on the wire. As far as website owners are concerned, it's virtually a flip of the switch that can result in anywhere from 14-39 percent file savings on text-based assets when running full-blast.</p>
</blockquote>
<p>Brotli was introduced in 2015 and while it has some caveats, especially when used for compression of dynamic assets, it is still better than GZIP on average. Time to start using it!</p>
Aerotwist - The Anatomy of a Frame2019-12-26T22:09:46.969-00:00https://annualbeta.com/bookmarks/aerotwist-the-anatomy-of-a-frame/<blockquote>
<p>a little reference for what's involved in shipping pixels to screen</p>
</blockquote>
<p>Paul Lewis is going deep into the rendering pipeline - again. Caveat: his is a Blink / Chrome view of the world; most of the main thread tasks are 'shared' in some form by all vendors, like layout or style calcs, but the overall architecture he describes may vary for Gecko / Firefox and Internet Explorer / EdgeHTML.</p>
Performance is a feature2019-12-26T22:14:14.053-00:00https://annualbeta.com/bookmarks/performance-is-a-feature/<blockquote>
<p>The problem is that performance is a feature that is not on anyone's product roadmap.</p>
<p>For whatever reason, the fact that it correlates directly to bounce rate, time on site, pages-per-visit etc. has not struck home with many product owners.</p>
</blockquote>
How Medium does progressive image loading - José M. Pérez2019-12-26T22:18:58.176-00:00https://annualbeta.com/bookmarks/how-medium-does-progressive-image-loading-jose-m-perez/<blockquote>
<p>As our pages load more and more images, it is good to think of their loading process on our pages, since it affects performance and user experience.</p>
<p>If you are generating several thumbnail sizes for your images, you can experiment using a very small one to use it as the background while the final image loads.</p>
</blockquote>
Book of Speed2019-12-26T22:22:05.141-00:00https://annualbeta.com/bookmarks/book-of-speed/<blockquote>
<p>No one likes to wait and we all hate slow pages.</p>
</blockquote>
<p>Stoyan Stefanov wrote an online book about 'The business, psychology and technology of high-performance web apps'. It's from 2011, but it's still a good primer.</p>
The future of loading CSS - JakeArchibald.com2019-12-26T22:28:10.199-00:00https://annualbeta.com/bookmarks/the-future-of-loading-css-jakearchibaldcom/<blockquote>
<p>The plan is for each <code><link rel='stylesheet'></code> to block rendering of subsequent content while the stylesheet loads, but allow the rendering of content before it. The stylesheets load in parallel, but they apply in series. This makes <code><link rel='stylesheet'></code> behave similar to <code><script src='â¦'></script></code>.</p>
</blockquote>
<p>Almost four years ago, Jake Archibald described how we could get progressive loading of CSS files by placing the references inside the <code><body></code> element.</p>
Killing CORS Preflight Requests on a React SPA - AlphaSights Engineering2019-12-26T22:32:06.074-00:00https://annualbeta.com/bookmarks/killing-cors-preflight-requests-on-a-react-spa-alphasights-engineering/The Web Fonts: Preloaded-zachleat.com2019-12-26T22:33:03.445-00:00https://annualbeta.com/bookmarks/the-web-fonts-preloadedzachleatcom/<blockquote>
<p>If you're not currently using a font loading strategy, using preload with web fonts will reduce the amount of FOIT visitors will see when they visit your site-paid for by sacrificing initial render time. Don't preload too much or the cost to initial render will be too high.</p>
</blockquote>
Caching best practices & max-age gotchas - JakeArchibald.com2019-12-26T22:34:27.147-00:00https://annualbeta.com/bookmarks/caching-best-practices-andamp-maxage-gotchas-jakearchibaldcom/<blockquote>
<p>Getting caching right yields huge performance benefits, saves bandwidth, and reduces server costs, but many sites half-arse their caching, creating race conditions resulting in interdependent resources getting out of sync.</p>
</blockquote>
Webfonts Last | Frederic Marx, Front-End Developer2019-12-26T22:42:02.319-00:00https://annualbeta.com/bookmarks/webfonts-last-or-frederic-marx-frontend-developer/<blockquote>
<p>Webfonts, in theory, are great. Using them responsibly though, without negative impact on the user experience, is an engineering nightmare at best, impossible at worst.</p>
</blockquote>
<p>Frederic Marx lays out his thoughts on web fonts vs. system fonts in a beautiful essay and concludes:</p>
<blockquote>
<p>Sustainable product design is about forming a relationship with our audience over time. Users will appreciate our efforts to make them more comfortable if they come in the right dose at the right moment.</p>
<p>This is why I think webfonts will last.</p>
</blockquote>
Using UI System Fonts In Web Design: A Quick Practical Guide - Smashing Magazine2019-12-26T22:43:58.237-00:00https://annualbeta.com/bookmarks/using-ui-system-fonts-in-web-design-a-quick-practical-guide-smashing-magazine/<blockquote>
<p>For perhaps the first time since the original Macintosh, we can get excited about using system UI fonts. They're an interesting, fresh alternative to web typography - and one that doesn't require a web-font delivery service or font files stored on your server.</p>
</blockquote>
Our best practices are killing mobile web performance • molily2019-12-26T22:49:13.881-00:00https://annualbeta.com/bookmarks/our-best-practices-are-killing-mobile-web-performance-molily/<blockquote>
<p>So why do mobile websites perform so badly? In my opinion, developers follow best practices. If they are applied without consideration, these practices may kill us:</p>
<ul>
<li>Progressive Enhancement and Unobtrusive JavaScript</li>
<li>Non-blocking, asynchronous JavaScript</li>
<li>Lazy-loading of non-critical content</li>
</ul>
</blockquote>
<p>Mathias Schäfer (molily) points out a few flaws in existing best practices and questions the real-life benefits of using 'unobtrusive JavaScript'.</p>
<blockquote>
<p>In the age of predominant mobile web access, some of these practices are causing harm. Some are fine per se but applied without consideration. I'm not suggesting to abandon them completely, but to revise them for the mobile age.</p>
</blockquote>
Secret Media report on ad blocking - Business Insider2019-12-26T22:50:38.978-00:00https://annualbeta.com/bookmarks/secret-media-report-on-ad-blocking-business-insider/<blockquote>
<p>The real reason people are turning on ad blockers is less about annoying ads and more about data consumed by the invisible tracking that goes on behind them, according to a new report by Secret Media.</p>
</blockquote>
Dynamic Social Sharing Images with Eleventy2019-12-27T00:00:00-00:00https://annualbeta.com/blog/dynamic-social-sharing-images-with-eleventy/<p>One year ago, I read <a href="https://24ways.org/2018/dynamic-social-sharing-images/">Dynamic Social Sharing Images</a> by Drew McLellan on 24ways and got super excited with the simplicity and ingeniousness of the technique.</p>
<p>Because it's the holidays and because I love tinkering, I sat down to try and replicate this with Eleventy. I wanted to pretty much follow Drew's steps and</p>
<ol>
<li>Render a subpage for each blog post, showing only the title for starters</li>
<li>Style the subpage with CSS so it can be turned into a 600 × 315 pixel image (the correct aspect ratio for Facebook’s <a href="https://developers.facebook.com/docs/sharing/webmasters/images">recommended image size</a>).</li>
<li>Screenshot all new subpages with <a href="https://github.com/puppeteer/puppeteer">Puppeteer</a></li>
</ol>
<p>It turns out Eleventy has just the right tool for step 1: <a href="https://www.11ty.dev/docs/pagination/">Pagination</a>.</p>
<p>I created a new template file (Nunjucks) in the <code>src</code> folder:</p>
<pre class="language-twig"><code class="language-twig"><span class="token other">---<br>pagination:<br> data: collections.article<br> size: 1<br> alias: article<br>permalink: /blog/</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">article</span><span class="token punctuation">.</span><span class="token property">fileSlug</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">/og-image/<br>---<br><span class="token doctype"><!doctype html></span><br><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>utf-8<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1.0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>title</span><span class="token punctuation">></span></span>Dynamic social sharing image for “</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">article</span><span class="token punctuation">.</span><span class="token property">data</span><span class="token punctuation">.</span><span class="token property">title</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">”<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>title</span><span class="token punctuation">></span></span><br> <link rel="stylesheet" type="text/css" href="</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token string"><span class="token punctuation">'</span>/css/og-image.css<span class="token punctuation">'</span></span> <span class="token operator">|</span> <span class="token property">url</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">" /><br> <script src="</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token string"><span class="token punctuation">'</span>/js/og-image.js<span class="token punctuation">'</span></span> <span class="token operator">|</span> <span class="token property">url</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">" defer><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>robots<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>noindex,nofollow<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <link rel="icon" type="image/png" href="</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token string"><span class="token punctuation">'</span>/favicon-32x32.png<span class="token punctuation">'</span></span> <span class="token operator">|</span> <span class="token property">url</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">" sizes="32x32"><br><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>figure</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>ab-og-image<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>h1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">article</span><span class="token punctuation">.</span><span class="token property">data</span><span class="token punctuation">.</span><span class="token property">title</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>a post on <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">=</span><span class="token punctuation">"</span>https://annualbeta.com<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>annualbeta.com<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>figure</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span></span></code></pre>
<p>Enabling pagination in the frontmatter tells Eleventy to loop over all items in the "article" collection (which happen to be my blog posts), use <code>article</code> as an alias, create one new page for each article and save it to a folder named <code>og-image</code>. The magic here is that <code>{{ article.fileSlug }}</code> will return the folder name of the current article, so Eleventy will always write the new <code>index.html</code> into the right parent folder.</p>
<p>I decided to not even bother with a <a href="https://www.11ty.dev/docs/layouts/">layout</a> here because the page only needs the bare minimum of markup to function. The external stylesheet contains the CSS needed to format the text, size the figure correctly and add a subtle amount of background noise.</p>
<p>Because the end product would be a static image at a fixed size, I decided to satisfy my typographic vanity and also wrote a bit of JavaScript to prevent single words on the last line (to be clear: I <em>usually</em> prioritize having less JavaScript over typographic detail for production websites, but not always – sometimes the aesthetic benefits outweigh <a href="https://v8.dev/blog/cost-of-javascript-2019">the cost of JavaScript</a>). Andy Bell's <a href="https://github.com/hankchizljaw/typemate">TypeMate</a> was just the right plugin for that.</p>
<p>You can see the result for each blog post (<a href="https://annualbeta.com/blog/a-changelog-for-my-blog-posts/og-image/">like this one</a>) if you append <code>og-image/</code> to the URL.</p>
<p>That left only step 3. I like and use Gulp a lot, so I ported <a href="https://gist.github.com/drewm/993d2237e24a928151b953fa3964ce9c">Drew's solution</a> over to my build pipeline. The screenshots are saved to the respective article's folder in <code>src</code>, so they get copied over to the <code>dist</code> folder every time Eleventy runs. I chose the article's URL slug as the file name, prefixed with "og-img-". That meant I could now add social meta tags to my blog post template:</p>
<pre class="language-twig"><code class="language-twig"><span class="token other"><meta property="og:title" content="</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">title</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">"><br><meta property="og:image" content="</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">page</span><span class="token punctuation">.</span><span class="token property">url</span> <span class="token operator">|</span> <span class="token property">url</span> <span class="token operator">|</span> <span class="token property">absoluteUrl</span><span class="token punctuation">(</span><span class="token property">metadata</span><span class="token punctuation">.</span><span class="token property">url</span><span class="token punctuation">)</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">og-img-</span><span class="token tag"><span class="token ld"><span class="token punctuation">{{</span></span> <span class="token property">title</span> <span class="token operator">|</span> <span class="token property">slug</span> <span class="token rd"><span class="token punctuation">}}</span></span></span><span class="token other">.png"><br><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>twitter:card<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>summary_large_image<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br><span class="token comment">{# and a few more optional ones, like publication date, twitter user, … #}</span></code></pre>
<p>A quick test with the Twitter card validator proves that everything is working:</p>
<p><img src="https://annualbeta.com/blog/dynamic-social-sharing-images-with-eleventy/twitter-card-validator.png" alt="Screenshot of Twitter's card validator, showing the social image for a blog post, saying "A changelog for my blog posts – a post on annualbeta.com"" title="It works!"></p>
<p>And that's it, folks!</p>
Stop painting and have a Meaningful Interaction with me!2019-12-27T10:12:58.470-00:00https://annualbeta.com/bookmarks/stop-painting-and-have-a-meaningful-interaction-with-me/<blockquote>
<p>When it comes to performance, what do we measure?</p>
</blockquote>
<p>Dion Almaer explains why TTFB and file size should not be the only metrics we look at and why 'time to first meaningful interaction' is such a great new addition in the performance toolkit.</p>
Why AMP is fast - Malte Ubl - Medium2019-12-27T10:34:57.239-00:00https://annualbeta.com/bookmarks/why-amp-is-fast-malte-ubl-medium/<blockquote>
<p>This is an unsorted list of optimizations that apply to all AMP based documents, which in aggregate makes them load fast. Every web page can have these optimizations, but AMP pages <em>cannot not</em> have them.</p>
</blockquote>
<p>An early look behind the AMP curtain: nothing AMP does is special in principle, it only strictly enforces the best practice everyone should be already following.</p>
CSS Will Change Module Level 12019-12-27T11:47:38.525-00:00https://annualbeta.com/bookmarks/css-will-change-module-level-1/<blockquote>
<p>The will-change property, like all performance hints, can be somewhat difficult to learn how to use 'properly', particularly since it has very little, if any, effect an author can directly detect. However, there are several simple 'Dos and Don'ts' which hopefully will help develop a good intuition about how to use will-change well.</p>
</blockquote>
<p>Excellent tips right from the specification.</p>
Positioning Inline Scripts | High Performance Web Sites2019-12-27T11:49:11.917-00:00https://annualbeta.com/bookmarks/positioning-inline-scripts-or-high-performance-web-sites/<blockquote>
<p>Inline scripts block rendering, just like external scripts, but are worse. External scripts only block the rendering of elements below them in the page. Inline scripts block the rendering of everything in the page.</p>
</blockquote>
<p>Be careful when using inline scripts, even the ones for async loading of external scripts.</p>
Script-injected 'async scripts' considered harmful - igvita.com2019-12-27T11:50:22.287-00:00https://annualbeta.com/bookmarks/scriptinjected-async-scripts-considered-harmful-igvitacom/<blockquote>
<h2>Script-inject all the things? Not so fast.</h2>
<p>The inline JavaScript solution has a subtle, but very important (and an often overlooked) performance gotcha: inline scripts block on CSSOM before they are executed.</p>
</blockquote>
Ten Things you didn't know about WebPageTest.org2019-12-27T11:52:00.099-00:00https://annualbeta.com/bookmarks/ten-things-you-didnt-know-about-webpagetestorg/<p>WebPageTest is an indispensable tool for performance analysis and it never hurts to get to know it better.</p>
Resource Hints - What is Preload, Prefetch, and Preconnect? - KeyCDN2019-12-27T11:53:46.639-00:00https://annualbeta.com/bookmarks/resource-hints-what-is-preload-prefetch-and-preconnect-keycdn/<blockquote>
<p>[â¦] they allow web developers to optimize delivery of resources, reduce round trips, and fetch resources to deliver content faster while a user is browsing a page.</p>
</blockquote>
<p>A good primer on resource hints.</p>
A Standard System of Measurements? - daverupert.com2019-12-27T11:55:57.857-00:00https://annualbeta.com/bookmarks/a-standard-system-of-measurements-daverupertcom/<blockquote>
<p>If the quasi-scientific Web Community were to collectively agree (yes, I realize this will never happen) to use the same testing environments, like Science, it could help eliminate vanity metrics and produce better comparisons and better practices.</p>
</blockquote>
<p>Dave wonders what a standardized, reproducible and objective system of performance metrics might look like.</p>
A Case Study on Boosting Front-End Performance | CSS-Tricks2019-12-27T11:59:30.220-00:00https://annualbeta.com/bookmarks/a-case-study-on-boosting-frontend-performance-or-csstricks/<blockquote>
<p>Our goal was to take control, focus on performance, be flexible for the future and make it fun to write content for our site. Here's how we mastered front-end performance for our site.</p>
</blockquote>
<p>A very solid and hands-on case study. Lots of good insights!</p>
AMP Websites Examples - amp.dev2019-12-27T12:01:35.711-00:00https://annualbeta.com/bookmarks/amp-websites-examples-ampdev/<p>A list of useful AMP example components.</p>
Why Perceived Performance Matters, Part 1: The Perception Of Time - Smashing Magazine2019-12-27T12:05:49.263-00:00https://annualbeta.com/bookmarks/why-perceived-performance-matters-part-1-the-perception-of-time-smashing-magazine/<blockquote>
<p>I aim to provide you with the reasons and theories for why things function in certain way. I will use examples that are observable in the offline world and, using principles of psychology, research and analysis in psychophysics and neuroscience, I will try to answer some 'Why?' questions.</p>
</blockquote>
<p>A superb round-trip from performance to neuroscience and back. This article heavily inspired a talk on perceived performance I delivered to <a href="http://uxbn.de/">UXBN</a> in 2017.</p>
Front-End Performance Checklist 2019 [PDF, Apple Pages, MS Word] - Smashing Magazine2019-12-27T12:07:29.051-00:00https://annualbeta.com/bookmarks/frontend-performance-checklist-2019-pdf-apple-pages-ms-word-smashing-magazine/<blockquote>
<p>An annual front-end performance checklist (PDF/Apple Pages/MS Word), with everything you need to know to create fast experiences today. Updated since 2016.</p>
</blockquote>
<p>This is a massive, <em>massive</em> beast of a checklist. Start reading at your own risk. 😅</p>
Understanding the Critical Rendering Path2019-12-27T12:14:03.077-00:00https://annualbeta.com/bookmarks/understanding-the-critical-rendering-path/<blockquote>
<p>Knowledge of the CRP is incredibly useful for understanding how a site's performance can be improved.</p>
</blockquote>
<p>So true.</p>
Web Performance Stats are Overrated - Standardista2019-12-27T12:15:49.142-00:00https://annualbeta.com/bookmarks/web-performance-stats-are-overrated-standardista/<blockquote>
<p>The statistic many web speeders quote 'a one second delay in web page responsiveness leads to a 7% decrease in conversions, an 11% drop in pageviews, and a 16% decrease in customer satisfaction' is kind of bullshit today. This quote comes from a <em>2008 study</em>: a study that came out a few months after the iPhone SDK was released and Android Market came into being, before either proliferated. When that study was conducted, the iPad and Android tablet were a few years off. As web speeders, we can't quote a performance study that basically predates mobile. Yet, that's what we're doing.</p>
</blockquote>
nytimes perf audit 2/19/172019-12-27T12:21:37.094-00:00https://annualbeta.com/bookmarks/nytimes-perf-audit-21917/<p>A Google Doc of a performance audit performed by Sam Saccone. Very nice to see how he tries to answer questions that arise during his audit. (via <a href="https://twitter.com/samccone">@samccone</a>)</p>
Flame Graphs2019-12-27T12:30:44.196-00:00https://annualbeta.com/bookmarks/flame-graphs/<blockquote>
<p>Flame graphs are a visualization of profiled software, allowing the most frequent code-paths to be identified quickly and accurately.</p>
</blockquote>
<p>An introduction to flame graphs by their creator. They were introduced in 2011 and inspired the <em>flame charts</em> we know from Dev Tools (first used in Chrome/WebKit in 2013).</p>
Aerotwist - 🌟 When everything's important, nothing is! 🌟2019-12-27T14:13:46.557-00:00https://annualbeta.com/bookmarks/aerotwist-when-everythings-important-nothing-is/<blockquote>
<p>Prioritization is a big deal. As our apps get bigger and more complex we need mechanisms to handle that.</p>
<p>I suppose what I'm really saying is that we need to move on from an off-on component world to one with nuance and, in particular, priorities.</p>
</blockquote>
<p>Paul Lewis dives deep into the advantages, trade-offs and just plain differences of server-side vs. client-side rendering.</p>
<p>Incidentally, this is the last blog post he ever wrote because he was completely disheartened by the massive amount of unnecessarily negative and violent pushback he received.</p>
Preload: What Is It Good For? - Smashing Magazine2019-12-27T14:16:37.684-00:00https://annualbeta.com/bookmarks/preload-what-is-it-good-for-smashing-magazine/<blockquote>
<p>Preload (<a href="https://w3c.github.io/preload/">spec</a>) is a new web standard aimed at improving performance and providing more granular loading control to web developers. It gives developers the ability to define custom loading logic without suffering the performance penalty that script-based resource loaders incur.</p>
</blockquote>
<p>Yoav Weià dives deep into the <code><link rel='preload'></code> resource hint.</p>
2019 in review2020-01-03T00:00:00-00:00https://annualbeta.com/blog/2019-in-review/<p>I've never written one of these before, but I enjoy reading other people's review posts, so maybe someone will find this interesting, too. In the very least, I can come back to it and use it to jog my memory.</p>
<p>Here are a few notable details that shaped 2019 for me:</p>
<h2>IndieWeb 😍</h2>
<p>If 2019 was anything, it was my year of rediscovering the <a href="https://indieweb.org/">IndieWeb</a>. A heartfelt thanks goes out to <a href="https://www.zachleat.com/">Zach Leatherman</a> whose static site generator <a href="https://www.11ty.dev/">Eleventy</a> enabled me to take a glorious journey from "maybe relaunching my blog" to many of my coding highlights from 2019. I completely <a href="https://annualbeta.com/blog/on-tinkering/">rebuilt this site</a> and blog from scratch and then started adding features. The fact that everything from the templates I write to the rendering engine that powers it all is using front-end technologies that I understand is <em>incredibly</em> liberating and invigorating.</p>
<p>I am still more of a tinkerer than a writer, recently proven by my abandonment of the weeknote routine after exactly three entries. Fortunately, I find it easier to write about what I build, so that'll be my hack going forward: build, write, repeat.</p>
<p>My favourite from 2019 is my <a href="https://annualbeta.com/blog/a-changelog-for-my-blog-posts/">automated changelogs feature</a>, but I also remixed Max Böck's <a href="https://mxb.dev/blog/indieweb-link-sharing/">IndieWeb Link Sharing</a> and use it to save and own all of my <a href="https://annualbeta.com/bookmarks/">bookmarks</a> (an ongoing process of screening hundreds of old bookmarks in Firefox and Chrome). The bookmarks search is powered by my very first <a href="https://svelte.dev/">Svelte</a> app, which I had wanted to try for ages. I will probably write about both soon, as well as more recent projects like my first feeble attempts at <a href="https://annualbeta.com/generative-art/">generating fancy shapes with code</a>.</p>
<h2>Reading 📚</h2>
<p>Reading is a big part of my life, always has been. In recent years, it is also increasingly one of my most important coping strategies for keeping resilience high and stress at bay. I'm an avid fantasy and science fiction reader, so my <a href="https://annualbeta.com/bookshelf">bookshelf</a> – an idea I poached from <a href="https://daverupert.com/bookshelf/">Dave Rupert</a> – is mostly filled with books I'd tag with "escapism", but there are also a few non-fiction ones.</p>
<p>The stand-out book was actually the first of the year: <a href="https://amzn.to/2F9cVLc">Foundryside</a> by Robert Jackson Bennet. I'm very much looking forward to the next instalment in the trilogy. Runner-up is the "Expanse"-series by James S. A. Corey. I like the TV series, but I loved the books.</p>
<h2>Health 💪</h2>
<p>Overall, 2019 was another healthy year for me. I think I may have taken one sick day, but I'm not exactly sure. I do consider myself incredibly lucky that I rarely get sick, I don't even catch any of the common bugs that make the rounds in our social circles.</p>
<p>But. There's a tiny "but". From late 2018 all the way into fall 2019 was also the time when my tendonitis in both wrists kept me from bouldering. Unfortunately, running still isn't something I enjoy (or actually <em>do</em>), so I have gained about 8 kilos despite walking an average of 5.1 kilometres a day (according to my phone). However! I restarted careful training in November and hope to be able to regularly visit my beloved <a href="https://www.bouldershabitat.de/">Boulder's Habitat</a> in 2020.</p>
<h2>Obi-Wau Kenobi 🐶</h2>
<p>Hi, my name is Søren and I am a dog person. Who knew? I certainly didn't. Niko – or, as he is secretly known, Obi-Wau Kenobi – continues to be a delight. He came to us in 2018 as part of an <a href="https://www.webfactory.de/blog/unser-buerohund-experiment">experiment</a> and stayed. He is a 10-years-old-ish rescue dog from Romania with a severe heart condition (3 leaky valves), arthritis and hypothyroidism, but the meds are working great and we had a good, drama-free year.</p>
<p><img src="https://annualbeta.com/blog/2019-in-review/niko.jpg" alt="Niko, backlit by the setting sun, is attentively looking past the camera – probably at a food source" title="Niko, completely ignoring me because he spotted fooood!"></p>
<p>He accompanies us to the office every day, so we're rarely apart. I am still not entirely sure how this grumpy, cantankerous, socially awkward and sleepy old furball managed to dig himself this deep into my heart.</p>
<h2>Travel 🏖</h2>
<p>We didn't do a lot of traveling this year except for one three-weeks-long vacation in the south of France on our beloved campsite. I'm afraid a sure sign of getting older is my growing fondness of revisiting known favourite places over trying to discover new ones.</p>
<p>No small thanks to Niko, we also explored the surrounding countryside around Bonn on many, many day trips.</p>
<h2>Music 🎧</h2>
<p>Thanks to Spotify, I can confidently announce that 2019 was the year when I listened to… Scorecore. A lot. Like, really. To be honest, I don't even know what Scorecore <em>is</em> – all I did was go looking for electronic music, preferably without vocals, to put on as a kind of "active noise-cancelling" at work. But yeah, Scorecore.</p>
<p>I also fulfilled a promise and went to a concert by The Kelly Family in early summer. It was a good gig, but they haven't become a staple in any of my playlists.</p>
<h2>Work 👨💻</h2>
<p>Work was neither exceptionally good nor bad in 2019. If I had to give it a score, I'd put it at about 3.75 out of 5. We still have the luxury problem of being asked for more work than we can deliver, which resulted in a bit of stress here and there.</p>
<p>On the positive side, I've thought a lot about what kind of work I want to do (and what not) and confirmed my feeling that my current job ticks many important checkboxes. I love that I can focus on things dear to my heart — like craftsmanship, accessibility and performance — every day, while working almost exclusively on projects that are ad-free and that further social engagement, for example the German programme website for the EU's <a href="https://www.solidaritaetskorps.de/">European Solidarity Corps</a>.</p>
<p>I'm looking forward to a great 2020, content in the knowledge that I love doing what I do.</p>
<h2>Fewer interruptions 😎</h2>
<p>I've managed to largely opt out of the attention game. I uninstalled my phone's apps for Facebook, Instagram and Twitter in late 2018 and have since also disabled notifications for WhatsApp, Slack and E-Mail. What can I say? It's bliss.</p>
<h2>Conferences 🧠</h2>
<p>I only went to one conference this year, but I made it count: <a href="https://beyondtellerrand.com/events/dusseldorf-2019/speakers">beyond tellerand (Düsseldorf edtion)</a>. As usual, Marc put on an excellent show. I especially loved Mike Hill's talk about <a href="https://www.youtube.com/watch?v=2V4cKPwRJBU">The Power of Metaphor</a>. Tickets for the 2020 event are already purchased. Obviously.</p>
<p class="margin-top-double margin-bottom-oneandhalf">Thanks for reading and all the best for 2020!</p>
It's 2019 and I Still Make Websites with my Bare Hands2020-01-03T16:27:29.660-00:00https://annualbeta.com/bookmarks/its-2019-and-i-still-make-websites-with-my-bare-hands/<blockquote>
<p>I have no idea how to make a website the way the cool kids do today.</p>
</blockquote>
<p>Very cool piece from an 'old guard' that speaks a lot to my <s>prejudices</s> preferences. I do understand why React, Vue et al make a lot of sense in certain situations - <a href="https://mxstbr.com/thoughts/css-in-js">Max Stoiber's "Why I write CSS-in-JS</a> helped me appreciate what other people might gain from CSS-in-JS approaches, for example - but for my day-to-day work, they mostly don't.</p>
<p>Which is why my experience with any modern JS framework is still quite limited, and most job descriptions look rather unattainable to me. That's okay, though - they usually look undesirable to me, too. (via <a href="https://twitter.com/meyerweb">@meyerweb</a>)</p>
Using Immutable Caching To Speed Up The Web - Mozilla Hacks - the Web developer blog2020-01-04T13:25:39.220-00:00https://annualbeta.com/bookmarks/using-immutable-caching-to-speed-up-the-web-mozilla-hacks-the-web-developer-blog/<blockquote>
<p>The benefits of immutable mean that when a page is refreshed, which is an extremely common social media scenario, elements that were previously marked immutable with an HTTP response header do not have to be revalidated with the server. Lacking this hint, the browser needs to guess which objects may or may not change on reload - wasting time on one hand or risking website incompatibility on the other.</p>
<p>For smaller objects, the work of this revalidation via a 304 HTTP response code can be almost as much work as just transferring the response fully.</p>
<p>It turns out this can save a lot of work. The page's javascript, fonts, and stylesheets do not change between reloads. More importantly, the dozens of images do not change either - different images may be included by the markup, but the content of individual images do not change. Indeed, about the only thing that might change is the markup itself.</p>
</blockquote>
The Critical Request | CSS-Tricks2020-01-04T13:28:44.591-00:00https://annualbeta.com/bookmarks/the-critical-request-or-csstricks/<blockquote>
<p>Serving a website seems pretty simple: Send some HTML, the browser figures out what resources to load next. Then we wait patiently for the page to be ready.</p>
<p>Little may you know, a lot is going on under the hood.</p>
<p>Have you ever wondered how browser figures out which assets should be requested and in what order?</p>
<p>Today we're going to take a look at how we can use resource priorities to improve the speed of delivery.</p>
</blockquote>
How to use SVG as a Placeholder, and Other Image Loading Techniques2020-01-04T13:35:37.415-00:00https://annualbeta.com/bookmarks/how-to-use-svg-as-a-placeholder-and-other-image-loading-techniques-jose-m-perez/<blockquote>
<p>[There are many] different tools and techniques to generate SVGs from images and use them as placeholders. The same way WebP is a fantastic format for thumbnails, SVG is also an interesting format to use in placeholders. We can control the level of detail (and thus, size), it's highly compressible and easy to manipulate with CSS and JS.</p>
</blockquote>
Should you self-host Google Fonts? | Tune The Web2020-01-29T08:26:15.503-00:00https://annualbeta.com/bookmarks/should-you-selfhost-google-fonts-or-tune-the-web/<blockquote>
<p>To answer the question in the title of this post: <strong>yes, it's better to self-host as the performance gains are substantial</strong>. (via <a href="https://twitter.com/fontspeed">@fontspeed</a>)</p>
</blockquote>
Old CSS, new CSS2020-02-03T22:03:00.373-00:00https://annualbeta.com/bookmarks/old-css-new-css-fuzzy-notepad/<p>Eevee pulls out all the stops with this mammoth piece about CSS history.</p>
<blockquote>
<p>In the beginning, there was no CSS.</p>
<p>This was very bad.</p>
</blockquote>
<p>Well, it wasn't all <em>bad</em>, but it was… tedious. Very, very tedious. 😁</p>
<blockquote>
<p>CSS 2 introduced the <code>></code> child selector, so you could write stuff like <code>ul.foo > li</code> to style special lists without messing up nested lists, and IE 6! Didn't! Fucking! Support! It!</p>
</blockquote>
<p>Oh dear, yes. I remember learning about the <code>></code> child selector and <em>then</em> learning that, well, IE 6! Didn't! Fucking! Support! It! 👀</p>
<blockquote>
<p>Another major factor appeared on April Fools' Day, 2004, when Google announced Gmail. Ha, ha! A funny joke. Webmail that isn't terrible? That's a good one, Google.</p>
<p>Oh. Oh, fuck. Oh they're not kidding. How the fuck does this even work</p>
</blockquote>
<p>Haha, hellooo <code>XMLHttpRequest</code>! What a great trip down memory lane. (via <a href="https://twitter.com/xkons64">@xkons64</a>)</p>
Hydration2020-02-07T19:11:36.845-00:00https://annualbeta.com/bookmarks/hydration/<blockquote>
<p>Don't get me wrong: server-side rendering is great …if what you're sending from the server is functional. It's the combination of hollow HTML sent from the server, followed by a huge browser-freezing dump of JavaScript that is an anti-pattern.</p>
<p>This use of server-side rendering followed by hydration feels like progressive enhancement, because it separates out the delivery of markup and scripts. But it's missing the mindset.<br>
[…]</p>
<p>The situation we have now is the worst of both worlds: server-side rendering followed by a tsunami of hydration. It has a whiff of progressive enhancement to it (because there's a cosmetic separation of concerns) but it has none of the user benefits.</p>
</blockquote>
The 2020 Election Will Be a War of Disinformation - The Atlantic2020-02-09T12:39:53.064-00:00https://annualbeta.com/bookmarks/the-2020-election-will-be-a-war-of-disinformation-the-atlantic/<blockquote>
<p>I was surprised by the effect it had on me. I'd assumed that my skepticism and media literacy would inoculate me against such distortions. But I soon found myself reflexively questioning every headline. It wasn't that I believed Trump and his boosters were telling the truth. It was that, in this state of heightened suspicion, truth itself-about Ukraine, impeachment, or anything else-felt more and more difficult to locate. With each swipe, the notion of observable reality drifted further out of reach.</p>
<p>What I was seeing was a strategy that has been deployed by illiberal political leaders around the world. Rather than shutting down dissenting voices, these leaders have learned to harness the democratizing power of social media for their own purposes-jamming the signals, sowing confusion. They no longer need to silence the dissident shouting in the streets; they can use a megaphone to drown him out. Scholars have a name for this: censorship through noise.</p>
</blockquote>
The TypeScript Tax - JavaScript Scene - Medium2020-02-09T14:52:05.914-00:00https://annualbeta.com/bookmarks/the-typescript-tax-javascript-scene-medium/<p>Eric Elliott with a nuanced, data-driven analysis of the pros and cons of TypeScript:</p>
<blockquote>
<p>That said, TypeScript hit an inflection point in 2018, and in 2019, a large number of production projects will use it. As a JavaScript developer, you may not have a choice. The TypeScript decision will be made for you, and you shouldn't be afraid of learning and using it.</p>
<p>But if you're in the position of deciding whether or not to use it, you should have a realistic understanding of both the benefits and the costs. Will it have a positive or negative impact?</p>
<p>In my experience, it has both, but falls short of positive ROI. Many developers love using it, and there are many aspects of the TypeScript developer experience I genuinely love. But all of this comes with a cost.</p>
</blockquote>
AddyOsmani.com - Native image lazy-loading for the web!2020-02-11T15:32:31.383-00:00https://annualbeta.com/bookmarks/addyosmanicom-native-image-lazyloading-for-the-web/<blockquote>
<p>The <code>loading</code> attribute allows a browser to defer loading offscreen images and iframes until users scroll near them. <code>loading</code> supports three values:</p>
<ul>
<li><strong>lazy</strong>: is a good candidate for lazy loading.</li>
<li><strong>eager</strong>: is not a good candidate for lazy loading. Load right away.</li>
<li><strong>auto</strong>: browser will determine whether or not to lazily load.</li>
</ul>
</blockquote>
<p>This is so cool! It's available in Chrome and easy to implement as an enhancement of current lazyloading strategies, e.g. paired with <a href="https://github.com/aFarkas/lazysizes">LazySizes</a> as a fallback:</p>
<pre class="language-js"><code class="language-js"><span class="token operator"><</span>img data<span class="token operator">-</span>src<span class="token operator">=</span><span class="token string">'unicorn.jpg'</span> loading<span class="token operator">=</span><span class="token string">'lazy'</span> alt<span class="token operator">=</span><span class="token string">'..'</span> <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">'lazyload'</span><span class="token operator">/</span><span class="token operator">></span><br><span class="token operator"><</span>img data<span class="token operator">-</span>src<span class="token operator">=</span><span class="token string">'cats.jpg'</span> loading<span class="token operator">=</span><span class="token string">'lazy'</span> alt<span class="token operator">=</span><span class="token string">'..'</span> <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">'lazyload'</span><span class="token operator">/</span><span class="token operator">></span><br><span class="token operator"><</span>img data<span class="token operator">-</span>src<span class="token operator">=</span><span class="token string">'dogs.jpg'</span> loading<span class="token operator">=</span><span class="token string">'lazy'</span> alt<span class="token operator">=</span><span class="token string">'..'</span> <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">'lazyload'</span><span class="token operator">/</span><span class="token operator">></span><br><br><span class="token operator"><</span>script<span class="token operator">></span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token string">'loading'</span> <span class="token keyword">in</span> <span class="token class-name">HTMLImageElement</span><span class="token punctuation">.</span>prototype<span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> images <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'img.lazyload'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> images<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">img</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> img<span class="token punctuation">.</span>src <span class="token operator">=</span> img<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>src<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br> <span class="token comment">// Dynamically import the LazySizes library</span><br> <span class="token keyword">let</span> script <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">'script'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> script<span class="token punctuation">.</span>async <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span><br> script<span class="token punctuation">.</span>src <span class="token operator">=</span><br> <span class="token string">'https://cdnjs.cloudflare.com/ajax/libs/lazysizes/4.1.8/lazysizes.min.js'</span><span class="token punctuation">;</span><br> document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>script<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br><span class="token operator"><</span><span class="token operator">/</span>script<span class="token operator">></span></code></pre>
<p>The future is now! 😁🚀</p>
Agile as Trauma - Dorian Taylor2020-02-11T21:09:32.179-00:00https://annualbeta.com/bookmarks/agile-as-trauma-dorian-taylor/<blockquote>
<p>The Agile Manifesto is an immune response on the part of programmers to bad management. The document is <a href="https://agilemanifesto.org/history.html">an expression of trauma</a>, and its intellectual descendants continue to carry this baggage.</p>
</blockquote>
<p>Wow. Now <em>that</em> is an interesting perspective.</p>
<blockquote>
<p><strong>Conceptual friggin' integrity</strong></p>
<p>The one idea from the 1970s most conspicuously absent from Agile discourse is conceptual integrity. This-another contribution from Brooks-is roughly the state of having a unified mental model of both the project <em>and</em> the user, shared among all members of the team. Conceptual integrity makes the product both easier to develop and easier to use, because this integrity is communicated to both the development team <em>and</em> the user, <em>through</em> the product.</p>
</blockquote>
<p>I really like this. Conceptual integrity is such a great goal to strive for.</p>
<blockquote>
<p>Behaviour has an advantage over features in that you can describe any feature in terms of behaviour, but you can't describe behaviour in terms of features. This is because features are visible while the software is sitting still, whereas behaviour is only visible while the software is running. Moreover, the presence of a feature can only indicate to a user if a goal is <em>possible</em>, behaviour will determine how painful it will be to achieve it.</p>
</blockquote>
<p>This essay turns out to be infinitely quotable. All the more reason to read it in full! (via <a href="https://twitter.com/beep">@beep</a>)</p>
Shared Cache is Going Away2020-02-17T17:08:46.725-00:00https://annualbeta.com/bookmarks/shared-cache-is-going-away/<blockquote>
<p>Browsers historically have had a single HTTP Cache. This meant that if www.a.example and www.b.example both used cdn.example/jquery-1.2.1.js then JQuery would only be downloaded once.</p>
</blockquote>
<p>Yay!</p>
<blockquote>
<p>Unfortunately, a shared cache enables a privacy leak.</p>
</blockquote>
<p>Noooo!</p>
<blockquote>
<p>Browsers are responding by partitioning the cache.</p>
</blockquote>
<p>Alright…?</p>
<blockquote>
<p>What does this mean for developers? The main thing is that there's no longer any advantage to trying to use the same URLs as other sites. You won't get performance benefits from using a canonical URL over hosting on your own site (unless they're on a CDN and you're not) and you have no reason to use the same version as everyone else (but staying current is still a good idea).</p>
</blockquote>
<p>Okay! So shared cache advantages are supposedly getting smaller and smaller due to rightfully implemented security improvements. This reminds me of a related article: <a href="https://csswizardry.com/2019/05/self-host-your-static-assets/">Self-Host Your Static Assets</a> by <a href="https://twitter.com/csswizardry">Harry Roberts</a>.</p>
Components, props, and Cartesian products2020-02-26T00:22:17.616-00:00https://annualbeta.com/bookmarks/components-props-and-cartesian-products/<p>🤯🚀</p>
Writing alternative text that matters - DEV Community 👩💻👨💻2020-03-01T15:33:36.166-00:00https://annualbeta.com/bookmarks/writing-alternative-text-that-matters-dev-community/<blockquote>
<p>If I ask anyone what accessibility means to them, usually the first thing that they can identify is 'Adding alt text!' All you have to do is add alt tags to your images makes your site way more accessible, right? I love the enthusiasm to reduce accessibility errors, but I am about to give you an infuriating response. <strong>It depends.</strong></p>
<p>Adding alt text may mean that you don't get pesky errors about missing alternative text on an Accessibility Scan, but it doesn't necessarily mean that your images have a better meaning. Sometimes an empty alt tag is actually what you need for a more accessible image. Are you envisioning the mind blown emoji and/or gif yet? (via <a href="https://twitter.com/yayMark">@yayMark</a>)</p>
</blockquote>
WebAIM: Alternative Text2020-03-01T15:34:10.870-00:00https://annualbeta.com/bookmarks/webaim-alternative-text/<blockquote>
<p>Adding alternative text for images is the first principle of web accessibility. It is also one of the most difficult to properly implement. The web is replete with images that have missing, incorrect, or poor alternative text. Like many things in web accessibility, determining appropriate, equivalent, alternative text is often a matter of personal interpretation. Through the use of examples, this article will present our experienced interpretation of appropriate use of alternative text.</p>
</blockquote>
A Variable Fonts Primer2020-03-06T15:09:07.218-00:00https://annualbeta.com/bookmarks/a-variable-fonts-primer/<p>A primer for all things variable fonts!</p>
<blockquote>
<p>Welcome to the most radical change in type since Gutenberg. Variable fonts let you add nuance and artistry to your web typography without bogging down your site. Now you can accomplish what used to require several files with a single file and some CSS!</p>
<p>The goal of this site is to show you how variable fonts tick. Discover how they can benefit user interface (UI) design, accessibility, and long-form reading, and how they push the boundaries of skillful typographic expression on the web. (via <a href="https://twitter.com/adactio">@adactio</a>)</p>
</blockquote>
What is code?2020-03-06T23:42:37.373-00:00https://annualbeta.com/bookmarks/what-is-code/<blockquote>
<p>Code isn't just obscure commands in a file. It requires you to have a map in your head, to know where the good libraries, the best documentation, and the most helpful message boards are located. If you don't know where those things are, you will spend all of your time searching, instead of building cool new things.</p>
</blockquote>
<p>A wonderful essay about all things programming.</p>
My favourite Git commit2020-03-08T19:20:32.683-00:00https://annualbeta.com/bookmarks/my-favourite-git-commit/<p>I just <em>love</em> great commit messages. This one seems over-the-top, but the closer you look, the better it gets.</p>
Compile Svelte in your head (Part 1) | Tan Li Hau2020-03-11T19:43:25.432-00:00https://annualbeta.com/bookmarks/compile-svelte-in-your-head-part-1-or-tan-li-hau/<blockquote>
<p>The main idea of this article is to show how the Svelte compiler compiles the Svelte syntax into statements of codes that I've shown above.</p>
</blockquote>
<p>It does what it says on the box! 😉</p>
The History of the URL2020-03-11T19:56:15.835-00:00https://annualbeta.com/bookmarks/the-history-of-the-url/<p>A fascinating deep-dive into the history of the humble URL. (via <a href="https://twitter.com/adactio">@adactio</a>)</p>
How I Slack - Rands in Repose2020-03-13T10:46:29.751-00:00https://annualbeta.com/bookmarks/how-i-slack-rands-in-repose/<p>Rands (Michael Lopp) shares how he uses Slack to communicate with the least possible effort. He shares</p>
<blockquote>
<p>A useful set of communication primitives</p>
</blockquote>
<p>that is 90% keyboard shortcuts and 10% mindset. I like it!</p>
Feedback Ladders: The Code Review System We Follow at Netlify2020-03-13T10:52:32.705-00:00https://annualbeta.com/bookmarks/feedback-ladders-the-code-review-system-we-follow-at-netlify/<blockquote>
<p>In an effort to make the levels of the feedback ladder easier to remember and use, Kristen came up with metaphorical names to describe each step. The idea is that you're living in a house that's still being built. Each of the 'inconveniences' we've listed (mountain, boulder, pebble, sand, dust) has a different level of impact on your day-to-day life in the house. For example, you may notice dust on the floor, but it doesn't impede your ability to live your life; on the other hand, a boulder blocking the door would quite literally be a blocker.</p>
</blockquote>
Get Moving (or not) with CSS Motion Path2020-03-14T16:25:00.983-00:00https://annualbeta.com/bookmarks/get-moving-or-not-with-css-motion-path/<blockquote>
<p>With the release of Firefox 72 on January 7, 2020, CSS Motion Path is now in Firefox, new Edge (slated for a January 15, 2020 stable release), Chrome, and Opera (and other Blink-based browsers). That means each of these browsers supports a common baseline of offset-path: path(), offset-distance, and offset-rotate.</p>
<p>The time to explore their possibilities is now.</p>
</blockquote>
Designing a complex table for mobile consumption (nom)2020-03-26T10:09:46.439-00:00https://annualbeta.com/bookmarks/designing-a-complex-table-for-mobile-consumption-nom/<blockquote>
<p>When faced with overwhelming content, focusing on user behavior can help define the design approach.</p>
</blockquote>
<p>Sound advice!</p>
<blockquote>
<p>The game changer in this project was the realization that users don't need to view a large data set all at once. By focusing on the discrete steps of how information is consumed, we were able to limit the presented content to the absolutely relevant.</p>
</blockquote>
<p>(via <a href="https://twitter.com/smashingmag/status/1243105625854410752">https://twitter.com/smashingmag/status/1243105625854410752</a>)</p>
<css-doodle />2020-04-12T08:38:13.185-00:00https://annualbeta.com/bookmarks/lesscssdoodle-greater/<p>Amazing playground for generative art via CSS declarations.</p>
CSS Findings From The New Facebook Design2020-04-14T07:35:26.384-00:00https://annualbeta.com/bookmarks/css-findings-from-the-new-facebook-design/<p>An interesting look under the hood of Facebook's CSS. Some cool ideas in there, but I'm not fond of 'spacer <div>s'.</div></p>
Using the HTML title attribute - updated March 2020 | TPG - The Accessibility Experts2020-04-17T07:55:31.152-00:00https://annualbeta.com/bookmarks/using-the-html-title-attribute-updated-march-2020-or-tpg-the-accessibility-experts/<blockquote>
<p>The HTML title attribute is problematic. It is problematic because it is not well supported in some crucial respects, even though it has been with us for over 23 years. With the rise of touch screen interfaces, the usefulness of this attribute has decreased. The accessibility of the title attribute has fallen victim to a unfortunate combination of poor browser support, poor screen reader support and poor authoring practices.</p>
</blockquote>
<p>Hear, hear! (via <a href="https://twitter.com/janwowen">@janwowen</a>)</p>
The Indieweb privacy challenge (Webmentions, silo backfeeds, and the GDPR) // Sebastian Greger2020-04-19T08:41:14.732-00:00https://annualbeta.com/bookmarks/the-indieweb-privacy-challenge-webmentions-silo-backfeeds-and-the-gdpr-sebastian-greger/<p>An excellent, nuanced article about ethical and legal implications regarding the use of Webmentions. Three key points:</p>
<ol>
<li>Misrepresentation of meaning</li>
</ol>
<blockquote>
<p>Often seen on Twitter: users explicitly state in their bio that 'likes' are not endorsements but personal bookmarks. Displaying such interaction as a 'like' on a connected blog <em>misrepresents the original intention</em>.</p>
</blockquote>
<ol start="2">
<li>Opaque processing of personal interactions</li>
</ol>
<blockquote>
<p>Secondly, while a Twitter user technically 'publishes' a message for all world to see as they like or retweet a tweet, the consequence that simply pushing a button within Twitter will result in their profile picture, name and 'endorsement' <em>being displayed on a third-party website may not be understood</em>.</p>
</blockquote>
<ol start="3">
<li>Taking away control from others</li>
</ol>
<blockquote>
<p>[…] an Indieweb site is at the same time <em>also taking ownership of other peoples' content</em>, expressions and conversations - this comes with responsibilities [<em>ed.: being able to update or delete</em>]. […] When pulling in social silo feedback, this is not necessarily provided for - at least with most current implementations, 'unliking' a tweet later <em>will not reliably remove it</em> from a site with a backfeed. (via <a href="https://twitter.com/schneyra">@schneyra</a>)</p>
</blockquote>
The Contrast Triangle2020-04-23T12:44:45.737-00:00https://annualbeta.com/bookmarks/the-contrast-triangle/<blockquote>
<p>Removing underlines from links in HTML text presents an accessibility challenge. In order for a design to be considered accessible, there is now a three-sided design contraint - or what I call 'The Contrast Triangle'.</p>
</blockquote>
<p>This is both a fantastic explanation <em>and</em> tool! (via <a href="https://twitter.com/dboudreau">@dboudreau</a>)</p>
Atomic CSS-in-JS2020-05-09T19:51:47.851-00:00https://annualbeta.com/bookmarks/atomic-cssinjs/<blockquote>
<p>Atomic CSS-in-JS can be seen as 'automatic atomic CSS':</p>
<ul>
<li>You don't need to create a class name convention anymore</li>
<li>Common and one-off styles are treated the same way</li>
<li>Ability extract the critical CSS of a page, and do code-splitting</li>
<li>An opportunity to fix the CSS rules insertion order issues in JS</li>
</ul>
</blockquote>
Second-guessing the modern web - macwright.org2020-05-11T12:17:09.331-00:00https://annualbeta.com/bookmarks/secondguessing-the-modern-web-macwrightorg/<blockquote>
<p>[T]here is a swath of use cases which would be hard without React and which aren't complicated enough to push beyond React's limits. But there are also a lot of problems for which I can't see any concrete benefit to using React. Those are things like blogs, shopping-cart-websites, mostly-<a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD</a>-and-forms-websites. For these things, all of the fancy optimizations are optimizations to get you closer to <em>the performance you would've gotten if you just hadn't used so much technology</em>.</p>
</blockquote>
<p>☝️ <strong>Hell yeah!</strong> (via <a href="https://twitter.com/jaffathecake">@jaffathecake</a>)</p>
Programming Sucks2020-05-16T11:42:04.242-00:00https://annualbeta.com/bookmarks/programming-sucks/<blockquote>
<p>The human brain isn't particularly good at basic logic and now there's a whole career in doing nothing but really, really complex logic. Vast chains of abstract conditions and requirements have to be picked through to discover things like missing commas. Doing this all day leaves you in a state of mild aphasia as you look at people's faces while they're speaking and you don't know they've finished because there's no semicolon.</p>
</blockquote>
In defense of the modern web - DEV2020-05-16T11:45:41.072-00:00https://annualbeta.com/bookmarks/in-defense-of-the-modern-web-dev/<blockquote>
<p>The future I want - the future I see - is one with tooling that's accessible to the greatest number of people (including designers), that can intelligently move work between server and client as appropriate, that lets us build experiences that compete with native on UX (yes, even for blogs!), and where upgrading part of a site to 'interactive' or from 'static' to 'dynamic' doesn't involve communication across disparate teams using different technologies.</p>
</blockquote>
<p>Excellent addendum slash rebuttal to <a href="https://annualbeta.com/bookmarks/secondguessing-the-modern-web-macwrightorg/">Second-guessing the modern web</a>. I absolutely agree with the quoted part of Rich's take-aways.</p>
Four Cool URLs - Alex Pounds' Blog2020-05-16T11:56:35.183-00:00https://annualbeta.com/bookmarks/four-cool-urls-alex-pounds-blog/<blockquote>
<p>URLs have been around for more than 20 years. They're a method of 'identifying a resource', which means 'unambiguously pointing to something on the internet.' That's how they're normally used: URLs point to websites, articles, images, songs, videos, downloads - everything. Most people don't give them much thought, in part because web browsers increasingly hide URLs away. But some URLs have special powers.</p>
<p>We can see some general principles at work in these URLs:</p>
<ul>
<li>A URL points to a thing, but it can also be the thing itself. […]</li>
<li>URLs can be for both human and machine consumption. […]</li>
<li>URLs can be robust. Even if the <a href="http://combine.fm/">Combine.fm</a> service fails or dies, you can easily return to the original links.</li>
<li>URLs can be predictable. […]</li>
<li>Let power users edit your URLs […]</li>
<li>Good URLs are descriptive. […]</li>
</ul>
<p>These principles aren't applicable to every scenario. Sometimes a link is just a pointer to a particular document online. A news website would find it hard to let users edit their links in a meaningful way. And sometimes you want your URLs to be hard to predict (for instance, you don't want people to guess the links to your Google docs).</p>
<p>URLs are consumed by machines, but they should be designed for humans. If your URL thinking stops at 'uniquely identifies a page' and 'good for SEO', you're missing out.</p>
</blockquote>
static site with a scrollable sidebar? Memorize the scroll position across page loads!2020-05-18T13:06:19.688-00:00https://annualbeta.com/bookmarks/static-site-with-a-scrollable-sidebar-memorize-the-scroll-position-across-page-loads/<p>This is… pretty nifty!</p>
What's in a name? | Sarah Higley2020-05-20T14:25:33.053-00:00https://annualbeta.com/bookmarks/whats-in-a-name-or-sarah-higley/<blockquote>
<p>A missing or incorrect accessible name in some form or other is right up there with poor color contrast in the list of most common accessibility errors across the web.</p>
<p>More recently, a greater awareness of accessibility and an increase in the use of ARIA attributes has resulted in a sort of reverse naming problem, where elements that cannot or should not be named get artificial names through ARIA.</p>
</blockquote>
<p>Sarah goes in-depth about the reasons and options for giving elements accessible names.</p>
LoadCSS: Low-Priority Asynchronous CSS Loading Demo - with no JS and CSP Support2020-05-23T09:37:17.610-00:00https://annualbeta.com/bookmarks/loadcss-lowpriority-asynchronous-css-loading-demo-with-no-js-and-csp-support/<p>A variant of Scott Jehl's clever idea for async CSS loading, but this one handles CSP (which may not allow inline JS) and provides a no-JS fallback.</p>
You Are What You Eat | Trent Walton2020-07-04T15:26:54.919-00:00https://annualbeta.com/bookmarks/you-are-what-you-eat-or-trent-walton/<blockquote>
<p>[…] what we work on affects not only our bank accounts, but our careers and lives in general. I like to think about projects the same way I think about calories-they're good for you unless you have too many, or they come in unhealthy packages, so you better make them count.</p>
</blockquote>
<p>An excellent post that - at 9 years old! - really stands the test of time. Still relevant, and always will be! (via <a href="https://twitter.com/fehler">@fehler</a>)</p>
How to build a filterable list of things2020-07-28T00:00:00-00:00https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How to build a filterable list of things</title>
<link rel="dns-prefetch" href="https://webmention.io/">
<link rel="preconnect" href="https://webmention.io/">
<style>
@charset "UTF-8";/*! normalize-scss | MIT/GPLv2 License | bit.ly/normalize-scss */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}main{display:block}code,pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}small{font-size:80%}sub{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}video{display:inline-block}img{border-style:none}svg:not(:root){overflow:hidden}button,input,select,textarea{line-height:1.15;font-family:sans-serif;font-size:100%;margin:0}button{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}input{overflow:visible}[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;display:table;max-width:100%;padding:0;color:inherit;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}details{display:block}summary{display:list-item}menu{display:block}canvas{display:inline-block}template{display:none}[hidden]{display:none}*,::after,::before{-webkit-box-sizing:border-box;box-sizing:border-box}*{margin:0;padding:0;outline:0}abbr[title]{border-bottom:1px dotted currentColor;text-decoration:none}blockquote:not([class]){padding-right:0;padding-left:0;padding:2rem;font-size:1.25rem;line-height:1.4;background-color:#fff;-webkit-box-shadow:-6px 0 0 0 rgba(0,0,0,.1);box-shadow:-6px 0 0 0 rgba(0,0,0,.1)}*+blockquote:not([class]){margin-top:2rem}blockquote:not([class])+*{margin-top:2rem}blockquote:not([class])>*+*{margin-top:1rem}body{color:rgba(0,0,0,.8);font-size:1.125rem;font-family:-apple-system,BlinkMacSystemFont,"avenir next",avenir,"helvetica neue",helvetica,ubuntu,roboto,noto,"segoe ui",arial,sans-serif;line-height:1.5;background-color:#f7f5ed;-webkit-font-smoothing:antialiased}cite{display:block;font-size:.875em}code{display:inline-block;padding:0 .35em;font-size:.875em;font-family:Consolas,monaco,monospace;background-color:#fff}blockquote code{background-color:#f7f5ed}*+pre[class*=language-]{margin-top:2rem}pre[class*=language-]+*{margin-top:2rem}figure{margin:0;background-color:rgba(0,0,0,.05)}figure:not([class]){padding-right:0;padding-left:0}*+figure:not([class]){margin-top:2rem}figure:not([class])+*{margin-top:2rem}figcaption{padding-top:.6666666667rem;padding-bottom:.6666666667rem;color:rgba(0,0,0,.55);font-size:.75em;background-color:#fff;padding-right:1rem;padding-left:1rem}@media (min-width:768px){figcaption{padding-right:2rem;padding-left:2rem}}@media (min-width:1024px){figcaption{padding-right:6rem;padding-left:6rem}}@media (min-width:768px){figcaption{padding-top:1rem;padding-bottom:1rem;font-size:.875em}}fieldset{padding:0;border:none}textarea{min-height:7.5em;line-height:1.25}.h1,h1:not([class]){margin:.125em 0 0;font-size:2.25rem;line-height:1.125;letter-spacing:-.025em}.h1+*,h1:not([class])+*{margin-top:4rem}@media (min-width:768px){.h1,h1:not([class]){font-size:3rem}}.h2,h2:not([class]){position:relative;font-size:1.5em;line-height:1.125;letter-spacing:-.025em}*+.h2,*+h2:not([class]){margin-top:2rem}@media (min-width:768px){.h2,h2:not([class]){font-size:2rem}}.h3,h3:not([class]){position:relative;font-size:1.15rem;line-height:1.125}.h3+*,h3:not([class])+*{margin-top:1rem}@media (min-width:768px){.h3,h3:not([class]){font-size:1.25rem}}.h4,h4:not([class]){font-size:1rem;line-height:1.125}.h4+*,h4:not([class])+*{margin-top:1rem}img{max-width:100%}figure img{display:block;margin:0 auto}a:not([class]){color:rgba(0,0,0,.9)}a:not([class]):focus,a:not([class]):hover{color:#fff;text-decoration:none;background-color:#000;-webkit-box-shadow:0 0 0 3px #000;box-shadow:0 0 0 3px #000}ul{margin-top:0;margin-bottom:0;margin-left:0;padding-left:0;list-style:none}*+ul:not([class]){margin-top:1rem}ul:not([class])>li{position:relative;padding-left:1.375em}ul:not([class])>li::before{position:absolute;top:-.25em;left:.3em;color:rgba(0,0,0,.8);content:"•";font-size:1.33em}ul:not([class])>li+li{margin-top:1rem}ul:not([class]) ol,ul:not([class]) ul{margin-top:.6666666667rem;margin-bottom:1rem;font-size:.9em}ol{margin:0;padding-left:0;list-style:none}ol:not([class]){list-style:decimal}*+ol:not([class]){margin-top:1rem}ol:not([class])>li{position:relative;margin-left:1.375em}ol:not([class])>li+li{margin-top:1rem}ol:not([class]) ol,ol:not([class]) ul{margin-top:.6666666667rem;margin-bottom:1rem;font-size:.9em}ol:not([class]) ol{list-style:lower-latin}ol:not([class]) ol ol{list-style:lower-roman}small{font-size:.875em}video{display:block;max-height:80vh}figure video{margin:0 auto}.ab-content-wrapper{max-width:61.875rem;margin:0 auto}@media (min-width:768px){.ab-content>*{padding-right:2rem;padding-left:2rem}}@media (min-width:1024px){.ab-content>*{padding-right:6rem;padding-left:6rem}}.o-flow>*+*{margin-top:1rem}.ab-blogpost__date{display:block;color:rgba(0,0,0,.55);font-weight:700;font-size:1rem;font-family:Consolas,monaco,monospace}.ab-blogpost__date+*{margin-top:.5rem}.ab-blogpost__link{color:rgba(0,0,0,.9);display:inline-block}.ab-blogpost__link:focus,.ab-blogpost__link:hover{color:#fff;text-decoration:none;background-color:#000;-webkit-box-shadow:0 0 0 3px #000;box-shadow:0 0 0 3px #000}.ab-button{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;padding:.4rem 1rem .5rem 1rem;border:1px solid #000;color:inherit;font-weight:inherit;font-size:inherit;font-family:-apple-system,BlinkMacSystemFont,"avenir next",avenir,"helvetica neue",helvetica,ubuntu,roboto,noto,"segoe ui",arial,sans-serif;line-height:1;text-decoration:none;background:0 0;cursor:pointer}.ab-button:focus,.ab-button:hover{color:#fff;text-decoration:none;background-color:#000;-webkit-box-shadow:0 0 0 3px #000;box-shadow:0 0 0 3px #000}.ab-button::-moz-focus-inner{padding:0;border:0}.ab-button--small{padding:.175em .35em;font-size:.875em}.ab-heading-anchor{color:rgba(0,0,0,.55);position:absolute;top:0;left:0;display:none;font-weight:400;text-decoration:none}.ab-heading-anchor:focus,.ab-heading-anchor:hover{color:rgba(0,0,0,.8);-webkit-box-shadow:0 0 0 3px #fff,0 0 0 5px #000;box-shadow:0 0 0 3px #fff,0 0 0 5px #000}@media (min-width:768px){h2:hover .ab-heading-anchor,h3:hover .ab-heading-anchor{display:block}}@media (min-width:1024px){.ab-heading-anchor{left:4rem}}.ab-link--undercover{color:rgba(0,0,0,.55);text-decoration:none}.ab-link--undercover:focus,.ab-link--undercover:hover{color:rgba(0,0,0,.8);text-decoration:underline}.ab-main{padding-top:4rem;padding-bottom:4rem}.ab-nav__item{margin-left:-.33em}.ab-nav__item+.ab-nav__item{margin-left:1.3333333333rem}@media (min-width:480px){.ab-nav__item+.ab-nav__item{margin-left:2rem}}.ab-nav__link{color:rgba(0,0,0,.9);text-decoration:none;padding:0 .33em}.ab-nav__link:focus,.ab-nav__link:hover{color:#fff;text-decoration:none;background-color:#000;-webkit-box-shadow:0 0 0 3px #000;box-shadow:0 0 0 3px #000}.ab-page{max-width:73.125rem;margin-right:auto;margin-left:auto;padding-right:1rem;padding-left:1rem}@media (min-width:768px){.ab-page{padding-right:2rem;padding-left:2rem}}.ab-header{padding-top:1rem;padding-bottom:1rem;border-bottom:2px solid rgba(0,0,0,.1)}@media (min-width:480px){.ab-header .ab-content-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}}@media (min-width:768px){.ab-header{padding-right:2rem;padding-left:2rem}}.ab-logo{color:rgba(0,0,0,.9);text-decoration:none;font-size:1.25rem}.ab-logo:focus,.ab-logo:hover{color:#fff;text-decoration:none;background-color:#000;-webkit-box-shadow:0 0 0 3px #000;box-shadow:0 0 0 3px #000}@media (min-width:768px){.ab-logo{font-size:1.5rem}}.ab-badge{padding:.125em .35em .15em;border:1px solid currentColor;color:rgba(0,0,0,.8);font-size:.75em;line-height:1.25;text-decoration:none}.ab-badge:focus,.ab-badge:hover{color:#fff;background-color:#000}.ab-badge:focus .ab-badge__count,.ab-badge:hover .ab-badge__count{color:#fff}.ab-badge__count{color:rgba(0,0,0,.55);font-size:.75em;font-family:Consolas,monaco,monospace}.ab-featured-posts{display:grid;grid-template-columns:1fr;gap:2rem}@media (min-width:768px){.ab-featured-posts{grid-template-columns:1fr 1fr}}@media (min-width:1024px){.ab-featured-posts{grid-template-columns:1fr 1fr 1fr}}.ab-featured__item{background-color:#fff}.ab-featured__header{padding:2rem}.ab-social__icon{display:block;pointer-events:none;width:1.3333333333rem}.ab-webmentions{padding-top:2rem;padding-bottom:2rem;border-top:1px solid rgba(0,0,0,.1)}*+.ab-webmentions{margin-top:2rem}.ab-comment__info{max-width:18rem;margin-top:1rem;color:rgba(0,0,0,.55);font-size:.75em;line-height:1.25}@media (min-width:532px){.ab-comment__info{margin-top:0}}*+.ab-webmentions__container+.ab-webmentions__container{margin-top:2rem}.ab-webmentions__list{margin:0;padding:0;list-style:none;margin-top:2rem}.ab-likes__list,.ab-reposts__list{margin:0;padding:0;list-style:none;margin-top:.5rem;padding-bottom:1rem;padding-left:1.5rem}.ab-webmentions__item{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding-left:4rem;font-size:1rem}*+.ab-webmentions__item{margin-top:2rem}.ab-webmentions__item--by-myself{padding-top:.5rem;padding-right:.5rem;padding-bottom:.5rem;font-size:.75em;background-color:rgba(0,0,0,.05)}.ab-webmention__meta{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.ab-webmention__author{color:rgba(0,0,0,.9);display:-webkit-box;display:-ms-flexbox;display:flex;margin-right:1rem}.ab-webmention__author:focus,.ab-webmention__author:hover{color:#fff;text-decoration:none;background-color:#000;-webkit-box-shadow:0 0 0 3px #000;box-shadow:0 0 0 3px #000}.ab-webmention__author-photo{position:absolute;top:0;left:0;width:3rem;border-radius:50%}.ab-webmentions__item--by-myself .ab-webmention__author-photo{top:.5rem;left:.5rem;width:2.5rem}.ab-webmention__pubdate{color:rgba(0,0,0,.55);font-size:.75em;font-family:Consolas,monaco,monospace}*+.ab-webmention__content{margin-top:.5rem}*+.ab-webmentions__item--by-myself .ab-webmention__content{margin-top:.25rem}code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}.u-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;margin:-1px!important;padding:0!important;border:0!important;overflow:hidden!important;clip:rect(0 0 0 0)!important}.u-background{background-color:rgba(0,0,0,.05)}.u-background-white{background-color:#fff}.u-background-black{background-color:#000}.u-block{display:block!important}.u-inline-block{display:inline-block!important}.u-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.u-flex-wrap{-ms-flex-wrap:wrap;flex-wrap:wrap}.u-ai-center{-webkit-box-align:center;-ms-flex-align:center;align-items:center}[hidden]{display:none}.list--unstyled{margin:0;padding:0;list-style:none}.margin{margin:2rem!important}.margin-top{margin-top:2rem!important}.margin-top-half{margin-top:1rem!important}.margin-top-fourth{margin-top:.5rem!important}.margin-top-fifth{margin-top:.4rem!important}.margin-top-sixth{margin-top:.3333333333rem!important}.margin-top-tenth{margin-top:.2rem!important}.margin-top-oneandhalf{margin-top:3rem!important}.margin-top-double{margin-top:4rem!important}.margin-bottom{margin-bottom:2rem!important}.margin-bottom-half{margin-bottom:1rem!important}.margin-bottom-fourth{margin-bottom:.5rem!important}.margin-bottom-sixth{margin-bottom:.3333333333rem!important}.margin-bottom-oneandhalf{margin-bottom:3rem!important}.margin-bottom-double{margin-bottom:4rem!important}.margin-left{margin-left:2rem!important}.margin-left-fourth{margin-left:.5rem!important}.margin-right{margin-right:2rem!important}.margin-right-fourth{margin-right:.5rem!important}.padding-0{padding:0!important}.padding{padding:2rem!important}.padding-half{padding:1rem!important}.padding-double{padding:4rem!important}.padding-top-half{padding-top:1rem!important}.padding-left{padding-left:2rem!important}.u-text-small{font-size:.875em}.u-text-smaller{font-size:.75em}.u-text-smallest{font-size:.66666667em}.u-text-bigger{font-size:1.25em}.u-text-bold{font-weight:700}.u-text-muted{color:rgba(0,0,0,.55)}.u-text-monospace{font-family:Consolas,monaco,monospace!important}.u-text-tight{line-height:1.25}.u-text-center{text-align:center}
</style>
<style>
.talk {
margin-top: 1rem;
}
[x-data] .ab-heading-anchor {
display: none !important;
}
label {
display: block;
}
select {
width: 100%;
padding: 0.125rem;
border: 1px solid currentColor;
font-weight: inherit;
font-size: inherit;
font-family: inherit;
}
select:hover,
select:focus {
box-shadow: 0 0 0 3px #000;
}
@supports (display: grid) {
.filters {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(12rem, 15rem));
justify-content: center;
}
.talks {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}
.talk {
margin-top: 0;
}
.no-results {
display: block;
grid-column: 1 / -1;
text-align: center;
}
.talk:not([style*='display: none']) ~ .no-results {
display: none;
}
}
</style>
<script src="https://annualbeta.com/js/lazyloading.js" defer=""></script>
<script type="module" src="https://annualbeta.com/js/alpine.min.js" defer=""></script>
<script nomodule="" src="https://annualbeta.com/js/alpine-ie11.min.js" defer=""></script>
<link rel="icon" type="image/png" href="https://annualbeta.com/favicon-32x32.png" sizes="32x32">
<link rel="webmention" href="https://webmention.io/annualbeta.com/webmention">
<link rel="pingback" href="https://webmention.io/annualbeta.com/xmlrpc">
<link rel="alternate" href="https://annualbeta.com/feed.xml" type="application/atom+xml" title="annualbeta">
<meta name="monetization" content="$pay.stronghold.co/1a1f797e1989cae43fbac4aaa8dc37aee69">
<meta name="description" content="Infrequently published reports from the world of front-end development with a focus on accessibility, web performance, CSS and 11ty.">
<meta property="og:title" content="How to build a filterable list of things">
<meta property="og:image" content="https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/og-img-how-to-build-a-filterable-list-of-things.png">
<meta property="og:url" content="https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/">
<meta name="twitter:card" content="summary_large_image">
<meta property="og:type" content="article">
<meta property="article:published_time" content="2020-07-28T00:00:00">
<meta name="author" content="Søren Birkemeyer">
<meta property="og:site_name" content="The personal website & blog of Søren Birkemeyer, a front-end developer from Bonn, Germany">
<meta property="og:locale" content="en_GB">
<meta name="twitter:site" content="@polarbirke">
</head>
<body>
<div class="ab-page">
<header class="ab-header">
<div class="ab-content-wrapper">
<a class="ab-logo u-text-bold" href="https://annualbeta.com/">annualbeta</a>
<nav class="ab-nav margin-top-tenth" aria-label="Main navigation">
<ul class="u-flex list--unstyled">
<li class="ab-nav__item">
<a class="ab-nav__link" href="https://annualbeta.com/blog/">
Blog
</a>
</li><li class="ab-nav__item">
<a class="ab-nav__link" href="https://annualbeta.com/bookmarks/">
Bookmarks
</a>
</li><li class="ab-nav__item">
<a class="ab-nav__link" href="https://annualbeta.com/about/">
About
</a>
</li></ul>
</nav>
</div>
</header>
<main id="main" class="ab-main">
<article class="ab-content-wrapper o-flow eleventy-transform h-entry">
<header class="ab-content">
<time class="ab-blogpost__date dt-published" datetime="2020-07-28T00:00:00">July 28, 2020</time>
<h1 class="h1 p-name">How to build a filterable list of things</h1>
</header>
<div class="o-flow ab-content margin-top-double e-content">
<p>Recently, one of our clients asked us to build a filter UI for a list of things. I thought it might be fun to write up how I approach a brief like that. This will not be a technical tutorial, but there is a <a href="#h-functional-demo:-a-filterable-list-of-talks-for-a-fictitious-conference">functional demo</a> that you can inspect with dev tools.</p>
<h2>What am I doing here?</h2>
<p>The first question I ask is, "What problem is this supposed to solve?" Sometimes the client's idea of a solution is a partial fit, but a different approach might be even better for their users. In this case, the brief is to add a filter to an existing list of things. The client wants to give customers the option to narrow down the amount of things and make the list easier to scan, which is pretty straightforward.</p>
<h2>Do I go for a server-side or client-side solution?</h2>
<p>Second question: "Do I build the filter in PHP or in JavaScript"? This may be an odd question for modern front-end developers, but many of our client projects are presentational sites which (in my opinion) benefit from our PHP/Symfony-based stack that does all the heavy lifting on the server and sends finished HTML with very little JavaScript to the browser.</p>
<p>There's a follow-up question hidden inside: if I build the filter with JavaScript, do I take over rendering the list or do I keep it in PHP/HTML?</p>
<p>Here's the kind of checklist that I go through before making a decision:</p>
<ul>
<li><p class="u-text-bold">Are there a lot of these things? Is the list going to be paginated?</p>
<p class="u-text-small margin-top-sixth">If the answer is yes, PHP scores a point for both list rendering and the filter, because I'm not a fan of juggling pagination and filtering in JavaScript (although it can be done).</p></li>
<li class="margin-top-half"><p class="u-text-bold">Is the list of things the main content of the page, or is it more of an aside that could be considered "bonus content"?</p>
<p class="u-text-small margin-top-sixth">If it's the main content I give a point to PHP for list rendering. Having everything in the HTML is a robust baseline.</p></li>
<li class="margin-top-half"><p class="u-text-bold">What's the initial state? Show everything and allow users to filter it down, or show nothing and reveal matches upon user interaction?</p>
<p class="u-text-small margin-top-sixth">If the default is to see all things, I have a strong preference for having all things in the server-rendered HTML payload. This answer would be a point for list rendering in PHP.</p></li>
<li class="margin-top-half"><p class="u-text-bold">Do filtered results need to be persisted when users navigate away from the list page and come back via "back" button? Should filtered results be shareable via URLs?</p>
<p class="u-text-small margin-top-sixth">If yes, I keep that in mind as a filter requirement, but neither PHP nor JavaScript get a point because both are a good enough fit.</p></li>
<li class="margin-top-half"><p class="u-text-bold">Would the user experience be improved by an instant filter response instead of a whole server roundtrip?</p>
<p class="u-text-small margin-top-sixth">Arguably, the answer to this is always yes – unless the search is perceived as complex and a super fast response might erode user trust (<a href="https://90percentofeverything.com/2010/12/16/adding-delays-to-increase-perceived-value-does-it-work/">that's a thing</a>). This point goes to a client-side filter built with JavaScript 99,9% of the time.</p></li>
</ul>
<p>Holding this list against my project, I see that there are 12-20 things at most, so pagination is not necessary. The list is the main content and the default state is to show all things. The client does not care about shareable URLs.</p>
<p>After looking at these answers, I decide to implement the filter function as a client-side enhancement for a server-rendered list. The main purpose is to let people access the things, and at a maximum of 20 things the filter is nice-to-have, not a necessity.</p>
<p>There are questions left to ask at this point, but for our kind of projects they're rhetorical:</p>
<ul>
<li><p class="u-text-bold">What about older browsers like IE11?</p>
<p class="u-text-small margin-top-sixth">We support them as much as possible. I'm confident I can build this filter in a way that it works in old IE, but I'd be okay with hiding this nice-to-have enhancement from old browsers if supporting them were to prove unreasonable.</p></li>
<li class="margin-top-half"><p class="u-text-bold">How accessible are we going to make this?</p>
<p class="u-text-small margin-top-sixth">Again, as much as possible. I try to deliver accessible solutions to the best of my ability.</p></li>
</ul>
<h2>How am I going to build this, then?</h2>
<p>It's time to make a plan. Since the list is already in the HTML, I don't need a big JavaScript framework to render it. I could write the filter myself, but I think that <a href="https://github.com/alpinejs/alpine">Alpine.js</a> is an excellent fit for my task: tiny footprint, nice reactivity and easy to use as a drop-in tool. And there is a way to support older browsers without penalizing modern ones via the <a href="https://philipwalton.com/articles/deploying-es2015-code-in-production-today/">module/nomodule pattern</a>.</p>
<p>Alpine.js has great documentation and works a lot like Vue.js, which has even more documentation, so I won't go into implementation details here. You can inspect the demo if you're curious.</p>
<p>Back to the plan. The filter will be a <code><form></code> with <code><label></code> and <code><select></code> elements. Using native HTML elements will not make the filter functionality accessible by itself, but take us a good part of way there. With Alpine.js, it makes sense to build the form in the template and deliver it with the list HTML. I'm optimistic that most users will be able to use JavaScript, but I will hide the form with CSS if the browser does not remove the class <code>no-js</code> from the <code><html></code> element with JavaScript (<a href="https://www.paulirish.com/2009/avoiding-the-fouc-v3/">a trick from the olden days</a>). On the other hand, it's nice to have the form in the HTML at first render, which sidesteps any issues with layout shift that might occur if we created the form with JavaScript.</p>
<p>A slight disadvantage of the client-side filter is that we have to notify the user about list updates ourselves. With a conventional form submit you get a full page load, which is a heavy-handed kind of notification to be sure, but it's also hard to miss. The Alpine.js-powered filter will update the list instantly, but without extra work, there is no guarantee users of assistive technology (AT) will have a similar experience as sighted users who notice the list changing in their field of view. This is what <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">ARIA live regions</a> are for. There was a timely livestream about them last week: <a href="https://www.youtube.com/watch?v=W5YAaLLBKhQ">The Many Lives of a Notification</a> by <a href="https://twitter.com/codingchaos">Sarah Higley</a>. The most important takeaway: keep notifications concise. It would be a bad idea to make the list a live region, because then every filter update would prompt AT to read out all remaining things. I am going to add a visually hidden live region instead that will announce the number of matches after each update.</p>
<h2>Functional demo: a filterable list of talks for a fictitious conference</h2>
<figure x-data="alpine.filter()">
<div class="padding-double">
<h2 class="u-text-center u-text-tight">
<span class="u-text-monospace u-block u-text-bigger">:pseudo-conference 2020</span>
Schedule
</h2>
<form class="margin-top" @submit="preventDefault();">
<fieldset>
<legend class="u-visually-hidden">Filter talks</legend>
<ul class="filters">
<li class="filter">
<label for="day">Day</label>
<select class="margin-top-tenth" id="day" name="day" @change="$nextTick(() => updateResultsCount());" x-model="day">
<option value="">Any</option>
<option value="monday">Monday</option>
<option value="tuesday">Tuesday</option>
</select>
</li>
<li class="filter">
<label for="track">Track</label>
<select class="margin-top-tenth" id="track" name="track" @change="$nextTick(() => updateResultsCount());" x-model="track">
<option value="">Any</option>
<option value="a11y">a11y</option>
<option value="css">CSS</option>
<option value="jamstack">Jamstack</option>
<option value="privacy">Privacy</option>
</select>
</li>
<li class="filter">
<label for="location">Location</label>
<select class="margin-top-tenth" id="location" name="location" @change="$nextTick(() => updateResultsCount());" x-model="location">
<option value="">Any</option>
<option value="google meet">Google Meet</option>
<option value="zoom">Zoom</option>
</select>
</li>
</ul>
</fieldset>
</form>
<ul class="list--unstyled talks margin-top-oneandhalf">
<li class="talk padding-half u-background-white js-talk" x-show="(!day || day === 'monday') && (!track || track === 'jamstack') && (!location || location === 'zoom')">
<span class="u-text-monospace u-text-small">#Jamstack</span>
<h3 class="u-text-tight">How to use 11ty for absolutely everything</h3>
<p class="u-text-small margin-top-tenth">Join us on <b>Monday</b> in <b>Zoom</b>.</p>
</li>
<li class="talk padding-half u-background-white js-talk" x-show="(!day || day === 'tuesday') && (!track || track === 'a11y') && (!location || location === 'google meet')">
<span class="u-text-monospace u-text-small">#a11y</span>
<h3 class="u-text-tight">That <div> should probably be a button</h3>
<p class="u-text-small margin-top-tenth">Join us on <b>Tuesday</b> in <b>Google Meet</b>.</p>
</li>
<li class="talk padding-half u-background-white js-talk" x-show="(!day || day === 'tuesday') && (!track || track === 'css') && (!location || location === 'google meet')">
<span class="u-text-monospace u-text-small">#CSS</span>
<h3 class="u-text-tight">Learn ASS - The Agile CSS methodology without specificity waterfalls</h3>
<p class="u-text-small margin-top-tenth">Join us on <b>Tuesday</b> in <b>Google Meet</b>.</p>
</li>
<li class="talk padding-half u-background-white js-talk" x-show="(!day || day === 'monday') && (!track || track === 'privacy') && (!location || location === 'zoom')">
<span class="u-text-monospace u-text-small">#Privacy</span>
<h3 class="u-text-tight">Adventures in GDPR: you can have the cookie and eat it, too</h3>
<p class="u-text-small margin-top-tenth">Join us on <b>Monday</b> in <b>Zoom</b>.</p>
</li>
<li class="talk padding-half u-background-white js-talk" x-show="(!day || day === 'monday') && (!track || track === 'css') && (!location || location === 'zoom')">
<span class="u-text-monospace u-text-small">#CSS</span>
<h3 class="u-text-tight">The power of rarely used CSS selectors</h3>
<p class="u-text-small margin-top-tenth">Join us on <b>Monday</b> in <b>Zoom</b>.</p>
</li>
<li class="talk padding-half u-background-white js-talk" x-show="(!day || day === 'tuesday') && (!track || track === 'a11y') && (!location || location === 'google meet')">
<span class="u-text-monospace u-text-small">#a11y</span>
<h3 class="u-text-tight">Why "The Last of Us Part II" is such a big deal for Accessibility</h3>
<p class="u-text-small margin-top-tenth">Join us on <b>Tuesday</b> in <b>Google Meet</b>.</p>
</li>
</ul>
<div class="u-text-center" x-bind:class="{'u-visually-hidden': currentCount !== 0}" role="region" aria-live="polite">
<p x-text="filterNotification"></p>
</div>
</div>
<figcaption>
Demo for a filterable list built with AlpineJS
</figcaption>
</figure>
<p>☝️ Here's the plan put into practice. Is it done? I think it's good enough for a first iteration. I went with CSS Grid for now, meaning IE11 and others will get stacked form fields and list items instead of horizontal rows, but I think that's okay. And it could be remedied with flexbox if need be. Another possible tweak: hide the filters on small screens and show a toggle button. Right now, the stack of three <code><select></code> elements takes up a lot of screen estate on a mobile phone and as I established earlier, the filter is a nice-to-have add-on and not a baseline feature.</p>
<p>Now, please play around with the demo and tell me if I missed anything. I'd be happy to learn how to make it (even) more accessible.</p>
</div>
</article>
<script>
window.alpine = {};
window.alpine.filter = function() {
return {
day:'',
track:'',
location:'',
filterNotification: '',
currentCount: '',
updateResultsCount: function() {
this.currentCount = [...document.querySelectorAll('.js-talk')]
.filter(talk => window.getComputedStyle(talk).display !== 'none')
.length;
this.filterNotification = this.currentCount ? `${this.currentCount} ${this.currentCount === 1 ? 'talk' : 'talks'} found.` : `No talks found. Try to change your filters!`;
}
}
}
</script>
<div id="changelog" class="ab-content-wrapper ab-content margin-top">
<div class="o-flow">
<p class="u-text-monospace u-text-small u-text-bold"> Changelog</p>
<ul class="list--unstyled margin-top-fourth">
<li id="c549283" class="u-text-smaller">
<span class="u-inline-block u-text-muted u-text-monospace margin-right-fourth">Jul 28, 2020</span>
<span class="u-inline-block">Fix filter layout on small screens</span>
</li>
<li id="5757aad" class="u-text-smaller">
<span class="u-inline-block u-text-muted u-text-monospace margin-right-fourth">Jul 27, 2020</span>
<span class="u-inline-block">Fix typo</span>
</li>
<li id="058b875" class="u-text-smaller">
<span class="u-inline-block u-text-muted u-text-monospace margin-right-fourth">Jul 27, 2020</span>
<span class="u-inline-block">Fix escaping of inline code snippet</span>
</li>
<li id="856d0ea" class="u-text-smaller">
<span class="u-inline-block u-text-muted u-text-monospace margin-right-fourth">Jul 27, 2020</span>
<span class="u-inline-block">Add ideas for the next iteration of the filter UI</span>
</li>
</ul>
</div>
</div>
<div class="ab-webmentions ab-content-wrapper ab-content" id="webmentions">
<div class="u-flex u-ai-center u-flex-wrap u-text-small">
<a class="ab-button u-flex u-ai-center margin-right" href="https://twitter.com/intent/tweet/?text=https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/">Post a comment <svg aria-hidden="true" focusable="false" class="ab-social__icon margin-left-fourth" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg></a>
<span class="ab-comment__info">
If you post a tweet with a link to this page, it will appear here as a <a href="https://indieweb.org/Webmention">webmention</a> (updated daily).
</span>
</div>
<h2 class="h3 margin-top-oneandhalf">Webmentions</h2>
<div class="ab-webmentions__container">
<ol class="ab-webmentions__list margin-top">
<li class="ab-webmentions__item">
<article class="ab-webmention h-cite" id="webmention-832359">
<div class="ab-webmention__meta">
<a class="ab-webmention__author p-author h-card u-url" href="https://twitter.com/BonnerBlogs/status/1287583541405024256" target="_blank" rel="noopener noreferrer">
<img class="ab-webmention__author-photo u-photo lazyload" data-src="https://webmention.io/avatar/pbs.twimg.com/5b79e1c933add508233ae6e3b0aaecace67f4c7758d831a05bd8b682b03ded96.jpg" alt="Bonner Blogs">
<strong class="p-name">Bonner Blogs</strong>
</a>
<time class="ab-webmention__pubdate dt-published" datetime="2020-07-27T03:00:13+00:00">Jul 27, 2020 · 03:00</time>
</div>
<div class="ab-webmention__content p-content">
How to build a filterable list of things <a href="https://twitter.com/search?q=%23Digital">#Digital</a> <a href="https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/">annualbeta.com/blog/how-to-bu…</a>
</div>
</article>
</li>
<li class="ab-webmentions__item">
<article class="ab-webmention h-cite" id="webmention-1375295">
<div class="ab-webmention__meta">
<a class="ab-webmention__author p-author h-card u-url" href="https://tsdigital.ca/how-to-build-a-progressively-enhanced-accessible-filterable-and-paginated-list/" target="_blank" rel="noopener noreferrer">
<img class="ab-webmention__author-photo lazyload" data-src="/img/webmention-avatar-default.svg" alt="">
<strong class="p-name">Admin</strong>
</a>
</div>
<div class="ab-webmention__content p-content">
Most sites I build are static sites with HTML files generated by a static site generator or pages served on a server by a CMS like <a href="https://wordpress.com/">WordPress</a> or <a href="https://craftcms.com/">CraftCMS</a>. I use JavaScript only on top to enhance the user experience. I use it for things like disclosure widgets, accordions, fly-out navigations, or modals.The requirements for most of these features are simple, so using a library or framework would be overkill. Recently, however, I found myself in a situation where writing a component from scratch in Vanilla JS without the help of a framework would’ve been too complicated and messy.Lightweight FrameworksMy task was to add multiple filters, sorting and pagination to an existing list of items. I didn’t want to use a JavaScript Framework like Vue or React, only because I needed help in some places on my site, and I didn’t want to change my stack. <a href="https://twitter.com/mmatuzo/status/1483739260092100609">I consulted Twitter</a>, and people suggested minimal frameworks like <a href="https://lit.dev/">lit</a>, <a href="https://github.com/vuejs/petite-vue">petite-vue</a>, <a href="https://hyperscript.org/">hyperscript</a>, <a href="https://htmx.org/">htmx</a> or <a href="https://alpinejs.dev/">Alpine.js</a>. I went with Alpine because it sounded like it was exactly what I was looking for:“Alpine is a rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web. Plop in a script tag and get going.” Alpine.jsAlpine is a lightweight (~7KB) collection of 15 attributes, 6 properties, and 2 methods. I won’t go into the basics of it (check out this <a href="https://css-tricks.com/alpine-js-the-javascript-framework-thats-used-like-jquery-written-like-vue-and-inspired-by-tailwindcss/">article about Alpine</a> by <a href="https://codewithhugo.com/">Hugo Di Francesco</a> or read the <a href="https://alpinejs.dev/start-here">Alpine docs</a>), but let me quickly introduce you to Alpine:<strong>Note:</strong> <em>You can skip this intro and go straight to the <a href="https://tsdigital.ca/how-to-build-a-progressively-enhanced-accessible-filterable-and-paginated-list/#static-paginated-list">main content of the article</a> if you’re already familiar with Alpine.js.</em>Let’s say we want to turn a simple list with many items into a <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#disclosure">disclosure widget</a>. You could use the native HTML elements: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details">details and summary</a> for that, but for this exercise, I’ll use Alpine.By default, with JavaScript disabled, we show the list, but we want to hide it and allow users to open and close it by pressing a button if JavaScript is enabled:<h2>Beastie Boys Anthology</h2>
<p>The Sounds of Science is the first anthology album by American rap rock group Beastie Boys composed of greatest hits, B-sides, and previously unreleased tracks.</p>
<ol> <li>Beastie Boys</li> <li>Slow And Low</li> <li>Shake Your Rump</li> <li>Gratitude</li> <li>Skills To Pay The Bills</li> <li>Root Down</li> <li>Believe Me</li> …
</ol>First, we include Alpine using a script tag. Then we wrap the list in a div and use the x-data directive to pass data into the component. The open property inside the object we passed is available to all children of the div:<div x-data="{ open: false }"> <ol> <li>Beastie Boys</li> <li>Slow And Low</li> … </ol>
</div> <script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>We can use the open property for the x-show directive, which determines whether or not an element is visible:<div x-data="{ open: false }"> <ol x-show="open"> <li>Beastie Boys</li> <li>Slow And Low</li> … </ol>
</div>Since we set open to false, the list is hidden now.Next, we need a button that toggles the value of the open property. We can add events by using the x-on:click directive or the shorter @-Syntax @click:<div x-data="{ open: false }"> <button @click="open = !open">Tracklist</button> <ol x-show="open"> <li>Beastie Boys</li> <li>Slow And Low</li> … </ol>
</div>Pressing the button, open now switches between false and true and x-show reactively watches these changes, showing and hiding the list accordingly.While this works for keyboard and mouse users, it’s useless to screen reader users, as we need to communicate the state of our widget. We can do that by toggling the value of the aria-expanded attribute:<button @click="open = !open" :aria-expanded="open"> Tracklist
</button>We can also create a semantic connection between the button and the list using aria-controls for <a href="https://a11ysupport.io/tech/aria/aria-controls_attribute">screen readers that support the attribute</a>:<button @click="open = ! open" :aria-expanded="open" aria-controls="tracklist"> Tracklist
</button>
<ol x-show="open" id="tracklist"> …
</ol>Here’s the final result:SetupBefore we get started, let’s set up our site. We need:a project folder for our site,
11ty to generate HTML files,
an input file for our HTML,
a data file that contains the list of records.
On your command line, navigate to the folder where you want to save the project, create a folder, and cd into it:cd Sites # or wherever you want to save the project
mkdir myrecordcollection # pick any name
cd myrecordcollectionThen create a package.json file and <a href="https://www.11ty.dev/docs/getting-started/">install eleventy</a>:npm init -y
npm install @11ty/eleventyNext, create an index.njk file (.njk means this is a Nunjucks file; more about that below) and a folder _data with a records.json:touch index.njk
mkdir _data
touch _data/records.jsonYou don’t have to do all these steps on the command line. You can also create folders and files in any user interface. The final file and folder structure looks like this:Adding Content11ty allows you to write content directly into an HTML file (or Markdown, Nunjucks, and <a href="https://www.11ty.dev/docs/languages/">other template languages</a>). You can even store data in the <a href="https://www.11ty.dev/docs/data-frontmatter/">front matter</a> or in a JSON file. I don’t want to manage hundreds of entries manually, so I’ll store them in the JSON file we just created. Let’s add some data to the file:[ { "artist": "Akne Kid Joe", "title": "Die große Palmöllüge", "year": 2020 }, { "artist": "Bring me the Horizon", "title": "Post Human: Survial Horror", "year": 2020 }, { "artist": "Idles", "title": "Joy as an Act of Resistance", "year": 2018 }, { "artist": "Beastie Boys", "title": "Licensed to Ill", "year": 1986 }, { "artist": "Beastie Boys", "title": "Paul's Boutique", "year": 1989 }, { "artist": "Beastie Boys", "title": "Check Your Head", "year": 1992 }, { "artist": "Beastie Boys", "title": "Ill Communication", "year": 1994 }
]Finally, let’s add a basic HTML structure to the index.njk file and start eleventy:<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Record Collection</title>
</head>
<body> <h1>My Record Collection</h1> </body>
</html>By running the following command you should be able to access the site at http://localhost:8080:eleventy --serveDisplaying ContentNow let’s take the data from our JSON file and turn it into HTML. We can access it by looping over the records object in nunjucks:<div class="collection"> <ol> {% for record in records %} <li> <strong>{{ record.title }}</strong><br> Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}. </li> {% endfor %} </ol>
</div>PaginationEleventy supports pagination out of the box. All we have to do is add a frontmatter block to our page, tell 11ty which dataset it should use for pagination, and finally, we have to adapt our for loop to use the paginated list instead of all records:---
pagination: data: records size: 5
---
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Record Collection</title> </head> <body> <h1>My Record Collection</h1> <div class="collection"> <p id="message">Showing <output>{{ records.length }} records</output></p> <div aria-labelledby="message" role="region"> <ol class="records"> {% for record in pagination.items %} <li> <strong>{{ record.title }}</strong><br> Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}. </li> {% endfor %} </ol> </div> </div> </body>
</html>If you access the page again, the list only contains 5 items. You can also see that I’ve added a status message (ignore the output element for now), wrapped the list in a div with the role “region”, and that I’ve labelled it by creating a reference to #message using aria-labelledby. I did that to turn it into a <a href="https://www.htmhell.dev/tips/landmarks/">landmark</a> and allow screen reader users to access the list of results directly using keyboard shortcuts.Next, we’ll add a navigation with links to all pages created by the static site generator. The pagination object holds an array that contains all pages. We use aria-current="page" to highlight the current page:<nav aria-label="Select a page"> <ol class="pages"> {% for page_entry in pagination.pages %} {%- set page_url = pagination.hrefs[loop.index0] -%} <li> <a href="{{ page_url }}"{% if page.url == page_url %} aria-current="page"{% endif %}> Page {{ loop.index }} </a> </li> {% endfor %} </ol>
</nav>Finally, let’s add some basic CSS to improve the styling:body { font-family: sans-serif; line-height: 1.5;
} ol { list-style: none; margin: 0; padding: 0;
} .records > * + * { margin-top: 2rem;
} h2 { margin-bottom: 0;
} nav { margin-top: 1.5rem;
} .pages { display: flex; flex-wrap: wrap; gap: 0.5rem;
} .pages a { border: 1px solid #000000; padding: 0.5rem; border-radius: 5px; display: flex; text-decoration: none;
} .pages a:where([aria-current]) { background-color: #000000; color: #ffffff;
} .pages a:where(:focus, :hover) { background-color: #6c6c6c; color: #ffffff;
}You can see it in action in the <a href="https://sad-kare-e07051.netlify.app/">live demo</a> and you can check out the <a href="https://github.com/matuzo/articles/tree/main/11ty-alpine">code on GitHub</a>.This works fairly well with 7 records. It might even work with 10, 20, or 50, but I have over 400 records. We can make browsing the list easier by adding filters.A Dynamic Paginated And Filterable ListI like JavaScript, but I also believe that the core content and functionality of a website should be accessible without it. This doesn’t mean that you can’t use JavaScript at all, it just means that you start with a basic server-rendered foundation of your component or site, and you add functionality layer by layer. This is called <a href="https://briefs.video/videos/is-progressive-enhancement-dead-yet/">progressive enhancement</a>.Our foundation in this example is the static list created with 11ty, and now we add a layer of functionality with Alpine.First, right before the closing body tag, we reference the latest version (as of writing 3.9.1) of Alpine.js: <script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>
</body><strong>Note:</strong> <em>Be careful using a third-party CDN, this can have all kinds of negative implications (performance, privacy, security). Consider referencing the file locally or <a href="https://alpinejs.dev/essentials/installation#as-a-module">importing it as a module</a>.In case you’re wondering why you don’t see the <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity">Subresource Integrity</a> hash in the official docs, it’s because I’ve created and added it manually.</em>Since we’re moving into JavaScript-world, we need to make our records available to Alpine.js. Probably not the best, but the quickest solution is to create a .eleventy.js file in your root folder and add the following lines:module.exports = function(eleventyConfig) { eleventyConfig.addPassthroughCopy("_data");
};This ensures that eleventy doesn’t just generate HTML files, but it also copies the contents of the _data folder into our destination folder, making it accessible to our scripts.Fetching DataJust like in the previous example, we’ll add the x-data directive to our component to pass data:<div class="collection" x-data="{ records: [] }">
</div>We don’t have any data, so we need to fetch it as the component initialises. The x-init directive allows us to hook into the initialisation phase of any element and perform tasks:<div class="collection" x-init="records = await (await fetch('/_data/records.json')).json()" x-data="{ records: [] }"> <div x-text="records"></div> […]
</div>If we output the results directly, we see a list of [object Object]s, because we’re fetching and receiving an array. Instead, we should iterate over the list using the x-for directive on a template tag and output the data using x-text:<template x-for="record in records"> <li> <strong x-text="record.title"></strong><br> Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>. </li>
</template>
The <template> HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.
<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template">MDN: <template>: The Content Template Element</a>
Here’s how the whole list looks like now:<div class="collection" x-init="records = await (await fetch('/_data/records.json')).json()" x-data="{ records: [] }"> <p id="message">Showing <output>{{ records.length }} records</output></p> <div aria-labelledby="message" role="region"> <ol class="records"> <template x-for="record in records"> <li> <strong x-text="record.title"></strong><br> Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>. </li> </template> {%- for record in pagination.items %} <li> <strong>{{ record.title }}</strong><br> Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}. </li> {%- endfor %} </ol> </div> […]
</div>Isn’t it amazing how quickly we were able to fetch and output data? Check out the demo below to see how Alpine populates the list with results.<strong>Hint:</strong> <em>You don’t see any Nunjucks code in this CodePen, because 11ty doesn’t run in the browser. I’ve just copied and pasted the rendered HTML of the first page.</em>See the Pen <a href="https://codepen.io/smashingmag/pen/abEWRMY">Pagination + Filter with Alpine.js Step 1</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.You can achieve a lot by using Alpine’s directives, but at some point relying only on attributes can get messy. That’s why I’ve decided to move the data and some of the logic into a separate Alpine component object.Here’s how that works: Instead of passing data directly, we now reference a component using x-data. The rest is pretty much identical: Define a variable to hold our data, then fetch our JSON file in the initialization phase. However, we don’t do that inside an attribute, but inside a script tag or file instead:<div class="collection" x-data="collection"> […]
</div> […] <script> document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); }, init() { this.getRecords(); } })) })
</script> <script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>Looking at the previous CodePen, you’ve probably noticed that we now have a duplicate set of data. That’s because our static 11ty list is still there. Alpine has a directive that tells it to ignore certain DOM elements. I don’t know if this is actually necessary here, but it’s a nice way of marking these <em>unwanted</em> elements. So, we add the x-ignore directive on our 11ty list items, and we add a class to the html element when the data has loaded and then use the class and the attribute to hide those list items in CSS:<style> .alpine [x-ignore] { display: none; }
</style> […]
{%- for record in pagination.items %} <li x-ignore> <strong>{{ record.title }}</strong><br> Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}. </li>
{%- endfor %}
[…]
<script> document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } })) })
</script>11ty data is hidden, results are coming from Alpine, but the pagination is not functional at the moment:See the Pen <a href="https://codepen.io/smashingmag/pen/eYyWQOe">Pagination + Filter with Alpine.js Step 2</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.PaginationBefore we add filters, let’s paginate our data. 11ty did us the favor of handling all the logic for us, but now we have to do it on our own. In order to split our data across multiple pages, we need the following:the number of items per page (itemsPerPage),
the current page (currentPage),
the total number of pages (numOfPages),
a dynamic, paged subset of the whole data (page).
document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], itemsPerPage: 5, currentPage: 0, numOfPages: // total number of pages, page: // paged items async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } }))
})The number of items per page is a fixed value (5), and the current page starts with 0. We get the number of pages by dividing the total number of items by the number of items per page:numOfPages() { return Math.ceil(this.records.length / this.itemsPerPage) // 7 / 5 = 1.4 // Math.ceil(7 / 5) = 2
},The easiest way for me to get the items per page was to use the slice() method in JavaScript and take out the slice of the dataset that I need for the current page:page() { return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage) // this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage // Page 1: 0 * 5, (0 + 1) * 5 (=> slice(0, 5);) // Page 2: 1 * 5, (1 + 1) * 5 (=> slice(5, 10);) // Page 3: 2 * 5, (2 + 1) * 5 (=> slice(10, 15);)
}To only display the items for the current page, we have to adapt the for loop to iterate over page instead of records:<ol class="records"> <template x-for="record in page"> <li> <strong x-text="record.title"></strong><br> Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>. </li> </template>
</ol>We now have a page, but no links that allow us to jump from page to page. Just like earlier, we use the template element and the x-for directive to display our page links:<ol class="pages"> <template x-for="idx in numOfPages"> <li> <a :href="`/${idx}`" x-text="`Page ${idx}`" :aria-current="idx === currentPage + 1 ? 'page' : false" @click.prevent="currentPage = idx - 1"></a> </li> </template> {% for page_entry in pagination.pages %} <li x-ignore> […] </li> {% endfor %}
</ol>Since we don’t want to reload the whole page anymore, we put a click event on each link, prevent the default click behavior, and change the current page number on click:<a href="/" @click.prevent="currentPage = idx - 1"></a>Here’s what that looks like in the browser. (I’ve added more entries to the JSON file. You can <a href="https://raw.githubusercontent.com/matuzo/articles/main/11ty-alpine/_data/records_full.json">download it on GitHub</a>.)See the Pen <a href="https://codepen.io/smashingmag/pen/GRymwjg">Pagination + Filter with Alpine.js Step 3</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.FilteringI want to be able to filter the list by artist and by decade.We add two select elements wrapped in a fieldset to our component, and we put a x-model directive on each of them. x-model allows us to bind the value of an input element to Alpine data:<fieldset class="filters"> <legend>Filter by</legend> <label for="artist">Artist</label> <select id="artist" x-model="filters.artist"> <option value="">All</option> </select> <label for="decade">Decade</label> <select id="decade" x-model="filters.year"> <option value="">All</option> </select>
</fieldset>Of course, we also have to create these data fields in our Alpine component:document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ filters: { year: '', artist: '', }, records: [], itemsPerPage: 5, currentPage: 0, numOfPages() { return Math.ceil(this.records.length / this.itemsPerPage) }, page() { return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage) }, async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } }))
})If we change the selected value in each select, filters.artist and filters.year will update automatically. You can try it here with some dummy data I’ve added manually:See the Pen <a href="https://codepen.io/smashingmag/pen/GGRymwEp">Pagination + Filter with Alpine.js Step 4</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.Now we have select elements, and we’ve bound the data to our component. The next step is to populate each select dynamically with artists and decades respectively. For that we take our records array and manipulate the data a bit:document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ artists: [], decades: [], // […] async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); this.artists = [...new Set(this.records.map(record => record.artist))].sort(); this.decades = [...new Set(this.records.map(record => record.year.toString().slice(0, -1)))].sort(); document.documentElement.classList.add('alpine'); }, // […] }))
})This looks wild, and I’m sure that I’ll forget what’s going on here real soon, but what this code does is that it takes the array of objects and turns it into an array of strings (map()), it makes sure that each entry is unique (that’s what [...new Set()] does here) and sorts the array alphabetically (sort()). For the decade’s array, I’m additionally slicing off the last digit of the year because I don’t want this filter to be too granular. Filtering by decade is good enough.Next, we populate the artist and decade select elements, again using the template element and the x-for directive:<label for="artist">Artist</label>
<select id="artist" x-model="filters.artist"> <option value="">All</option> <template x-for="artist in artists"> <option x-text="artist"></option> </template>
</select> <label for="decade">Decade</label>
<select id="decade" x-model="filters.year"> <option value="">All</option> <template x-for="year in decades"> <option :value="year" x-text="`${year}0`"></option> </template>
</select>Try it yourself in <a href="https://codepen.io/matuzo/pen/KKZwqQe?editors=1010">demo 5 on Codepen</a>.See the Pen <a href="https://codepen.io/smashingmag/pen/OJzmaZb">Pagination + Filter with Alpine.js Step 5</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.We’ve successfully populated the select elements with data from our JSON file. To finally filter the data, we go through all records, we check whether a filter is set. If that’s the case, we check that the respective field of the record corresponds to the selected value of the filter. If not, we filter this record out. We’re left with a filtered array that matches the criteria:get filteredRecords() { const filtered = this.records.filter((item) => { for (var key in this.filters) { if (this.filters[key] === '') { continue } if(!String(item[key]).includes(this.filters[key])) { return false } } return true }); return filtered
}For this to take effect we have to adapt our numOfPages() and page() functions to use only the filtered records:numOfPages() { return Math.ceil(this.filteredRecords.length / this.itemsPerPage)
},
page() { return this.filteredRecords.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)
},See the Pen <a href="https://codepen.io/smashingmag/pen/GRymwQZ">Pagination + Filter with Alpine.js Step 6</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.<strong>Three things left to do:</strong>fix a bug;
hide the form;
update the status message.
Bug Fix: Watching a Component PropertyWhen you open the first page, click on page 6, then select “1990” — you don’t see any results. That’s because our filter thinks that we’re still on page 6, but 1) we’re actually on page 1, and 2) there is no page 6 with “1990” active. We can fix that by resetting the currentPage when the user changes one of the filters. To watch changes in the filter object, we can use a so-called magic method:init() { this.getRecords(); this.$watch('filters', filter => this.currentPage = 0);
}Every time the filter property changes, the currentPage will be set to 0.Hiding the FormSince the filters only work with JavaScript enabled and functioning, we should hide the whole form when that’s not the case. We can use the .alpine class we created earlier for that:<fieldset class="filters" hidden> […]
</fieldset>.filters { display: block;
} html:not(.alpine) .filters { visibility: hidden;
}I’m using visibility: hidden instead of hidden only to avoid content shifting while Alpine is still loading.Communicating ChangesThe status message at the beginning of our list still reads “Showing 7 records”, but this doesn’t change when the user changes the page or filters the list. There are two things we have to do to make the paragraph dynamic: bind data to it and communicate changes to assistive technology (a screen reader, e.g.).First, we bind data to the output element in the paragraph that changes based on the current page and filter:<p id="message">Showing <output x-text="message">{{ records.length }} records</output></p>Alpine.data('collection', () => ({ message() { return `${this.filteredRecords.length} records`; },
// […]Next, we want to communicate to screen readers that the content on the page has changed. There are at least two ways of doing that:We could turn an element into a so-called <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">live region</a> using the aria-live attribute. A live region is an element that announces its content to screen readers every time it changes.<div aria-live="polite">Dynamic changes will be announced</div>
In our case, we don’t have to do anything, because we’re already using the output element (remember?) which is an implicit live region by default.
<p id="message">Showing <output x-text="message">{{ records.length }} records</output></p>
“The <output> HTML element is a container element into which a site or app can inject the results of a calculation or the outcome of a user action.”
Source: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"><output>: The Output Element</a>, MDN Web Docs
We could make the region focusable and move the focus to the region when its content changes. Since the region is labelled, its name and role will be announced when that happens.<div aria-labelledby="message" role="region" tabindex="-1" x-ref="region">
We can reference the region using the x-ref directive.
<a @click.prevent="currentPage = idx - 1; $nextTick(() => { $refs.region.focus(); $refs.region.scrollIntoView(); });" :href="/${idx}" x-text="Page ${idx}" :aria-current="idx === currentPage + 1 ? 'page' : false">
I’ve decided to do both:When users <em>filter</em> the page, we update the live region, but we don’t move focus.
When they <em>change</em> the page, we move focus to the list.
That’s it. Here’s the <a href="https://sad-kare-e07051.netlify.app/index_js/">final result</a>:See the Pen <a href="https://codepen.io/smashingmag/pen/zYpwMXX">Pagination + Filter with Alpine.js Step 7</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.<strong>Note:</strong> <em>When you filter by artist, and the status message shows “1 records”, and you filter again by another artist, also with just one record, the content of the output element doesn’t change, and nothing is reported to screen readers. This can be seen as a bug or as a feature to reduce redundant announcements. You’ll have to test this with users.</em>What’s Next?What I did here might seem redundant, but if you’re like me, and you don’t have enough trust in JavaScript, it’s worth the effort. And if you look at the <a href="https://codepen.io/matuzo/pen/GRygvwY?editors=1100">final CodePen</a> or the <a href="https://github.com/matuzo/articles/tree/main/11ty-alpine">complete code on GitHub</a>, it actually wasn’t that much extra work. Minimal frameworks like Alpine.js make it really easy to progressively enhance static components and make them reactive.I’m pretty happy with the result, but there are a few more things that could be improved:The pagination could be smarter (maximum number of pages, previous and next links, and so on).
Let users pick the number of items per page.
Sorting would be a nice feature.
Working with the history API would be great.
Content shifting can be improved.
The solution needs user testing and browser/screen reader testing.
<strong>P.S.</strong> <em>Yes, I know, Alpine produces invalid HTML with its custom x- attribute syntax. That hurts me as much as it hurts you, but as long as it doesn’t affect users, I can live with that. 🙂</em><strong>P.S.S.</strong> <em>Special thanks to Scott, Søren, Thain, David, Saptak and Christian for their feedback.</em>Further Resources“<a href="https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/">How To Build A Filterable List Of Things</a>”, Søren Birkemeyer
“<a href="https://www.scottohara.me/blog/2022/02/05/dynamic-results.html">Considering Dynamic Search Results And Content</a>”, Scott O’Hara
</div>
</article>
</li>
<li class="ab-webmentions__item">
<article class="ab-webmention h-cite" id="webmention-1375130">
<div class="ab-webmention__meta">
<a class="ab-webmention__author p-author h-card u-url" href="https://www.splendidwebsites.com/how-to-build-a-progressively-enhanced-accessible-filterable-and-paginated-list/" target="_blank" rel="noopener noreferrer">
<img class="ab-webmention__author-photo lazyload" data-src="/img/webmention-avatar-default.svg" alt="">
<strong class="p-name">stiggerr</strong>
</a>
<time class="ab-webmention__pubdate dt-published" datetime="2022-04-04T00:00:00">Apr 04, 2022 · 00:00</time>
</div>
<div class="ab-webmention__content p-content">
Most sites I build are static sites with HTML files generated by a static site generator or pages served on a server by a CMS like <a href="https://wordpress.com/">WordPress</a> or <a href="https://craftcms.com/">CraftCMS</a>. I use JavaScript only on top to enhance the user experience. I use it for things like disclosure widgets, accordions, fly-out navigations, or modals.
The requirements for most of these features are simple, so using a library or framework would be overkill. Recently, however, I found myself in a situation where writing a component from scratch in Vanilla JS without the help of a framework would’ve been too complicated and messy.
Lightweight Frameworks
My task was to add multiple filters, sorting and pagination to an existing list of items. I didn’t want to use a JavaScript Framework like Vue or React, only because I needed help in some places on my site, and I didn’t want to change my stack. <a href="https://twitter.com/mmatuzo/status/1483739260092100609">I consulted Twitter</a>, and people suggested minimal frameworks like <a href="https://lit.dev/">lit</a>, <a href="https://github.com/vuejs/petite-vue">petite-vue</a>, <a href="https://hyperscript.org/">hyperscript</a>, <a href="https://htmx.org/">htmx</a> or <a href="https://alpinejs.dev/">Alpine.js</a>. I went with Alpine because it sounded like it was exactly what I was looking for:
“Alpine is a rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web. Plop in a script tag and get going.”
Alpine.js
Alpine is a lightweight (~7KB) collection of 15 attributes, 6 properties, and 2 methods. I won’t go into the basics of it (check out this <a href="https://css-tricks.com/alpine-js-the-javascript-framework-thats-used-like-jquery-written-like-vue-and-inspired-by-tailwindcss/">article about Alpine</a> by <a href="https://codewithhugo.com/">Hugo Di Francesco</a> or read the <a href="https://alpinejs.dev/start-here">Alpine docs</a>), but let me quickly introduce you to Alpine:
<strong>Note:</strong> <em>You can skip this intro and go straight to the <a href="https://www.splendidwebsites.com/how-to-build-a-progressively-enhanced-accessible-filterable-and-paginated-list/#static-paginated-list">main content of the article</a> if you’re already familiar with Alpine.js.</em>
Let’s say we want to turn a simple list with many items into a <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#disclosure">disclosure widget</a>. You could use the native HTML elements: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details">details and summary</a> for that, but for this exercise, I’ll use Alpine.
By default, with JavaScript disabled, we show the list, but we want to hide it and allow users to open and close it by pressing a button if JavaScript is enabled:
<h2>Beastie Boys Anthology</h2>
<p>The Sounds of Science is the first anthology album by American rap rock group Beastie Boys composed of greatest hits, B-sides, and previously unreleased tracks.</p>
<ol> <li>Beastie Boys</li> <li>Slow And Low</li> <li>Shake Your Rump</li> <li>Gratitude</li> <li>Skills To Pay The Bills</li> <li>Root Down</li> <li>Believe Me</li> …
</ol>
First, we include Alpine using a script tag. Then we wrap the list in a div and use the x-data directive to pass data into the component. The open property inside the object we passed is available to all children of the div:
<div x-data="{ open: false }"> <ol> <li>Beastie Boys</li> <li>Slow And Low</li> … </ol>
</div> <script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>
We can use the open property for the x-show directive, which determines whether or not an element is visible:
<div x-data="{ open: false }"> <ol x-show="open"> <li>Beastie Boys</li> <li>Slow And Low</li> … </ol>
</div>
Since we set open to false, the list is hidden now.
Next, we need a button that toggles the value of the open property. We can add events by using the x-on:click directive or the shorter @-Syntax @click:
<div x-data="{ open: false }"> <button @click="open = !open">Tracklist</button> <ol x-show="open"> <li>Beastie Boys</li> <li>Slow And Low</li> … </ol>
</div>
Pressing the button, open now switches between false and true and x-show reactively watches these changes, showing and hiding the list accordingly.
While this works for keyboard and mouse users, it’s useless to screen reader users, as we need to communicate the state of our widget. We can do that by toggling the value of the aria-expanded attribute:
<button @click="open = !open" :aria-expanded="open"> Tracklist
</button>
We can also create a semantic connection between the button and the list using aria-controls for <a href="https://a11ysupport.io/tech/aria/aria-controls_attribute">screen readers that support the attribute</a>:
<button @click="open = ! open" :aria-expanded="open" aria-controls="tracklist"> Tracklist
</button>
<ol x-show="open" id="tracklist"> …
</ol>
Here’s the final result:
Setup
Before we get started, let’s set up our site. We need:
a project folder for our site,
11ty to generate HTML files,
an input file for our HTML,
a data file that contains the list of records.
On your command line, navigate to the folder where you want to save the project, create a folder, and cd into it:
cd Sites # or wherever you want to save the project
mkdir myrecordcollection # pick any name
cd myrecordcollection
Then create a package.json file and <a href="https://www.11ty.dev/docs/getting-started/">install eleventy</a>:
npm init -y
npm install @11ty/eleventy
Next, create an index.njk file (.njk means this is a Nunjucks file; more about that below) and a folder _data with a records.json:
touch index.njk
mkdir _data
touch _data/records.json
You don’t have to do all these steps on the command line. You can also create folders and files in any user interface. The final file and folder structure looks like this:
Adding Content
11ty allows you to write content directly into an HTML file (or Markdown, Nunjucks, and <a href="https://www.11ty.dev/docs/languages/">other template languages</a>). You can even store data in the <a href="https://www.11ty.dev/docs/data-frontmatter/">front matter</a> or in a JSON file. I don’t want to manage hundreds of entries manually, so I’ll store them in the JSON file we just created. Let’s add some data to the file:
[ { "artist": "Akne Kid Joe", "title": "Die große Palmöllüge", "year": 2020 }, { "artist": "Bring me the Horizon", "title": "Post Human: Survial Horror", "year": 2020 }, { "artist": "Idles", "title": "Joy as an Act of Resistance", "year": 2018 }, { "artist": "Beastie Boys", "title": "Licensed to Ill", "year": 1986 }, { "artist": "Beastie Boys", "title": "Paul's Boutique", "year": 1989 }, { "artist": "Beastie Boys", "title": "Check Your Head", "year": 1992 }, { "artist": "Beastie Boys", "title": "Ill Communication", "year": 1994 }
]
Finally, let’s add a basic HTML structure to the index.njk file and start eleventy:
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Record Collection</title>
</head>
<body> <h1>My Record Collection</h1> </body>
</html>
By running the following command you should be able to access the site at http://localhost:8080:
eleventy --serve
Displaying Content
Now let’s take the data from our JSON file and turn it into HTML. We can access it by looping over the records object in nunjucks:
<div class="collection"> <ol> {% for record in records %} <li> <strong>{{ record.title }}</strong><br> Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}. </li> {% endfor %} </ol>
</div>
Pagination
Eleventy supports pagination out of the box. All we have to do is add a frontmatter block to our page, tell 11ty which dataset it should use for pagination, and finally, we have to adapt our for loop to use the paginated list instead of all records:
---
pagination: data: records size: 5
---
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Record Collection</title> </head> <body> <h1>My Record Collection</h1> <div class="collection"> <p id="message">Showing <output>{{ records.length }} records</output></p> <div aria-labelledby="message" role="region"> <ol class="records"> {% for record in pagination.items %} <li> <strong>{{ record.title }}</strong><br> Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}. </li> {% endfor %} </ol> </div> </div> </body>
</html>
If you access the page again, the list only contains 5 items. You can also see that I’ve added a status message (ignore the output element for now), wrapped the list in a div with the role “region”, and that I’ve labelled it by creating a reference to #message using aria-labelledby. I did that to turn it into a <a href="https://www.htmhell.dev/tips/landmarks/">landmark</a> and allow screen reader users to access the list of results directly using keyboard shortcuts.
Next, we’ll add a navigation with links to all pages created by the static site generator. The pagination object holds an array that contains all pages. We use aria-current="page" to highlight the current page:
<nav aria-label="Select a page"> <ol class="pages"> {% for page_entry in pagination.pages %} {%- set page_url = pagination.hrefs[loop.index0] -%} <li> <a href="{{ page_url }}"{% if page.url == page_url %} aria-current="page"{% endif %}> Page {{ loop.index }} </a> </li> {% endfor %} </ol>
</nav>
Finally, let’s add some basic CSS to improve the styling:
body { font-family: sans-serif; line-height: 1.5;
} ol { list-style: none; margin: 0; padding: 0;
} .records > * + * { margin-top: 2rem;
} h2 { margin-bottom: 0;
} nav { margin-top: 1.5rem;
} .pages { display: flex; flex-wrap: wrap; gap: 0.5rem;
} .pages a { border: 1px solid #000000; padding: 0.5rem; border-radius: 5px; display: flex; text-decoration: none;
} .pages a:where([aria-current]) { background-color: #000000; color: #ffffff;
} .pages a:where(:focus, :hover) { background-color: #6c6c6c; color: #ffffff;
}
You can see it in action in the <a href="https://sad-kare-e07051.netlify.app/">live demo</a> and you can check out the <a href="https://github.com/matuzo/articles/tree/main/11ty-alpine">code on GitHub</a>.
This works fairly well with 7 records. It might even work with 10, 20, or 50, but I have over 400 records. We can make browsing the list easier by adding filters.
A Dynamic Paginated And Filterable List
I like JavaScript, but I also believe that the core content and functionality of a website should be accessible without it. This doesn’t mean that you can’t use JavaScript at all, it just means that you start with a basic server-rendered foundation of your component or site, and you add functionality layer by layer. This is called <a href="https://briefs.video/videos/is-progressive-enhancement-dead-yet/">progressive enhancement</a>.
Our foundation in this example is the static list created with 11ty, and now we add a layer of functionality with Alpine.
First, right before the closing body tag, we reference the latest version (as of writing 3.9.1) of Alpine.js:
<script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>
</body>
<strong>Note:</strong> <em>Be careful using a third-party CDN, this can have all kinds of negative implications (performance, privacy, security). Consider referencing the file locally or <a href="https://alpinejs.dev/essentials/installation#as-a-module">importing it as a module</a>.In case you’re wondering why you don’t see the <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity">Subresource Integrity</a> hash in the official docs, it’s because I’ve created and added it manually.</em>
Since we’re moving into JavaScript-world, we need to make our records available to Alpine.js. Probably not the best, but the quickest solution is to create a .eleventy.js file in your root folder and add the following lines:
module.exports = function(eleventyConfig) { eleventyConfig.addPassthroughCopy("_data");
};
This ensures that eleventy doesn’t just generate HTML files, but it also copies the contents of the _data folder into our destination folder, making it accessible to our scripts.
Fetching Data
Just like in the previous example, we’ll add the x-data directive to our component to pass data:
<div class="collection" x-data="{ records: [] }">
</div>
We don’t have any data, so we need to fetch it as the component initialises. The x-init directive allows us to hook into the initialisation phase of any element and perform tasks:
<div class="collection" x-init="records = await (await fetch('/_data/records.json')).json()" x-data="{ records: [] }"> <div x-text="records"></div> […]
</div>
If we output the results directly, we see a list of [object Object]s, because we’re fetching and receiving an array. Instead, we should iterate over the list using the x-for directive on a template tag and output the data using x-text:
<template x-for="record in records"> <li> <strong x-text="record.title"></strong><br> Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>. </li>
</template>
The <template> HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.
<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template">MDN: <template>: The Content Template Element</a>
Here’s how the whole list looks like now:
<div class="collection" x-init="records = await (await fetch('/_data/records.json')).json()" x-data="{ records: [] }"> <p id="message">Showing <output>{{ records.length }} records</output></p> <div aria-labelledby="message" role="region"> <ol class="records"> <template x-for="record in records"> <li> <strong x-text="record.title"></strong><br> Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>. </li> </template> {%- for record in pagination.items %} <li> <strong>{{ record.title }}</strong><br> Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}. </li> {%- endfor %} </ol> </div> […]
</div>
Isn’t it amazing how quickly we were able to fetch and output data? Check out the demo below to see how Alpine populates the list with results.
<strong>Hint:</strong> <em>You don’t see any Nunjucks code in this CodePen, because 11ty doesn’t run in the browser. I’ve just copied and pasted the rendered HTML of the first page.</em>
See the Pen <a href="https://codepen.io/smashingmag/pen/abEWRMY">Pagination + Filter with Alpine.js Step 1</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
You can achieve a lot by using Alpine’s directives, but at some point relying only on attributes can get messy. That’s why I’ve decided to move the data and some of the logic into a separate Alpine component object.
Here’s how that works: Instead of passing data directly, we now reference a component using x-data. The rest is pretty much identical: Define a variable to hold our data, then fetch our JSON file in the initialization phase. However, we don’t do that inside an attribute, but inside a script tag or file instead:
<div class="collection" x-data="collection"> […]
</div> […] <script> document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); }, init() { this.getRecords(); } })) })
</script> <script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>
Looking at the previous CodePen, you’ve probably noticed that we now have a duplicate set of data. That’s because our static 11ty list is still there. Alpine has a directive that tells it to ignore certain DOM elements. I don’t know if this is actually necessary here, but it’s a nice way of marking these <em>unwanted</em> elements. So, we add the x-ignore directive on our 11ty list items, and we add a class to the html element when the data has loaded and then use the class and the attribute to hide those list items in CSS:
<style> .alpine [x-ignore] { display: none; }
</style> […]
{%- for record in pagination.items %} <li x-ignore> <strong>{{ record.title }}</strong><br> Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}. </li>
{%- endfor %}
[…]
<script> document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } })) })
</script>
11ty data is hidden, results are coming from Alpine, but the pagination is not functional at the moment:
See the Pen <a href="https://codepen.io/smashingmag/pen/eYyWQOe">Pagination + Filter with Alpine.js Step 2</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Pagination
Before we add filters, let’s paginate our data. 11ty did us the favor of handling all the logic for us, but now we have to do it on our own. In order to split our data across multiple pages, we need the following:
the number of items per page (itemsPerPage),
the current page (currentPage),
the total number of pages (numOfPages),
a dynamic, paged subset of the whole data (page).
document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ records: [], itemsPerPage: 5, currentPage: 0, numOfPages: // total number of pages, page: // paged items async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } }))
})
The number of items per page is a fixed value (5), and the current page starts with 0. We get the number of pages by dividing the total number of items by the number of items per page:
numOfPages() { return Math.ceil(this.records.length / this.itemsPerPage) // 7 / 5 = 1.4 // Math.ceil(7 / 5) = 2
},
The easiest way for me to get the items per page was to use the slice() method in JavaScript and take out the slice of the dataset that I need for the current page:
page() { return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage) // this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage // Page 1: 0 * 5, (0 + 1) * 5 (=> slice(0, 5);) // Page 2: 1 * 5, (1 + 1) * 5 (=> slice(5, 10);) // Page 3: 2 * 5, (2 + 1) * 5 (=> slice(10, 15);)
}
To only display the items for the current page, we have to adapt the for loop to iterate over page instead of records:
<ol class="records"> <template x-for="record in page"> <li> <strong x-text="record.title"></strong><br> Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>. </li> </template>
</ol>
We now have a page, but no links that allow us to jump from page to page. Just like earlier, we use the template element and the x-for directive to display our page links:
<ol class="pages"> <template x-for="idx in numOfPages"> <li> <a :href="`/${idx}`" x-text="`Page ${idx}`" :aria-current="idx === currentPage + 1 ? 'page' : false" @click.prevent="currentPage = idx - 1"></a> </li> </template> {% for page_entry in pagination.pages %} <li x-ignore> […] </li> {% endfor %}
</ol>
Since we don’t want to reload the whole page anymore, we put a click event on each link, prevent the default click behavior, and change the current page number on click:
<a href="/" @click.prevent="currentPage = idx - 1"></a>
Here’s what that looks like in the browser. (I’ve added more entries to the JSON file. You can <a href="https://raw.githubusercontent.com/matuzo/articles/main/11ty-alpine/_data/records_full.json">download it on GitHub</a>.)
See the Pen <a href="https://codepen.io/smashingmag/pen/GRymwjg">Pagination + Filter with Alpine.js Step 3</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Filtering
I want to be able to filter the list by artist and by decade.
We add two select elements wrapped in a fieldset to our component, and we put a x-model directive on each of them. x-model allows us to bind the value of an input element to Alpine data:
<fieldset class="filters"> <legend>Filter by</legend> <label for="artist">Artist</label> <select id="artist" x-model="filters.artist"> <option value="">All</option> </select> <label for="decade">Decade</label> <select id="decade" x-model="filters.year"> <option value="">All</option> </select>
</fieldset>
Of course, we also have to create these data fields in our Alpine component:
document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ filters: { year: '', artist: '', }, records: [], itemsPerPage: 5, currentPage: 0, numOfPages() { return Math.ceil(this.records.length / this.itemsPerPage) }, page() { return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage) }, async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } }))
})
If we change the selected value in each select, filters.artist and filters.year will update automatically. You can try it here with some dummy data I’ve added manually:
See the Pen <a href="https://codepen.io/smashingmag/pen/GGRymwEp">Pagination + Filter with Alpine.js Step 4</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Now we have select elements, and we’ve bound the data to our component. The next step is to populate each select dynamically with artists and decades respectively. For that we take our records array and manipulate the data a bit:
document.addEventListener('alpine:init', () => { Alpine.data('collection', () => ({ artists: [], decades: [], // […] async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); this.artists = [...new Set(this.records.map(record => record.artist))].sort(); this.decades = [...new Set(this.records.map(record => record.year.toString().slice(0, -1)))].sort(); document.documentElement.classList.add('alpine'); }, // […] }))
})
This looks wild, and I’m sure that I’ll forget what’s going on here real soon, but what this code does is that it takes the array of objects and turns it into an array of strings (map()), it makes sure that each entry is unique (that’s what [...new Set()] does here) and sorts the array alphabetically (sort()). For the decade’s array, I’m additionally slicing off the last digit of the year because I don’t want this filter to be too granular. Filtering by decade is good enough.
Next, we populate the artist and decade select elements, again using the template element and the x-for directive:
<label for="artist">Artist</label>
<select id="artist" x-model="filters.artist"> <option value="">All</option> <template x-for="artist in artists"> <option x-text="artist"></option> </template>
</select> <label for="decade">Decade</label>
<select id="decade" x-model="filters.year"> <option value="">All</option> <template x-for="year in decades"> <option :value="year" x-text="`${year}0`"></option> </template>
</select>
Try it yourself in <a href="https://codepen.io/matuzo/pen/KKZwqQe?editors=1010">demo 5 on Codepen</a>.
See the Pen <a href="https://codepen.io/smashingmag/pen/OJzmaZb">Pagination + Filter with Alpine.js Step 5</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
We’ve successfully populated the select elements with data from our JSON file. To finally filter the data, we go through all records, we check whether a filter is set. If that’s the case, we check that the respective field of the record corresponds to the selected value of the filter. If not, we filter this record out. We’re left with a filtered array that matches the criteria:
get filteredRecords() { const filtered = this.records.filter((item) => { for (var key in this.filters) { if (this.filters[key] === '') { continue } if(!String(item[key]).includes(this.filters[key])) { return false } } return true }); return filtered
}
For this to take effect we have to adapt our numOfPages() and page() functions to use only the filtered records:
numOfPages() { return Math.ceil(this.filteredRecords.length / this.itemsPerPage)
},
page() { return this.filteredRecords.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)
},
See the Pen <a href="https://codepen.io/smashingmag/pen/GRymwQZ">Pagination + Filter with Alpine.js Step 6</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
<strong>Three things left to do:</strong>
fix a bug;
hide the form;
update the status message.
Bug Fix: Watching a Component Property
When you open the first page, click on page 6, then select “1990” — you don’t see any results. That’s because our filter thinks that we’re still on page 6, but 1) we’re actually on page 1, and 2) there is no page 6 with “1990” active. We can fix that by resetting the currentPage when the user changes one of the filters. To watch changes in the filter object, we can use a so-called magic method:
init() { this.getRecords(); this.$watch('filters', filter => this.currentPage = 0);
}
Every time the filter property changes, the currentPage will be set to 0.
Hiding the Form
Since the filters only work with JavaScript enabled and functioning, we should hide the whole form when that’s not the case. We can use the .alpine class we created earlier for that:
<fieldset class="filters" hidden> […]
</fieldset>
.filters { display: block;
} html:not(.alpine) .filters { visibility: hidden;
}
I’m using visibility: hidden instead of hidden only to avoid content shifting while Alpine is still loading.
Communicating Changes
The status message at the beginning of our list still reads “Showing 7 records”, but this doesn’t change when the user changes the page or filters the list. There are two things we have to do to make the paragraph dynamic: bind data to it and communicate changes to assistive technology (a screen reader, e.g.).
First, we bind data to the output element in the paragraph that changes based on the current page and filter:
<p id="message">Showing <output x-text="message">{{ records.length }} records</output></p>
Alpine.data('collection', () => ({ message() { return `${this.filteredRecords.length} records`; },
// […]
Next, we want to communicate to screen readers that the content on the page has changed. There are at least two ways of doing that:
We could turn an element into a so-called <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">live region</a> using the aria-live attribute. A live region is an element that announces its content to screen readers every time it changes.<div aria-live="polite">Dynamic changes will be announced</div>
In our case, we don’t have to do anything, because we’re already using the output element (remember?) which is an implicit live region by default.
<p id="message">Showing <output x-text="message">{{ records.length }} records</output></p>
“The <output> HTML element is a container element into which a site or app can inject the results of a calculation or the outcome of a user action.”
Source: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"><output>: The Output Element</a>, MDN Web Docs
We could make the region focusable and move the focus to the region when its content changes. Since the region is labelled, its name and role will be announced when that happens.
<div aria-labelledby="message" role="region" tabindex="-1" x-ref="region">
We can reference the region using the x-ref directive.
<a @click.prevent="currentPage = idx - 1; $nextTick(() => { $refs.region.focus(); $refs.region.scrollIntoView(); });" :href="/${idx}" x-text="Page ${idx}" :aria-current="idx === currentPage + 1 ? 'page' : false">
I’ve decided to do both:
When users <em>filter</em> the page, we update the live region, but we don’t move focus.
When they <em>change</em> the page, we move focus to the list.
That’s it. Here’s the <a href="https://sad-kare-e07051.netlify.app/index_js/">final result</a>:
See the Pen <a href="https://codepen.io/smashingmag/pen/zYpwMXX">Pagination + Filter with Alpine.js Step 7</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
<strong>Note:</strong> <em>When you filter by artist, and the status message shows “1 records”, and you filter again by another artist, also with just one record, the content of the output element doesn’t change, and nothing is reported to screen readers. This can be seen as a bug or as a feature to reduce redundant announcements. You’ll have to test this with users.</em>
What’s Next?
What I did here might seem redundant, but if you’re like me, and you don’t have enough trust in JavaScript, it’s worth the effort. And if you look at the <a href="https://codepen.io/matuzo/pen/GRygvwY?editors=1100">final CodePen</a> or the <a href="https://github.com/matuzo/articles/tree/main/11ty-alpine">complete code on GitHub</a>, it actually wasn’t that much extra work. Minimal frameworks like Alpine.js make it really easy to progressively enhance static components and make them reactive.
I’m pretty happy with the result, but there are a few more things that could be improved:
The pagination could be smarter (maximum number of pages, previous and next links, and so on).
Let users pick the number of items per page.
Sorting would be a nice feature.
Working with the history API would be great.
Content shifting can be improved.
The solution needs user testing and browser/screen reader testing.
<strong>P.S.</strong> <em>Yes, I know, Alpine produces invalid HTML with its custom x- attribute syntax. That hurts me as much as it hurts you, but as long as it doesn’t affect users, I can live with that. 🙂</em>
<strong>P.S.S.</strong> <em>Special thanks to Scott, Søren, Thain, David, Saptak and Christian for their feedback.</em>
Further Resources
“<a href="https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/">How To Build A Filterable List Of Things</a>”, Søren Birkemeyer
“<a href="https://www.scottohara.me/blog/2022/02/05/dynamic-results.html">Considering Dynamic Search Results And Content</a>”, Scott O’Hara
</div>
</article>
</li>
<li class="ab-webmentions__item">
<article class="ab-webmention h-cite" id="webmention-1374975">
<div class="ab-webmention__meta">
<a class="ab-webmention__author p-author h-card u-url" href="https://h4host.com/index.php/2022/04/04/how-to-build-a-progressively-enhanced-accessible-filterable-and-paginated-list/top-web-development-news/admin/" target="_blank" rel="noopener noreferrer">
<img class="ab-webmention__author-photo lazyload" data-src="/img/webmention-avatar-default.svg" alt="">
<strong class="p-name">admin</strong>
</a>
<time class="ab-webmention__pubdate dt-published" datetime="2022-04-04T13:00:08+00:00">Apr 04, 2022 · 13:00</time>
</div>
<div class="ab-webmention__content p-content">
Most sites I build are static sites with HTML files generated by a static site generator or pages served on a server by a CMS like <a href="https://wordpress.com/">WordPress</a> or <a href="https://craftcms.com/">CraftCMS</a>. I use JavaScript only on top to enhance the user experience. I use it for things like disclosure widgets, accordions, fly-out navigations, or modals.
The requirements for most of these features are simple, so using a library or framework would be overkill. Recently, however, I found myself in a situation where writing a component from scratch in Vanilla JS without the help of a framework would’ve been too complicated and messy.
Lightweight Frameworks
My task was to add multiple filters, sorting and pagination to an existing list of items. I didn’t want to use a JavaScript Framework like Vue or React, only because I needed help in some places on my site, and I didn’t want to change my stack. <a href="https://twitter.com/mmatuzo/status/1483739260092100609">I consulted Twitter</a>, and people suggested minimal frameworks like <a href="https://lit.dev/">lit</a>, <a href="https://github.com/vuejs/petite-vue">petite-vue</a>, <a href="https://hyperscript.org/">hyperscript</a>, <a href="https://htmx.org/">htmx</a> or <a href="https://alpinejs.dev/">Alpine.js</a>. I went with Alpine because it sounded like it was exactly what I was looking for:
“Alpine is a rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web. Plop in a script tag and get going.”
Alpine.js
Alpine is a lightweight (~7KB) collection of 15 attributes, 6 properties, and 2 methods. I won’t go into the basics of it (check out this <a href="https://css-tricks.com/alpine-js-the-javascript-framework-thats-used-like-jquery-written-like-vue-and-inspired-by-tailwindcss/">article about Alpine</a> by <a href="https://codewithhugo.com/">Hugo Di Francesco</a> or read the <a href="https://alpinejs.dev/start-here">Alpine docs</a>), but let me quickly introduce you to Alpine:
<strong>Note:</strong> <em>You can skip this intro and go straight to the <a href="https://www.smashingmagazine.com/#static-paginated-list">main content of the article</a> if you’re already familiar with Alpine.js.</em>
Let’s say we want to turn a simple list with many items into a <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#disclosure">disclosure widget</a>. You could use the native HTML elements: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details">details and summary</a> for that, but for this exercise, I’ll use Alpine.
By default, with JavaScript disabled, we show the list, but we want to hide it and allow users to open and close it by pressing a button if JavaScript is enabled:
<h2>Beastie Boys Anthology</h2>
<p>The Sounds of Science is the first anthology album by American rap rock group Beastie Boys composed of greatest hits, B-sides, and previously unreleased tracks.</p>
<ol>
<li>Beastie Boys</li>
<li>Slow And Low</li>
<li>Shake Your Rump</li>
<li>Gratitude</li>
<li>Skills To Pay The Bills</li>
<li>Root Down</li>
<li>Believe Me</li>
…
</ol>
First, we include Alpine using a script tag. Then we wrap the list in a div and use the x-data directive to pass data into the component. The open property inside the object we passed is available to all children of the div:
<div x-data="{ open: false }">
<ol>
<li>Beastie Boys</li>
<li>Slow And Low</li>
…
</ol>
</div>
<script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>
We can use the open property for the x-show directive, which determines whether or not an element is visible:
<div x-data="{ open: false }">
<ol x-show="open">
<li>Beastie Boys</li>
<li>Slow And Low</li>
…
</ol>
</div>
Since we set open to false, the list is hidden now.
Next, we need a button that toggles the value of the open property. We can add events by using the x-on:click directive or the shorter @-Syntax @click:
<div x-data="{ open: false }">
<button @click="open = !open">Tracklist</button>
<ol x-show="open">
<li>Beastie Boys</li>
<li>Slow And Low</li>
…
</ol>
</div>
Pressing the button, open now switches between false and true and x-show reactively watches these changes, showing and hiding the list accordingly.
While this works for keyboard and mouse users, it’s useless to screen reader users, as we need to communicate the state of our widget. We can do that by toggling the value of the aria-expanded attribute:
<button @click="open = !open" :aria-expanded="open">
Tracklist
</button>
We can also create a semantic connection between the button and the list using aria-controls for <a href="https://a11ysupport.io/tech/aria/aria-controls_attribute">screen readers that support the attribute</a>:
<button @click="open = ! open" :aria-expanded="open" aria-controls="tracklist">
Tracklist
</button>
<ol x-show="open" id="tracklist">
…
</ol>
Here’s the final result:
Setup
Before we get started, let’s set up our site. We need:
a project folder for our site,
11ty to generate HTML files,
an input file for our HTML,
a data file that contains the list of records.
On your command line, navigate to the folder where you want to save the project, create a folder, and cd into it:
cd Sites # or wherever you want to save the project
mkdir myrecordcollection # pick any name
cd myrecordcollection
Then create a package.json file and <a href="https://www.11ty.dev/docs/getting-started/">install eleventy</a>:
npm init -y
npm install @11ty/eleventy
Next, create an index.njk file (.njk means this is a Nunjucks file; more about that below) and a folder _data with a records.json:
touch index.njk
mkdir _data
touch _data/records.json
You don’t have to do all these steps on the command line. You can also create folders and files in any user interface. The final file and folder structure looks like this:
Adding Content
11ty allows you to write content directly into an HTML file (or Markdown, Nunjucks, and <a href="https://www.11ty.dev/docs/languages/">other template languages</a>). You can even store data in the <a href="https://www.11ty.dev/docs/data-frontmatter/">front matter</a> or in a JSON file. I don’t want to manage hundreds of entries manually, so I’ll store them in the JSON file we just created. Let’s add some data to the file:
[
{
"artist": "Akne Kid Joe",
"title": "Die große Palmöllüge",
"year": 2020
},
{
"artist": "Bring me the Horizon",
"title": "Post Human: Survial Horror",
"year": 2020
},
{
"artist": "Idles",
"title": "Joy as an Act of Resistance",
"year": 2018
},
{
"artist": "Beastie Boys",
"title": "Licensed to Ill",
"year": 1986
},
{
"artist": "Beastie Boys",
"title": "Paul's Boutique",
"year": 1989
},
{
"artist": "Beastie Boys",
"title": "Check Your Head",
"year": 1992
},
{
"artist": "Beastie Boys",
"title": "Ill Communication",
"year": 1994
}
]
Finally, let’s add a basic HTML structure to the index.njk file and start eleventy:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Record Collection</title>
</head>
<body>
<h1>My Record Collection</h1>
</body>
</html>
By running the following command you should be able to access the site at http://localhost:8080:
eleventy --serve
Displaying Content
Now let’s take the data from our JSON file and turn it into HTML. We can access it by looping over the records object in nunjucks:
<div class="collection">
<ol>
{% for record in records %}
<li>
<strong>{{ record.title }}</strong><br>
Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}.
</li>
{% endfor %}
</ol>
</div>
Pagination
Eleventy supports pagination out of the box. All we have to do is add a frontmatter block to our page, tell 11ty which dataset it should use for pagination, and finally, we have to adapt our for loop to use the paginated list instead of all records:
---
pagination:
data: records
size: 5
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Record Collection</title>
</head>
<body>
<h1>My Record Collection</h1>
<div class="collection">
<p id="message">Showing <output>{{ records.length }} records</output></p>
<div aria-labelledby="message" role="region">
<ol class="records">
{% for record in pagination.items %}
<li>
<strong>{{ record.title }}</strong><br>
Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}.
</li>
{% endfor %}
</ol>
</div>
</div>
</body>
</html>
If you access the page again, the list only contains 5 items. You can also see that I’ve added a status message (ignore the output element for now), wrapped the list in a div with the role “region”, and that I’ve labelled it by creating a reference to #message using aria-labelledby. I did that to turn it into a <a href="https://www.htmhell.dev/tips/landmarks/">landmark</a> and allow screen reader users to access the list of results directly using keyboard shortcuts.
Next, we’ll add a navigation with links to all pages created by the static site generator. The pagination object holds an array that contains all pages. We use aria-current="page" to highlight the current page:
<nav aria-label="Select a page">
<ol class="pages">
{% for page_entry in pagination.pages %}
{%- set page_url = pagination.hrefs[loop.index0] -%}
<li>
<a href="{{ page_url }}"{% if page.url == page_url %} aria-current="page"{% endif %}>
Page {{ loop.index }}
</a>
</li>
{% endfor %}
</ol>
</nav>
Finally, let’s add some basic CSS to improve the styling:
body {
font-family: sans-serif;
line-height: 1.5;
}
ol {
list-style: none;
margin: 0;
padding: 0;
}
.records > * + * {
margin-top: 2rem;
}
h2 {
margin-bottom: 0;
}
nav {
margin-top: 1.5rem;
}
.pages {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.pages a {
border: 1px solid #000000;
padding: 0.5rem;
border-radius: 5px;
display: flex;
text-decoration: none;
}
.pages a:where([aria-current]) {
background-color: #000000;
color: #ffffff;
}
.pages a:where(:focus, :hover) {
background-color: #6c6c6c;
color: #ffffff;
}
You can see it in action in the <a href="https://sad-kare-e07051.netlify.app/">live demo</a> and you can check out the <a href="https://github.com/matuzo/articles/tree/main/11ty-alpine">code on GitHub</a>.
This works fairly well with 7 records. It might even work with 10, 20, or 50, but I have over 400 records. We can make browsing the list easier by adding filters.
A Dynamic Paginated And Filterable List
I like JavaScript, but I also believe that the core content and functionality of a website should be accessible without it. This doesn’t mean that you can’t use JavaScript at all, it just means that you start with a basic server-rendered foundation of your component or site, and you add functionality layer by layer. This is called <a href="https://briefs.video/videos/is-progressive-enhancement-dead-yet/">progressive enhancement</a>.
Our foundation in this example is the static list created with 11ty, and now we add a layer of functionality with Alpine.
First, right before the closing body tag, we reference the latest version (as of writing 3.9.1) of Alpine.js:
<script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>
</body>
<strong>Note:</strong> <em>Be careful using a third-party CDN, this can have all kinds of negative implications (performance, privacy, security). Consider referencing the file locally or <a href="https://alpinejs.dev/essentials/installation#as-a-module">importing it as a module</a>.In case you’re wondering why you don’t see the <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity">Subresource Integrity</a> hash in the official docs, it’s because I’ve created and added it manually.</em>
Since we’re moving into JavaScript-world, we need to make our records available to Alpine.js. Probably not the best, but the quickest solution is to create a .eleventy.js file in your root folder and add the following lines:
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("_data");
};
This ensures that eleventy doesn’t just generate HTML files, but it also copies the contents of the _data folder into our destination folder, making it accessible to our scripts.
Fetching Data
Just like in the previous example, we’ll add the x-data directive to our component to pass data:
<div class="collection" x-data="{ records: [] }">
</div>
We don’t have any data, so we need to fetch it as the component initialises. The x-init directive allows us to hook into the initialisation phase of any element and perform tasks:
<div class="collection" x-init="records = await (await fetch('/_data/records.json')).json()" x-data="{ records: [] }">
<div x-text="records"></div>
[…]
</div>
If we output the results directly, we see a list of [object Object]s, because we’re fetching and receiving an array. Instead, we should iterate over the list using the x-for directive on a template tag and output the data using x-text:
<template x-for="record in records">
<li>
<strong x-text="record.title"></strong><br>
Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>.
</li>
</template>
The <template> HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.
<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template">MDN: <template>: The Content Template Element</a>
Here’s how the whole list looks like now:
<div class="collection" x-init="records = await (await fetch('/_data/records.json')).json()" x-data="{ records: [] }">
<p id="message">Showing <output>{{ records.length }} records</output></p>
<div aria-labelledby="message" role="region">
<ol class="records">
<template x-for="record in records">
<li>
<strong x-text="record.title"></strong><br>
Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>.
</li>
</template>
{%- for record in pagination.items %}
<li>
<strong>{{ record.title }}</strong><br>
Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}.
</li>
{%- endfor %}
</ol>
</div>
[…]
</div>
Isn’t it amazing how quickly we were able to fetch and output data? Check out the demo below to see how Alpine populates the list with results.
<strong>Hint:</strong> <em>You don’t see any Nunjucks code in this CodePen, because 11ty doesn’t run in the browser. I’ve just copied and pasted the rendered HTML of the first page.</em>
See the Pen <a href="https://codepen.io/smashingmag/pen/abEWRMY">Pagination + Filter with Alpine.js Step 1</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
You can achieve a lot by using Alpine’s directives, but at some point relying only on attributes can get messy. That’s why I’ve decided to move the data and some of the logic into a separate Alpine component object.
Here’s how that works: Instead of passing data directly, we now reference a component using x-data. The rest is pretty much identical: Define a variable to hold our data, then fetch our JSON file in the initialization phase. However, we don’t do that inside an attribute, but inside a script tag or file instead:
<div class="collection" x-data="collection">
[…]
</div>
[…]
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('collection', () => ({
records: [],
async getRecords() {
this.records = await (await fetch('/_data/records.json')).json();
},
init() {
this.getRecords();
}
}))
})
</script>
<script src="https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"></script>
Looking at the previous CodePen, you’ve probably noticed that we now have a duplicate set of data. That’s because our static 11ty list is still there. Alpine has a directive that tells it to ignore certain DOM elements. I don’t know if this is actually necessary here, but it’s a nice way of marking these <em>unwanted</em> elements. So, we add the x-ignore directive on our 11ty list items, and we add a class to the html element when the data has loaded and then use the class and the attribute to hide those list items in CSS:
<style>
.alpine [x-ignore] {
display: none;
}
</style>
[…]
{%- for record in pagination.items %}
<li x-ignore>
<strong>{{ record.title }}</strong><br>
Released in <time datetime="{{ record.year }}">{{ record.year }}</time> by {{ record.artist }}.
</li>
{%- endfor %}
[…]
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('collection', () => ({
records: [],
async getRecords() {
this.records = await (await fetch('/_data/records.json')).json();
document.documentElement.classList.add('alpine');
},
init() {
this.getRecords();
}
}))
})
</script>
11ty data is hidden, results are coming from Alpine, but the pagination is not functional at the moment:
See the Pen <a href="https://codepen.io/smashingmag/pen/eYyWQOe">Pagination + Filter with Alpine.js Step 2</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Pagination
Before we add filters, let’s paginate our data. 11ty did us the favor of handling all the logic for us, but now we have to do it on our own. In order to split our data across multiple pages, we need the following:
the number of items per page (itemsPerPage),
the current page (currentPage),
the total number of pages (numOfPages),
a dynamic, paged subset of the whole data (page).
document.addEventListener('alpine:init', () => {
Alpine.data('collection', () => ({
records: [],
itemsPerPage: 5,
currentPage: 0,
numOfPages: // total number of pages,
page: // paged items
async getRecords() {
this.records = await (await fetch('/_data/records.json')).json();
document.documentElement.classList.add('alpine');
},
init() {
this.getRecords();
}
}))
})
The number of items per page is a fixed value (5), and the current page starts with 0. We get the number of pages by dividing the total number of items by the number of items per page:
numOfPages() {
return Math.ceil(this.records.length / this.itemsPerPage)
// 7 / 5 = 1.4
// Math.ceil(7 / 5) = 2
},
The easiest way for me to get the items per page was to use the slice() method in JavaScript and take out the slice of the dataset that I need for the current page:
page() {
return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)
// this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage
// Page 1: 0 * 5, (0 + 1) * 5 (=> slice(0, 5);)
// Page 2: 1 * 5, (1 + 1) * 5 (=> slice(5, 10);)
// Page 3: 2 * 5, (2 + 1) * 5 (=> slice(10, 15);)
}
To only display the items for the current page, we have to adapt the for loop to iterate over page instead of records:
<ol class="records">
<template x-for="record in page">
<li>
<strong x-text="record.title"></strong><br>
Released in <time :datetime="record.year" x-text="record.year"></time> by <span x-text="record.artist"></span>.
</li>
</template>
</ol>
We now have a page, but no links that allow us to jump from page to page. Just like earlier, we use the template element and the x-for directive to display our page links:
<ol class="pages">
<template x-for="idx in numOfPages">
<li>
<a :href="`/${idx}`" x-text="`Page ${idx}`" :aria-current="idx === currentPage + 1 ? 'page' : false" @click.prevent="currentPage = idx - 1"></a>
</li>
</template>
{% for page_entry in pagination.pages %}
<li x-ignore>
[…]
</li>
{% endfor %}
</ol>
Since we don’t want to reload the whole page anymore, we put a click event on each link, prevent the default click behavior, and change the current page number on click:
<a href="/" @click.prevent="currentPage = idx - 1"></a>
Here’s what that looks like in the browser. (I’ve added more entries to the JSON file. You can <a href="https://raw.githubusercontent.com/matuzo/articles/main/11ty-alpine/_data/records_full.json">download it on GitHub</a>.)
See the Pen <a href="https://codepen.io/smashingmag/pen/GRymwjg">Pagination + Filter with Alpine.js Step 3</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Filtering
I want to be able to filter the list by artist and by decade.
We add two select elements wrapped in a fieldset to our component, and we put a x-model directive on each of them. x-model allows us to bind the value of an input element to Alpine data:
<fieldset class="filters">
<legend>Filter by</legend>
<label for="artist">Artist</label>
<select id="artist" x-model="filters.artist">
<option value="">All</option>
</select>
<label for="decade">Decade</label>
<select id="decade" x-model="filters.year">
<option value="">All</option>
</select>
</fieldset>
Of course, we also have to create these data fields in our Alpine component:
document.addEventListener('alpine:init', () => {
Alpine.data('collection', () => ({
filters: {
year: '',
artist: '',
},
records: [],
itemsPerPage: 5,
currentPage: 0,
numOfPages() {
return Math.ceil(this.records.length / this.itemsPerPage)
},
page() {
return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)
},
async getRecords() {
this.records = await (await fetch('/_data/records.json')).json();
document.documentElement.classList.add('alpine');
},
init() {
this.getRecords();
}
}))
})
If we change the selected value in each select, filters.artist and filters.year will update automatically. You can try it here with some dummy data I’ve added manually:
See the Pen <a href="https://codepen.io/smashingmag/pen/GGRymwEp">Pagination + Filter with Alpine.js Step 4</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Now we have select elements, and we’ve bound the data to our component. The next step is to populate each select dynamically with artists and decades respectively. For that we take our records array and manipulate the data a bit:
document.addEventListener('alpine:init', () => {
Alpine.data('collection', () => ({
artists: [],
decades: [],
// […]
async getRecords() {
this.records = await (await fetch('/_data/records.json')).json();
this.artists = [...new Set(this.records.map(record => record.artist))].sort();
this.decades = [...new Set(this.records.map(record => record.year.toString().slice(0, -1)))].sort();
document.documentElement.classList.add('alpine');
},
// […]
}))
})
This looks wild, and I’m sure that I’ll forget what’s going on here real soon, but what this code does is that it takes the array of objects and turns it into an array of strings (map()), it makes sure that each entry is unique (that’s what [...new Set()] does here) and sorts the array alphabetically (sort()). For the decade’s array, I’m additionally slicing off the last digit of the year because I don’t want this filter to be too granular. Filtering by decade is good enough.
Next, we populate the artist and decade select elements, again using the template element and the x-for directive:
<label for="artist">Artist</label>
<select id="artist" x-model="filters.artist">
<option value="">All</option>
<template x-for="artist in artists">
<option x-text="artist"></option>
</template>
</select>
<label for="decade">Decade</label>
<select id="decade" x-model="filters.year">
<option value="">All</option>
<template x-for="year in decades">
<option :value="year" x-text="`${year}0`"></option>
</template>
</select>
Try it yourself in <a href="https://codepen.io/matuzo/pen/KKZwqQe?editors=1010">demo 5 on Codepen</a>.
See the Pen <a href="https://codepen.io/smashingmag/pen/OJzmaZb">Pagination + Filter with Alpine.js Step 5</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
We’ve successfully populated the select elements with data from our JSON file. To finally filter the data, we go through all records, we check whether a filter is set. If that’s the case, we check that the respective field of the record corresponds to the selected value of the filter. If not, we filter this record out. We’re left with a filtered array that matches the criteria:
get filteredRecords() {
const filtered = this.records.filter((item) => {
for (var key in this.filters) {
if (this.filters[key] === '') {
continue
}
if(!String(item[key]).includes(this.filters[key])) {
return false
}
}
return true
});
return filtered
}
For this to take effect we have to adapt our numOfPages() and page() functions to use only the filtered records:
numOfPages() {
return Math.ceil(this.filteredRecords.length / this.itemsPerPage)
},
page() {
return this.filteredRecords.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)
},
See the Pen <a href="https://codepen.io/smashingmag/pen/GRymwQZ">Pagination + Filter with Alpine.js Step 6</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
<strong>Three things left to do:</strong>
fix a bug;
hide the form;
update the status message.
Bug Fix: Watching a Component Property
When you open the first page, click on page 6, then select “1990” — you don’t see any results. That’s because our filter thinks that we’re still on page 6, but 1) we’re actually on page 1, and 2) there is no page 6 with “1990” active. We can fix that by resetting the currentPage when the user changes one of the filters. To watch changes in the filter object, we can use a so-called magic method:
init() {
this.getRecords();
this.$watch('filters', filter => this.currentPage = 0);
}
Every time the filter property changes, the currentPage will be set to 0.
Hiding the Form
Since the filters only work with JavaScript enabled and functioning, we should hide the whole form when that’s not the case. We can use the .alpine class we created earlier for that:
<fieldset class="filters" hidden>
[…]
</fieldset>
.filters {
display: block;
}
html:not(.alpine) .filters {
visibility: hidden;
}
I’m using visibility: hidden instead of hidden only to avoid content shifting while Alpine is still loading.
Communicating Changes
The status message at the beginning of our list still reads “Showing 7 records”, but this doesn’t change when the user changes the page or filters the list. There are two things we have to do to make the paragraph dynamic: bind data to it and communicate changes to assistive technology (a screen reader, e.g.).
First, we bind data to the output element in the paragraph that changes based on the current page and filter:
<p id="message">Showing <output x-text="message">{{ records.length }} records</output></p>
Alpine.data('collection', () => ({
message() {
return `${this.filteredRecords.length} records`;
},
// […]
Next, we want to communicate to screen readers that the content on the page has changed. There are at least two ways of doing that:
We could turn an element into a so-called <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">live region</a> using the aria-live attribute. A live region is an element that announces its content to screen readers every time it changes.
<div aria-live="polite">Dynamic changes will be announced</div>
In our case, we don’t have to do anything, because we’re already using the output element (remember?) which is an implicit live region by default.
<p id="message">Showing <output x-text="message">{{ records.length }} records</output></p>
“The <output> HTML element is a container element into which a site or app can inject the results of a calculation or the outcome of a user action.”
Source: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"><output>: The Output Element</a>, MDN Web Docs
We could make the region focusable and move the focus to the region when its content changes. Since the region is labelled, its name and role will be announced when that happens.
<div aria-labelledby="message" role="region" tabindex="-1" x-ref="region">
We can reference the region using the x-ref directive.
<a @click.prevent="currentPage = idx - 1; $nextTick(() => { $refs.region.focus(); $refs.region.scrollIntoView(); });" :href="/${idx}" x-text="Page ${idx}" :aria-current="idx === currentPage + 1 ? 'page' : false">
I’ve decided to do both:
When users <em>filter</em> the page, we update the live region, but we don’t move focus.
When they <em>change</em> the page, we move focus to the list.
That’s it. Here’s the <a href="https://sad-kare-e07051.netlify.app/index_js/">final result</a>:
See the Pen <a href="https://codepen.io/smashingmag/pen/zYpwMXX">Pagination + Filter with Alpine.js Step 7</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
<strong>Note:</strong> <em>When you filter by artist, and the status message shows “1 records”, and you filter again by another artist, also with just one record, the content of the output element doesn’t change, and nothing is reported to screen readers. This can be seen as a bug or as a feature to reduce redundant announcements. You’ll have to test this with users.</em>
What’s Next?
What I did here might seem redundant, but if you’re like me, and you don’t have enough trust in JavaScript, it’s worth the effort. And if you look at the <a href="https://codepen.io/matuzo/pen/GRygvwY?editors=1100">final CodePen</a> or the <a href="https://github.com/matuzo/articles/tree/main/11ty-alpine">complete code on GitHub</a>, it actually wasn’t that much extra work. Minimal frameworks like Alpine.js make it really easy to progressively enhance static components and make them reactive.
I’m pretty happy with the result, but there are a few more things that could be improved:
The pagination could be smarter (maximum number of pages, previous and next links, and so on).
Let users pick the number of items per page.
Sorting would be a nice feature.
Working with the history API would be great.
Content shifting can be improved.
The solution needs user testing and browser/screen reader testing.
<strong>P.S.</strong> <em>Yes, I know, Alpine produces invalid HTML with its custom x- attribute syntax. That hurts me as much as it hurts you, but as long as it doesn’t affect users, I can live with that. 🙂</em>
<strong>P.S.S.</strong> <em>Special thanks to Scott, Søren, Thain, David, Saptak and Christian for their feedback.</em>
Further Resources
“<a href="https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/">How To Build A Filterable List Of Things</a>”, Søren Birkemeyer
“<a href="https://www.scottohara.me/blog/2022/02/05/dynamic-results.html">Considering Dynamic Search Results And Content</a>”, Scott O’Hara
<a href="https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/d87b905e-1190-4a40-80d1-4947c3185aa7/1-accessible-filterable-paginated-list-11ty-alpinejs.jpg"></a>
<a href="https://www.smashingmagazine.com/2022/04/accessible-filterable-paginated-list-11ty-alpinejs/"> Go to Source of this post </a>
Author Of this post:
Title Of post: How To Build A Progressively Enhanced, Accessible, Filterable And Paginated List
Author Link: {authorlink}
</div>
</article>
</li>
<li class="ab-webmentions__item">
<article class="ab-webmention h-cite" id="webmention-1374913">
<div class="ab-webmention__meta">
<a class="ab-webmention__author p-author h-card u-url" href="https://tavarro.com/magazine/how-to-build-a-progressively-enhanced-accessible-filterable-and-paginated-listhellosmashingmagazine-com-manuel-matuzovic/" target="_blank" rel="noopener noreferrer">
<img class="ab-webmention__author-photo u-photo lazyload" data-src="https://webmention.io/avatar/secure.gravatar.com/b8b88206409c4fce32c580415c722d3596a59a0532a1a292f9f9d0504f902ed6.jpg" alt="">
<strong class="p-name"></strong>
</a>
</div>
<div class="ab-webmention__content p-content">
Most sites I build are static sites with HTML files generated by a static site generator or pages served on a server by a CMS like <a href="https://wordpress.com/">WordPress</a> or <a href="https://craftcms.com/">CraftCMS</a>. I use JavaScript only on top to enhance the user experience. I use it for things like disclosure widgets, accordions, fly-out navigations, or modals.
The requirements for most of these features are simple, so using a library or framework would be overkill. Recently, however, I found myself in a situation where writing a component from scratch in Vanilla JS without the help of a framework would’ve been too complicated and messy.
Lightweight Frameworks
My task was to add multiple filters, sorting and pagination to an existing list of items. I didn’t want to use a JavaScript Framework like Vue or React, only because I needed help in some places on my site, and I didn’t want to change my stack. <a href="https://twitter.com/mmatuzo/status/1483739260092100609">I consulted Twitter</a>, and people suggested minimal frameworks like <a href="https://lit.dev/">lit</a>, <a href="https://github.com/vuejs/petite-vue">petite-vue</a>, <a href="https://hyperscript.org/">hyperscript</a>, <a href="https://htmx.org/">htmx</a> or <a href="https://alpinejs.dev/">Alpine.js</a>. I went with Alpine because it sounded like it was exactly what I was looking for:
“Alpine is a rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web. Plop in a script tag and get going.”
Alpine.js
Alpine is a lightweight (~7KB) collection of 15 attributes, 6 properties, and 2 methods. I won’t go into the basics of it (check out this <a href="https://css-tricks.com/alpine-js-the-javascript-framework-thats-used-like-jquery-written-like-vue-and-inspired-by-tailwindcss/">article about Alpine</a> by <a href="https://codewithhugo.com/">Hugo Di Francesco</a> or read the <a href="https://alpinejs.dev/start-here">Alpine docs</a>), but let me quickly introduce you to Alpine:
<strong>Note:</strong> <em>You can skip this intro and go straight to the <a href="https://www.smashingmagazine.com/#static-paginated-list">main content of the article</a> if you’re already familiar with Alpine.js.</em>
Let’s say we want to turn a simple list with many items into a <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#disclosure">disclosure widget</a>. You could use the native HTML elements: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details">details and summary</a> for that, but for this exercise, I’ll use Alpine.
By default, with JavaScript disabled, we show the list, but we want to hide it and allow users to open and close it by pressing a button if JavaScript is enabled:
<h2>Beastie Boys Anthology</h2>
<p>The Sounds of Science is the first anthology album by American rap rock group Beastie Boys composed of greatest hits, B-sides, and previously unreleased tracks.</p>
<ol>
<li>Beastie Boys</li>
<li>Slow And Low</li>
<li>Shake Your Rump</li>
<li>Gratitude</li>
<li>Skills To Pay The Bills</li>
<li>Root Down</li>
<li>Believe Me</li>
…
</ol>
First, we include Alpine using a script tag. Then we wrap the list in a div and use the x-data directive to pass data into the component. The open property inside the object we passed is available to all children of the div:
<div x-data=”{ open: false }”>
<ol>
<li>Beastie Boys</li>
<li>Slow And Low</li>
…
</ol>
</div>
<script src=”https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js” integrity=”sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I” crossorigin=”anonymous”></script>
We can use the open property for the x-show directive, which determines whether or not an element is visible:
<div x-data=”{ open: false }”>
<ol x-show=”open”>
<li>Beastie Boys</li>
<li>Slow And Low</li>
…
</ol>
</div>
Since we set open to false, the list is hidden now.
Next, we need a button that toggles the value of the open property. We can add events by using the x-on:click directive or the shorter @-Syntax @click:
<div x-data=”{ open: false }”>
<button @click=”open = !open”>Tracklist</button>
<ol x-show=”open”>
<li>Beastie Boys</li>
<li>Slow And Low</li>
…
</ol>
</div>
Pressing the button, open now switches between false and true and x-show reactively watches these changes, showing and hiding the list accordingly.
While this works for keyboard and mouse users, it’s useless to screen reader users, as we need to communicate the state of our widget. We can do that by toggling the value of the aria-expanded attribute:
<button @click=”open = !open” :aria-expanded=”open”>
Tracklist
</button>
We can also create a semantic connection between the button and the list using aria-controls for <a href="https://a11ysupport.io/tech/aria/aria-controls_attribute">screen readers that support the attribute</a>:
<button @click=”open = ! open” :aria-expanded=”open” aria-controls=”tracklist”>
Tracklist
</button>
<ol x-show=”open” id=”tracklist”>
…
</ol>
Here’s the final result:
Setup
Before we get started, let’s set up our site. We need:
a project folder for our site,
11ty to generate HTML files,
an input file for our HTML,
a data file that contains the list of records.
On your command line, navigate to the folder where you want to save the project, create a folder, and cd into it:
cd Sites # or wherever you want to save the project
mkdir myrecordcollection # pick any name
cd myrecordcollection
Then create a package.json file and <a href="https://www.11ty.dev/docs/getting-started/">install eleventy</a>:
npm init -y
npm install @11ty/eleventy
Next, create an index.njk file (.njk means this is a Nunjucks file; more about that below) and a folder _data with a records.json:
touch index.njk
mkdir _data
touch _data/records.json
You don’t have to do all these steps on the command line. You can also create folders and files in any user interface. The final file and folder structure looks like this:
Adding Content
11ty allows you to write content directly into an HTML file (or Markdown, Nunjucks, and <a href="https://www.11ty.dev/docs/languages/">other template languages</a>). You can even store data in the <a href="https://www.11ty.dev/docs/data-frontmatter/">front matter</a> or in a JSON file. I don’t want to manage hundreds of entries manually, so I’ll store them in the JSON file we just created. Let’s add some data to the file:
[
{
“artist”: “Akne Kid Joe”,
“title”: “Die große Palmöllüge”,
“year”: 2020
},
{
“artist”: “Bring me the Horizon”,
“title”: “Post Human: Survial Horror”,
“year”: 2020
},
{
“artist”: “Idles”,
“title”: “Joy as an Act of Resistance”,
“year”: 2018
},
{
“artist”: “Beastie Boys”,
“title”: “Licensed to Ill”,
“year”: 1986
},
{
“artist”: “Beastie Boys”,
“title”: “Paul’s Boutique”,
“year”: 1989
},
{
“artist”: “Beastie Boys”,
“title”: “Check Your Head”,
“year”: 1992
},
{
“artist”: “Beastie Boys”,
“title”: “Ill Communication”,
“year”: 1994
}
]
Finally, let’s add a basic HTML structure to the index.njk file and start eleventy:
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>My Record Collection</title>
</head>
<body>
<h1>My Record Collection</h1>
</body>
</html>
By running the following command you should be able to access the site at http://localhost:8080:
eleventy –serve
Displaying Content
Now let’s take the data from our JSON file and turn it into HTML. We can access it by looping over the records object in nunjucks:
<div class=”collection”>
<ol>
{% for record in records %}
<li>
<strong>{{ record.title }}</strong><br>
Released in <time datetime=”{{ record.year }}”>{{ record.year }}</time> by {{ record.artist }}.
</li>
{% endfor %}
</ol>
</div>
Pagination
Eleventy supports pagination out of the box. All we have to do is add a frontmatter block to our page, tell 11ty which dataset it should use for pagination, and finally, we have to adapt our for loop to use the paginated list instead of all records:
—
pagination:
data: records
size: 5
—
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>My Record Collection</title>
</head>
<body>
<h1>My Record Collection</h1>
<div class=”collection”>
<p id=”message”>Showing <output>{{ records.length }} records</output></p>
<div aria-labelledby=”message” role=”region”>
<ol class=”records”>
{% for record in pagination.items %}
<li>
<strong>{{ record.title }}</strong><br>
Released in <time datetime=”{{ record.year }}”>{{ record.year }}</time> by {{ record.artist }}.
</li>
{% endfor %}
</ol>
</div>
</div>
</body>
</html>
If you access the page again, the list only contains 5 items. You can also see that I’ve added a status message (ignore the output element for now), wrapped the list in a div with the role “region”, and that I’ve labelled it by creating a reference to #message using aria-labelledby. I did that to turn it into a <a href="https://www.htmhell.dev/tips/landmarks/">landmark</a> and allow screen reader users to access the list of results directly using keyboard shortcuts.
Next, we’ll add a navigation with links to all pages created by the static site generator. The pagination object holds an array that contains all pages. We use aria-current=”page” to highlight the current page:
<nav aria-label=”Select a page”>
<ol class=”pages”>
{% for page_entry in pagination.pages %}
{%- set page_url = pagination.hrefs[loop.index0] -%}
<li>
<a href=”{{ page_url }}”{% if page.url == page_url %} aria-current=”page”{% endif %}>
Page {{ loop.index }}
</a>
</li>
{% endfor %}
</ol>
</nav>
Finally, let’s add some basic CSS to improve the styling:
body {
font-family: sans-serif;
line-height: 1.5;
}
ol {
list-style: none;
margin: 0;
padding: 0;
}
.records > * + * {
margin-top: 2rem;
}
h2 {
margin-bottom: 0;
}
nav {
margin-top: 1.5rem;
}
.pages {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.pages a {
border: 1px solid #000000;
padding: 0.5rem;
border-radius: 5px;
display: flex;
text-decoration: none;
}
.pages a:where([aria-current]) {
background-color: #000000;
color: #ffffff;
}
.pages a:where(:focus, :hover) {
background-color: #6c6c6c;
color: #ffffff;
}
You can see it in action in the <a href="https://sad-kare-e07051.netlify.app/">live demo</a> and you can check out the <a href="https://github.com/matuzo/articles/tree/main/11ty-alpine">code on GitHub</a>.
This works fairly well with 7 records. It might even work with 10, 20, or 50, but I have over 400 records. We can make browsing the list easier by adding filters.
A Dynamic Paginated And Filterable List
I like JavaScript, but I also believe that the core content and functionality of a website should be accessible without it. This doesn’t mean that you can’t use JavaScript at all, it just means that you start with a basic server-rendered foundation of your component or site, and you add functionality layer by layer. This is called <a href="https://briefs.video/videos/is-progressive-enhancement-dead-yet/">progressive enhancement</a>.
Our foundation in this example is the static list created with 11ty, and now we add a layer of functionality with Alpine.
First, right before the closing body tag, we reference the latest version (as of writing 3.9.1) of Alpine.js:
<script src=”https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js” integrity=”sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I” crossorigin=”anonymous”></script>
</body>
<strong>Note:</strong> <em>Be careful using a third-party CDN, this can have all kinds of negative implications (performance, privacy, security). Consider referencing the file locally or <a href="https://alpinejs.dev/essentials/installation#as-a-module">importing it as a module</a>.In case you’re wondering why you don’t see the <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity">Subresource Integrity</a> hash in the official docs, it’s because I’ve created and added it manually.</em>
Since we’re moving into JavaScript-world, we need to make our records available to Alpine.js. Probably not the best, but the quickest solution is to create a .eleventy.js file in your root folder and add the following lines:
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy(“_data”);
};
This ensures that eleventy doesn’t just generate HTML files, but it also copies the contents of the _data folder into our destination folder, making it accessible to our scripts.
Fetching Data
Just like in the previous example, we’ll add the x-data directive to our component to pass data:
<div class=”collection” x-data=”{ records: [] }”>
</div>
We don’t have any data, so we need to fetch it as the component initialises. The x-init directive allows us to hook into the initialisation phase of any element and perform tasks:
<div class=”collection” x-init=”records = await (await fetch(‘/_data/records.json’)).json()” x-data=”{ records: [] }”>
<div x-text=”records”></div>
[…]
</div>
If we output the results directly, we see a list of [object Object]s, because we’re fetching and receiving an array. Instead, we should iterate over the list using the x-for directive on a template tag and output the data using x-text:
<template x-for=”record in records”>
<li>
<strong x-text=”record.title”></strong><br>
Released in <time :datetime=”record.year” x-text=”record.year”></time> by <span x-text=”record.artist”></span>.
</li>
</template>
The <template> HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.
<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template">MDN: <template>: The Content Template Element</a>
Here’s how the whole list looks like now:
<div class=”collection” x-init=”records = await (await fetch(‘/_data/records.json’)).json()” x-data=”{ records: [] }”>
<p id=”message”>Showing <output>{{ records.length }} records</output></p>
<div aria-labelledby=”message” role=”region”>
<ol class=”records”>
<template x-for=”record in records”>
<li>
<strong x-text=”record.title”></strong><br>
Released in <time :datetime=”record.year” x-text=”record.year”></time> by <span x-text=”record.artist”></span>.
</li>
</template>
{%- for record in pagination.items %}
<li>
<strong>{{ record.title }}</strong><br>
Released in <time datetime=”{{ record.year }}”>{{ record.year }}</time> by {{ record.artist }}.
</li>
{%- endfor %}
</ol>
</div>
[…]
</div>
Isn’t it amazing how quickly we were able to fetch and output data? Check out the demo below to see how Alpine populates the list with results.
<strong>Hint:</strong> <em>You don’t see any Nunjucks code in this CodePen, because 11ty doesn’t run in the browser. I’ve just copied and pasted the rendered HTML of the first page.</em>
See the Pen <a href="https://codepen.io/smashingmag/pen/abEWRMY">Pagination + Filter with Alpine.js Step 1</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
You can achieve a lot by using Alpine’s directives, but at some point relying only on attributes can get messy. That’s why I’ve decided to move the data and some of the logic into a separate Alpine component object.
Here’s how that works: Instead of passing data directly, we now reference a component using x-data. The rest is pretty much identical: Define a variable to hold our data, then fetch our JSON file in the initialization phase. However, we don’t do that inside an attribute, but inside a script tag or file instead:
<div class=”collection” x-data=”collection”>
[…]
</div>
[…]
<script>
document.addEventListener(‘alpine:init’, () => {
Alpine.data(‘collection’, () => ({
records: [],
async getRecords() {
this.records = await (await fetch(‘/_data/records.json’)).json();
},
init() {
this.getRecords();
}
}))
})
</script>
<script src=”https://unpkg.com/alpinejs@3.9.1/dist/cdn.min.js” integrity=”sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I” crossorigin=”anonymous”></script>
Looking at the previous CodePen, you’ve probably noticed that we now have a duplicate set of data. That’s because our static 11ty list is still there. Alpine has a directive that tells it to ignore certain DOM elements. I don’t know if this is actually necessary here, but it’s a nice way of marking these <em>unwanted</em> elements. So, we add the x-ignore directive on our 11ty list items, and we add a class to the html element when the data has loaded and then use the class and the attribute to hide those list items in CSS:
<style>
.alpine [x-ignore] {
display: none;
}
</style>
[…]
{%- for record in pagination.items %}
<li x-ignore>
<strong>{{ record.title }}</strong><br>
Released in <time datetime=”{{ record.year }}”>{{ record.year }}</time> by {{ record.artist }}.
</li>
{%- endfor %}
[…]
<script>
document.addEventListener(‘alpine:init’, () => {
Alpine.data(‘collection’, () => ({
records: [],
async getRecords() {
this.records = await (await fetch(‘/_data/records.json’)).json();
document.documentElement.classList.add(‘alpine’);
},
init() {
this.getRecords();
}
}))
})
</script>
11ty data is hidden, results are coming from Alpine, but the pagination is not functional at the moment:
See the Pen <a href="https://codepen.io/smashingmag/pen/eYyWQOe">Pagination + Filter with Alpine.js Step 2</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Pagination
Before we add filters, let’s paginate our data. 11ty did us the favor of handling all the logic for us, but now we have to do it on our own. In order to split our data across multiple pages, we need the following:
the number of items per page (itemsPerPage),
the current page (currentPage),
the total number of pages (numOfPages),
a dynamic, paged subset of the whole data (page).
document.addEventListener(‘alpine:init’, () => {
Alpine.data(‘collection’, () => ({
records: [],
itemsPerPage: 5,
currentPage: 0,
numOfPages: // total number of pages,
page: // paged items
async getRecords() {
this.records = await (await fetch(‘/_data/records.json’)).json();
document.documentElement.classList.add(‘alpine’);
},
init() {
this.getRecords();
}
}))
})
The number of items per page is a fixed value (5), and the current page starts with 0. We get the number of pages by dividing the total number of items by the number of items per page:
numOfPages() {
return Math.ceil(this.records.length / this.itemsPerPage)
// 7 / 5 = 1.4
// Math.ceil(7 / 5) = 2
},
The easiest way for me to get the items per page was to use the slice() method in JavaScript and take out the slice of the dataset that I need for the current page:
page() {
return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)
// this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage
// Page 1: 0 * 5, (0 + 1) * 5 (=> slice(0, 5);)
// Page 2: 1 * 5, (1 + 1) * 5 (=> slice(5, 10);)
// Page 3: 2 * 5, (2 + 1) * 5 (=> slice(10, 15);)
}
To only display the items for the current page, we have to adapt the for loop to iterate over page instead of records:
<ol class=”records”>
<template x-for=”record in page”>
<li>
<strong x-text=”record.title”></strong><br>
Released in <time :datetime=”record.year” x-text=”record.year”></time> by <span x-text=”record.artist”></span>.
</li>
</template>
</ol>
We now have a page, but no links that allow us to jump from page to page. Just like earlier, we use the template element and the x-for directive to display our page links:
<ol class=”pages”>
<template x-for=”idx in numOfPages”>
<li>
<a :href=”`/${idx}`” x-text=”`Page ${idx}`” :aria-current=”idx === currentPage + 1 ? ‘page’ : false” @click.prevent=”currentPage = idx – 1″></a>
</li>
</template>
{% for page_entry in pagination.pages %}
<li x-ignore>
[…]
</li>
{% endfor %}
</ol>
Since we don’t want to reload the whole page anymore, we put a click event on each link, prevent the default click behavior, and change the current page number on click:
<a href=”/” @click.prevent=”currentPage = idx – 1″></a>
Here’s what that looks like in the browser. (I’ve added more entries to the JSON file. You can <a href="https://raw.githubusercontent.com/matuzo/articles/main/11ty-alpine/_data/records_full.json">download it on GitHub</a>.)
See the Pen <a href="https://codepen.io/smashingmag/pen/GRymwjg">Pagination + Filter with Alpine.js Step 3</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Filtering
I want to be able to filter the list by artist and by decade.
We add two select elements wrapped in a fieldset to our component, and we put a x-model directive on each of them. x-model allows us to bind the value of an input element to Alpine data:
<fieldset class=”filters”>
<legend>Filter by</legend>
<label for=”artist”>Artist</label>
<select id=”artist” x-model=”filters.artist”>
<option value=””>All</option>
</select>
<label for=”decade”>Decade</label>
<select id=”decade” x-model=”filters.year”>
<option value=””>All</option>
</select>
</fieldset>
Of course, we also have to create these data fields in our Alpine component:
document.addEventListener(‘alpine:init’, () => {
Alpine.data(‘collection’, () => ({
filters: {
year: ”,
artist: ”,
},
records: [],
itemsPerPage: 5,
currentPage: 0,
numOfPages() {
return Math.ceil(this.records.length / this.itemsPerPage)
},
page() {
return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)
},
async getRecords() {
this.records = await (await fetch(‘/_data/records.json’)).json();
document.documentElement.classList.add(‘alpine’);
},
init() {
this.getRecords();
}
}))
})
If we change the selected value in each select, filters.artist and filters.year will update automatically. You can try it here with some dummy data I’ve added manually:
See the Pen <a href="https://codepen.io/smashingmag/pen/GGRymwEp">Pagination + Filter with Alpine.js Step 4</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
Now we have select elements, and we’ve bound the data to our component. The next step is to populate each select dynamically with artists and decades respectively. For that we take our records array and manipulate the data a bit:
document.addEventListener(‘alpine:init’, () => {
Alpine.data(‘collection’, () => ({
artists: [],
decades: [],
// […]
async getRecords() {
this.records = await (await fetch(‘/_data/records.json’)).json();
this.artists = […new Set(this.records.map(record => record.artist))].sort();
this.decades = […new Set(this.records.map(record => record.year.toString().slice(0, -1)))].sort();
document.documentElement.classList.add(‘alpine’);
},
// […]
}))
})
This looks wild, and I’m sure that I’ll forget what’s going on here real soon, but what this code does is that it takes the array of objects and turns it into an array of strings (map()), it makes sure that each entry is unique (that’s what […new Set()] does here) and sorts the array alphabetically (sort()). For the decade’s array, I’m additionally slicing off the last digit of the year because I don’t want this filter to be too granular. Filtering by decade is good enough.
Next, we populate the artist and decade select elements, again using the template element and the x-for directive:
<label for=”artist”>Artist</label>
<select id=”artist” x-model=”filters.artist”>
<option value=””>All</option>
<template x-for=”artist in artists”>
<option x-text=”artist”></option>
</template>
</select>
<label for=”decade”>Decade</label>
<select id=”decade” x-model=”filters.year”>
<option value=””>All</option>
<template x-for=”year in decades”>
<option :value=”year” x-text=”`${year}0`”></option>
</template>
</select>
Try it yourself in <a href="https://codepen.io/matuzo/pen/KKZwqQe?editors=1010">demo 5 on Codepen</a>.
See the Pen <a href="https://codepen.io/smashingmag/pen/OJzmaZb">Pagination + Filter with Alpine.js Step 5</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
We’ve successfully populated the select elements with data from our JSON file. To finally filter the data, we go through all records, we check whether a filter is set. If that’s the case, we check that the respective field of the record corresponds to the selected value of the filter. If not, we filter this record out. We’re left with a filtered array that matches the criteria:
get filteredRecords() {
const filtered = this.records.filter((item) => {
for (var key in this.filters) {
if (this.filters[key] === ”) {
continue
}
if(!String(item[key]).includes(this.filters[key])) {
return false
}
}
return true
});
return filtered
}
For this to take effect we have to adapt our numOfPages() and page() functions to use only the filtered records:
numOfPages() {
return Math.ceil(this.filteredRecords.length / this.itemsPerPage)
},
page() {
return this.filteredRecords.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)
},
See the Pen <a href="https://codepen.io/smashingmag/pen/GRymwQZ">Pagination + Filter with Alpine.js Step 6</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
<strong>Three things left to do:</strong>
fix a bug;
hide the form;
update the status message.
Bug Fix: Watching a Component Property
When you open the first page, click on page 6, then select “1990” — you don’t see any results. That’s because our filter thinks that we’re still on page 6, but 1) we’re actually on page 1, and 2) there is no page 6 with “1990” active. We can fix that by resetting the currentPage when the user changes one of the filters. To watch changes in the filter object, we can use a so-called magic method:
init() {
this.getRecords();
this.$watch(‘filters’, filter => this.currentPage = 0);
}
Every time the filter property changes, the currentPage will be set to 0.
Hiding the Form
Since the filters only work with JavaScript enabled and functioning, we should hide the whole form when that’s not the case. We can use the .alpine class we created earlier for that:
<fieldset class=”filters” hidden>
[…]
</fieldset>
.filters {
display: block;
}
html:not(.alpine) .filters {
visibility: hidden;
}
I’m using visibility: hidden instead of hidden only to avoid content shifting while Alpine is still loading.
Communicating Changes
The status message at the beginning of our list still reads “Showing 7 records”, but this doesn’t change when the user changes the page or filters the list. There are two things we have to do to make the paragraph dynamic: bind data to it and communicate changes to assistive technology (a screen reader, e.g.).
First, we bind data to the output element in the paragraph that changes based on the current page and filter:
<p id=”message”>Showing <output x-text=”message”>{{ records.length }} records</output></p>
Alpine.data(‘collection’, () => ({
message() {
return `${this.filteredRecords.length} records`;
},
// […]
Next, we want to communicate to screen readers that the content on the page has changed. There are at least two ways of doing that:
We could turn an element into a so-called <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">live region</a> using the aria-live attribute. A live region is an element that announces its content to screen readers every time it changes.
<div aria-live=”polite”>Dynamic changes will be announced</div>
In our case, we don’t have to do anything, because we’re already using the output element (remember?) which is an implicit live region by default.
<p id=”message”>Showing <output x-text=”message”>{{ records.length }} records</output></p>
“The <output> HTML element is a container element into which a site or app can inject the results of a calculation or the outcome of a user action.”
Source: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"><output>: The Output Element</a>, MDN Web Docs
We could make the region focusable and move the focus to the region when its content changes. Since the region is labelled, its name and role will be announced when that happens.<div aria-labelledby=”message” role=”region” tabindex=”-1″ x-ref=”region”>
We can reference the region using the x-ref directive.
<a @click.prevent=”currentPage = idx – 1; $nextTick(() => { $refs.region.focus(); $refs.region.scrollIntoView(); });” :href=”/${idx}” x-text=”Page ${idx}” :aria-current=”idx === currentPage + 1 ? ‘page’ : false”>
I’ve decided to do both:
When users <em>filter</em> the page, we update the live region, but we don’t move focus.
When they <em>change</em> the page, we move focus to the list.
That’s it. Here’s the <a href="https://sad-kare-e07051.netlify.app/index_js/">final result</a>:
See the Pen <a href="https://codepen.io/smashingmag/pen/zYpwMXX">Pagination + Filter with Alpine.js Step 7</a> by <a href="https://codepen.io/matuzo">Manuel Matuzovic</a>.
<strong>Note:</strong> <em>When you filter by artist, and the status message shows “1 records”, and you filter again by another artist, also with just one record, the content of the output element doesn’t change, and nothing is reported to screen readers. This can be seen as a bug or as a feature to reduce redundant announcements. You’ll have to test this with users.</em>
What’s Next?
What I did here might seem redundant, but if you’re like me, and you don’t have enough trust in JavaScript, it’s worth the effort. And if you look at the <a href="https://codepen.io/matuzo/pen/GRygvwY?editors=1100">final CodePen</a> or the <a href="https://github.com/matuzo/articles/tree/main/11ty-alpine">complete code on GitHub</a>, it actually wasn’t that much extra work. Minimal frameworks like Alpine.js make it really easy to progressively enhance static components and make them reactive.
I’m pretty happy with the result, but there are a few more things that could be improved:
The pagination could be smarter (maximum number of pages, previous and next links, and so on).
Let users pick the number of items per page.
Sorting would be a nice feature.
Working with the history API would be great.
Content shifting can be improved.
The solution needs user testing and browser/screen reader testing.
<strong>P.S.</strong> <em>Yes, I know, Alpine produces invalid HTML with its custom x- attribute syntax. That hurts me as much as it hurts you, but as long as it doesn’t affect users, I can live with that. 🙂</em>
<strong>P.S.S.</strong> <em>Special thanks to Scott, Søren, Thain, David, Saptak and Christian for their feedback.</em>
Further Resources
“<a href="https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/">How To Build A Filterable List Of Things</a>”, Søren Birkemeyer
“<a href="https://www.scottohara.me/blog/2022/02/05/dynamic-results.html">Considering Dynamic Search Results And Content</a>”, Scott O’Hara
,Ever wondered how to build a paginated list that works with and without JavaScript? In this article, Manuel explains how you can leverage the power of Progressive Enhancement and do just that with Eleventy and Alpine.js.,
<em>Related</em>
</div>
</article>
</li>
<li class="ab-webmentions__item">
<article class="ab-webmention h-cite" id="webmention-1311362">
<div class="ab-webmention__meta">
<a class="ab-webmention__author p-author h-card u-url" href="https://twitter.com/gandalfar/status/1466029577936031753" target="_blank" rel="noopener noreferrer">
<img class="ab-webmention__author-photo u-photo lazyload" data-src="https://webmention.io/avatar/pbs.twimg.com/718fefc5fa1c12495a7aa7319bff73632d2abf010c137ecf317c47596da15209.jpg" alt="jure">
<strong class="p-name">jure</strong>
</a>
<time class="ab-webmention__pubdate dt-published" datetime="2021-12-01T13:01:03+00:00">Dec 01, 2021 · 13:01</time>
</div>
<div class="ab-webmention__content p-content">
Very nice article <a href="https://annualbeta.com/blog/how-to-build-a-filterable-list-of-things/">annualbeta.com/blog/how-to-bu…</a>
</div>
</article>
</li>
</ol>
</div>
<details class="ab-webmentions__container">
<summary class="ab-webmentions__meta">
<span class="ab-webmentions__count">10 likes</span>
</summary>
<ol class="ab-likes__list">
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1483825784674529280#favorited-by-23411725">Jens Geiling</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1483825784674529280#favorited-by-9019882">Iago Barreiro</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1483825784674529280#favorited-by-7465672">Quinn Dombrowski</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1483825784674529280#favorited-by-58448784">Matthias Ott</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1483825784674529280#favorited-by-981996647906533378">Ricardo Blanch PM</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1483825784674529280#favorited-by-14912462">Tristan</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1483825784674529280#favorited-by-22161724">Manuel Matuzović</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1287688501295828992#favorited-by-1225049401573421056">Elaventi</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1287688501295828992#favorited-by-751036988">Trevor Adams</a>
</li>
<li class="ab-likes__item u-text-muted u-text-smaller">
liked by <a href="https://twitter.com/polarbirke/status/1287688501295828992#favorited-by-4915020623">Matthias 🦄</a>
</li>
</ol>
</details>
</div>
</main>
<style>
@charset "UTF-8";.ab-footer{padding-top:2rem;padding-bottom:2rem;margin-top:2rem;margin-bottom:.6666666667rem;border-top:2px solid rgba(0,0,0,.1);font-size:.875rem}@media (min-width:768px){.ab-footer{padding-right:2rem;padding-left:2rem;margin-bottom:2rem;font-size:1rem}.ab-footer .ab-content-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}}.ab-footer__contact{color:rgba(0,0,0,.9);text-decoration:none;display:inline-block;margin-bottom:3rem;font-size:1.5rem}.ab-footer__contact:focus,.ab-footer__contact:hover{color:#fff;text-decoration:none;background-color:#000;-webkit-box-shadow:0 0 0 3px #000;box-shadow:0 0 0 3px #000}.ab-footer__nav-item:not(:first-child)::before{content:"•";color:rgba(0,0,0,.55);display:inline-block;padding:0 .25em}.u-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.u-flex-wrap{-ms-flex-wrap:wrap;flex-wrap:wrap}.margin-top-fourth{margin-top:.5rem!important}.u-text-small{font-size:.875em}.u-text-bold{font-weight:700}
</style>
<footer class="ab-footer" role="contentinfo">
<div class="ab-content-wrapper">
<div>
Get in touch:<br>
<a href="https://indieweb.social/@polarbirke" class="ab-footer__contact u-text-bold" rel="me">@polarbirke@indieweb.social</a>
<nav class="u-text-small" aria-label="Footer navigation">
<ul class="u-flex u-flex-wrap">
<li class="ab-footer__nav-item"><a href="https://annualbeta.com/generative-art/">Generative Art</a></li>
<li class="ab-footer__nav-item"><a href="https://annualbeta.com/bookshelf/">Bookshelf</a></li>
<li class="ab-footer__nav-item"><a href="https://github.com/polarbirke" rel="me">Github</a></li>
<li class="ab-footer__nav-item"><a href="https://annualbeta.com/feed.xml">RSS Feed</a></li>
</ul>
</nav>
<p class="u-text-small margin-top-fourth">
annualbeta.com — infrequently updated since 2007
</p>
</div>
</div>
</footer>
</div>
</body>
</html>
Susan Kare, Iconographer (EG8) on Vimeo2020-08-08T13:15:43.749-00:00https://annualbeta.com/bookmarks/susan-kare-iconographer-eg8-on-vimeo/<p>A video of a 2014 talk by Susan Kare, best known for her interface elements and typefaces for the first Apple Macintosh. Her work is incredible, especially because many of the choices she made (trash icon, paint bucket icon, lasso icon, etc.) still persist to this day.</p>
<p>Extremely well spent 19 minutes of interface history.</p>
a11y is web accessibility | Eric Bailey2020-08-10T20:25:23.065-00:00https://annualbeta.com/bookmarks/a11y-is-web-accessibility-or-eric-bailey/<p>Eric with a wonderful essay about the history and value of the term <strong>a11y</strong>. It's about the hashtag, the numeronym and the question whether it itself is accessible - but it's also about so much more. Eric merely brushes the topics of ableism and allyship yet drops just enough thought-provoking insights to make you dive into his citations after finishing this article.</p>
<p>I think I will re-read this often. (via <a href="https://twitter.com/ericwbailey">@ericwbailey</a>)</p>
Why We Think Our Phones Are Secretly Listening to Us | by Simon Pitt | Aug, 2020 | OneZero2020-08-16T13:49:12.081-00:00https://annualbeta.com/bookmarks/why-we-think-our-phones-are-secretly-listening-to-us-or-by-simon-pitt-or-aug-2020-or-onezero/<p>You know that creepy feeling when you see a Facebook ad about a conversation you just had in real life? How it makes you think that your phone is listening to every conversation and using it for retargeting?</p>
<p>Simon Pitt explains why that is unlikely (we're saved by physics!) and how we still end up believing it's true.</p>
Accessibility Testing is like Making Coffee2020-08-19T17:37:45.042-00:00https://annualbeta.com/bookmarks/accessibility-testing-is-like-making-coffee/<blockquote>
<p>In accessibility testing, and when making coffee, we are shooting for the smoothest experience. We want to get to the essence of the thing we're making. We want to filter out the grit and bitterness and include everything that makes our final product enjoyable.</p>
</blockquote>
<p>This is an extremely excellent analogy. I have a feeling it could do wonders for making the realm of accessibility testing tools more accessible. (via <a href="https://twitter.com/ericwbailey">@ericwbailey</a>)</p>
Why you should hire a frontend developer - Technology in government2020-08-29T12:16:29.470-00:00https://annualbeta.com/bookmarks/why-you-should-hire-a-frontend-developer-technology-in-government/<p>Matt Hobbs penned a really good description of the role of a front-end developer.</p>
<blockquote>
<p>A frontend developer bridges the gap between technology and design. They are a cross between an interaction designer and a software developer.<br>
[…]<br>
All frontend development work should be focused on putting user needs first. A frontend developer is the person that all the team's work flows through. They will ultimately collaborate with and communicate the work of a whole agile team to the user. (via <a href="https://twitter.com/adactio">@adactio</a>)</p>
</blockquote>
Why You're So Terrible at Backing Up Your Data | by Simon Pitt | Aug, 2020 | OneZero2020-08-29T13:02:48.062-00:00https://annualbeta.com/bookmarks/why-youre-so-terrible-at-backing-up-your-data-or-by-simon-pitt-or-aug-2020-or-onezero/<blockquote>
<p>It's not our fault. We've all been caught in a world of unreliable drives, inconsistent services, and fragmented technology. There is only so much effort we can put toward this. So I run my too clever by half scripts and continue with my occasional, half-hearted backing-up strategy that backs up some things some of the time. And I hope that I never have to use them.</p>
<p>We rushed to the internet, with its half-formed systems and processes, and are doing the best we can. We didn't have to back up books and DVDs. This is all new to us.</p>
</blockquote>
Built to Last2020-09-22T09:43:15.247-00:00https://annualbeta.com/bookmarks/built-to-last/<blockquote>
<p>In a field that has elevated boy geniuses and rockstar coders, obscure hacks and complex black-boxed algorithms, it's perhaps no wonder that a committee-designed language meant to be easier to learn and use-and which was created by a team that included multiple women in positions of authority-would be held in low esteem. But modern computing has started to become undone, and to undo other parts of our societies, through the field's high opinion of itself, and through the way that it concentrates power into the hands of programmers who mistake social, political, and economic problems for technical ones, often with disastrous results.</p>
</blockquote>
<p>The article is mainly about COBOL and the false claim that it was to blame for failures in critical US public infrastructure during the COVID-19 pandemic, but it surfaces so much more valuable insight into today's 'modern' tech culture. (via <a href="https://twitter.com/adactio">@adactio</a>)</p>
The unreasonable effectiveness of simple HTML - Terence Eden's Blog2021-01-29T16:16:25.035-00:00https://annualbeta.com/bookmarks/the-unreasonable-effectiveness-of-simple-html-terence-edens-blog/<blockquote>
<p>But the <a href="http://gov.uk/">GOV.UK</a> pages are written in simple HTML. They are designed to be lightweight and will work even on rubbish browsers. They have to. This is for everyone.</p>
<p>Not everyone has a big monitor, or a multi-core CPU burning through the teraflops, or a broadband connection.</p>
<p>[…]</p>
<p>Go sit in an uncomfortable chair, in an uncomfortable location, and stare at an uncomfortably small screen with an uncomfortably outdated web browser. How easy is it to use the websites you've created? (via <a href="https://twitter.com/adactio">@adactio</a>)</p>
</blockquote>
Better CSS strikethroughs - Rob Sterlini2021-03-09T16:02:42.082-00:00https://annualbeta.com/bookmarks/better-css-strikethroughs-rob-sterlini/<blockquote>
<p>A quick tip to control the seemingly uncontrollable <code>text‑decoration: line‑through</code>.</p>
<p>Too long, didn't read: Use <code>text-underline-offset</code> and <code>@supports</code> to progressively enhance finer control over your strikethroughs. (via <a href="https://twitter.com/zachleat">@zachleat</a>)</p>
</blockquote>
The Healing Power of Javascript | WIRED2021-04-07T11:06:53.253-00:00https://annualbeta.com/bookmarks/the-healing-power-of-javascript-or-wired/<blockquote>
<p>Ellen Ullman writes in her book Life in Code: A Personal History of Technology, 'Until I became a programmer, I didn't thoroughly understand the usefulness of such isolation: the silence, the reduction of life to thought and form […]'</p>
</blockquote>
<p>Craig Mod with another beautifully crafted piece of writing. This feels very much like a rallying cry to Homebrew Website Clubs. Incidentally, the HWC Bonn - a biweekly meeting of makers, tinkerers and other nerd-minded beings - is taking place this week. (via <a href="https://twitter.com/adactio">@adactio</a>)</p>
The many languages of front-end development2021-11-19T00:00:00-00:00https://annualbeta.com/blog/the-many-languages-of-front-end-development/<p>I have been working on a series of web forms lately. At the end of many days, I often had not made much actual progress, but my brain felt like a fried vegetable anyway.</p>
<p>While I was working on a feature today, a multilingual text input component, I realized why: I have been juggling <em>a lot</em> of languages this whole time.</p>
<h2>Languages</h2>
<p>First, there is HTML and the elements that comprise a simple form: <code><form></code>, <code><label></code>, <code><input></code> and <code><button></code>. There are of course more than those to choose from for specific tasks, like <code><fieldset></code> if you need to group related input fields. Also, there are a couple of attributes to consider, for example <code>id</code>, <code>type</code> , <code>required</code> and <code>autocomplete</code> for inputs. There is <code>for</code> as one way to associate a label with an input and create what is called the "accessible name" for the input.</p>
<p><a href="https://w3c.github.io/using-aria/">ARIA</a> (sometimes even WAI-ARIA, an acronym for "Web Accessibility Initiative – Accessible Rich Internet Applications") certainly feels like its own language that is also expressed via HTML attributes. <a href="https://w3c.github.io/using-aria/#rule1">ARIA should only be used when HTML alone does not convey enough meaning or information by itself</a>. <code>aria-describedby</code> is a great way to to associate instructions with an input field, so that assistive software can pick up on the connection. A screenreader like <a href="https://www.nvaccess.org/">NVDA</a> could then read out the instructions when the associated input is focused. <code>aria-invalid="true"</code> can be used to communicate that a certain input field cannot be accepted because it is in an invalid state – for example, because an input value is required, but the field was left empty.</p>
<p>To communicate the same invalid state visually, CSS comes to play. A warning icon or different graphical distinction should be used to highlight which field is in need of attention. And that's not the only thing CSS is used for; every HTML element is shaped and positioned <em>just so</em> with CSS to achieve a clear and consistent overall form presentation.</p>
<p>For more elaborate form components, there's JavaScript (JS) and Progressive Enhancement. The <code>aria-invalid="true"</code> from earlier might have been added with JS – when focus moved out of a required field left empty – to trigger an immediate feedback. JS is also useful to conditionally reveal form fields that are only necessary if a certain answer was given earlier. "Are you a front-end developer?" If yes, display the follow-up question "<a href="https://bradfrost.com/blog/post/front-of-the-front-end-and-back-of-the-front-end-web-development/">Do you work on the front or the back of the front-end – or both?</a>".</p>
<p>That's four languages already: HTML, ARIA, CSS and JavaScript. But it's 2021, right? We're a bit more sophisticated these days and usually use a templating language instead of writing HTML directly. In my case, that's <a href="https://twig.symfony.com/">Twig</a>. Twig is marketed as "the flexible, fast, and secure template engine for PHP" and has powerful features that HTML is sadly lacking. I really like it – but it is another language to be learned, with its own quirks to handle.</p>
<p>Twig was conceived for <a href="https://symfony.com/">Symfony</a>, which apparently is both "a set of reusable PHP components" and "a PHP framework for web projects". In our projects, business logic, field type configuration and actual composition of forms and fields mostly happens in PHP, so I have to speak PHP as well. At least enough to understand the equivalent of the useful phrases every tourist picks up quickly in a foreign county: "Sí", "Nein", "Deux baguettes, s'il vous plait!", "Dimanakah tandas?" and the ever useful "How do I customize the error message for this particular form input constraint?".</p>
<h2>Dialects</h2>
<p>Now there are six languages: HTML, ARIA, CSS, JavaScript, Twig and PHP. But wait! I haven't mentioned dialects yet. Yes, dialects. Because even with all the fantastic standardization and specification work that's been done, there are still differences in how these languages are processed. <a href="https://webkit.org/">WebKit</a>, the browser engine that powers Apple's Safari browser, requires a bit of extra finesse, sorry, CSS to customize the look of certain form elements. Microsoft's Internet Explorer used to be the same, but it was replaced by Edge and <a href="https://gs.statcounter.com/browser-market-share/desktop/worldwide">its dialect is slowly becoming extinct</a>.</p>
<p>All browsers combine HTML, ARIA and CSS to generate the so-called accessibility tree and <a href="https://www.smashingmagazine.com/2015/03/web-accessibility-with-accessibility-api/">pass it on to the operating system's accessibility API</a> (details vary slightly between platforms), which is then exposed to assistive software. Different assistive software takes the exposed information and uses it in different ways – or doesn't. Remember <code>aria-invalid="true"</code>? It's widely supported and safe to use! Expect it <em>doesn't</em> work if it is used on a <code><select></code> and if the assistive software is VoiceOver in Safari on iOS. Maybe. I haven't actually tested this myself, I'm using <a href="https://a11ysupport.io/tech/aria/aria-invalid_attribute#support-table-1">a11ysupport.io</a>, which lists this as unsupported on Nov 19, 2021. In any case, there are lots of nuances to be aware of when you try out your hard-learned HTML and ARIA skills in the real world. It's a bit like Chinese dialects in Hongkong: speaking Mandarin is definitely a good start, but Cantonese will supposedly get you further.</p>
<p>Last but not least, there's standard Twig and there's this special world of magic (not in the good way) Twig functions and blocks that is <a href="https://symfony.com/doc/current/form/form_themes.html">Symfony Form Themes</a>.</p>
<h2>It's okay to feel exhausted</h2>
<p>So. Six languages, with dialects. I don't actually know how to count the dialects, as it'd be excessive to multiply each browser engine (WebKit, Chromium, Gecko) with HTML, ARIA and CSS and then multiply the result again with each assistive software.</p>
<p>But thanks to my languages epiphany I certainly feel less inadequate at the end of this day.</p>
Building on the shoulders of giants2021-12-17T00:00:00-00:00https://annualbeta.com/blog/building-on-the-shoulders-of-giants/<p>Two weeks ago, I noticed a tweet by <a href="https://twitter.com/jlengstorf">Jason Lengstorf</a> about a Netlify project called <a href="https://dusty.domains/">Dusty Domains</a>. The idea: use a domain that you registered but never actually used, "ship a site to Netlify and turn that dusty domain you’ve been squatting on into real money for charity".</p>
<p>Well.</p>
<p>I have plenty domains, most of them unused. And I had had an idea about a particular one a few days earlier.</p>
<p>In 2019, when the world was … not good, but hey, pre-COVID! Anyways, in 2019 I registered <a href="http://heilpragmatiker.de/">heilpragmatiker.de</a> as a joke. It plays on the term <a href="https://en.wikipedia.org/wiki/Heilpraktiker">Heilpraktiker</a>, which is German for non-medical or alternative healing practitioner. I am extremely sceptic of most alternative or complementary health care methods and much prefer hard science. Heilpragmatiker is a pun in that vein, translating to "the pragmatic healer".</p>
<p>I decided to go with my tongue-in-cheek idea and launch a super tiny one-pager with a simple message: "Lasst euch impfen!" (Get vaccinated!).</p>
<p>I created an HTML file, put my message in there, centered it in the viewport (with CSS, no less) and <a href="https://app.netlify.com/drop">drag & drop deployed it to Netlify</a> (yes, really).</p>
<p>Done and dusted, I thought. But while I was waiting for DNS changes to propagate (they don't, actually, the waiting is for caches to expire) so Netlify could issue a SSL certificate, I got restless.</p>
<p>A proper Heilpragmatiker should do more than just shout something into the void. They should base their message on hard facts. Facts like current COVID data. I thought, "API!" and DuckDuckGo quickly gave me the <a href="https://api.corona-zahlen.org/docs/">Robert Koch-Institut COVID-19 API</a> by <a href="https://twitter.com/Marlon360">Marlon Lückert</a>.</p>
<p>The API has tons of data, but I only wanted the current 7-day incidence. The significance of the incidence as a data point is debatable for a lot of reasons, but it is still used widely in the media and easily recognisable.</p>
<p>I went back to my HTML file and got into a proper flow for a few hours. In the end, I did a bit more than only adding a single number:</p>
<ul>
<li>I switched to <a href="https://www.11ty.dev/">Eleventy</a>, a static site generator, and query the API via <a href="https://www.11ty.dev/docs/data-js/">JS data files</a> during the HTML build step. The actual website remains pure HTML and CSS, no JavaScript needed.</li>
<li>I noticed that the API also has an endpoint for the colour ranges used in maps. I like colours, so I decided to use the current applicable incidence colour as the website's background colour. I also like accessibility, so I have a function that (rather simplistically) determines whether my text needs to be black or white to achieve sufficient contrast against the dynamically determined background colour.</li>
<li>With all that colour, the text could use a bit more oomph. I used fluid type with <code>clamp()</code> and viewport units to tie my font-size to the viewport size (careful here: <a href="https://adrianroselli.com/2019/12/responsive-type-and-zoom.html">make sure that text zoom is still possible</a>!). <code>clamp()</code> is one of the many recent additions to CSS that are going to change the way we build the web.</li>
<li>When I set up the Netlify site to automatically deploy from my Github repository and checked out a deploy preview of my pull request, Firefox warned me about a missing favicon. Point to you, Firefox. I remembered something <a href="https://twitter.com/derSchepp/">@derSchepp</a> said in a recent podcast (<a href="https://wowirsindistvorne.show/web-performance-mit-christian-schepp-schaefer/">WWSIV #33</a>): you can use inline SVG data urls as placeholders for to-be-lazy-loaded images. I meshed this with another new capability of the web platform: SVG favicons. <a href="https://caniuse.com/link-icon-svg">Browser support for SVG favicons is still dodgy</a>, but hey, YOLO. It's a fun pet project. With a bit of tweaking, I got a SVG data string that always contains the incidence and colour currently shown on the website.</li>
<li>The elegant synchronicity of favicon and website content pushed me towards a related topic: social images. <em>Of course</em> I wanted Heilpragmatiker to shout their message in website previews around social media. Turns out that's not a hard problem: there is a <a href="https://github.com/11ty/api-screenshot">runtime service API</a> built by <a href="https://twitter.com/zachleat">Zach Leatherman</a> (also creator of Eleventy) that you can deploy to Netlify and query with URLs to receive a screenshot of the website. Mind-boggling, I know. I attach build timestamps to the URLs to get fresh images every day.</li>
<li>Speaking of builds: I used Zapier to set up a scheduler that triggers a Netlify build every morning at 06:00 am – about three hours after the COVID API data is refreshed.</li>
</ul>
<p>And there you have it: A really tiny website with a few hidden tricks. You can <a href="https://github.com/polarbirke/heilpragmatiker">have a look around the code</a>, if you like.</p>
<p>I had a lot of fun going down this rabbit hole – thanks for the push, Netlify!</p>
<p>PS: Get vaccinated, people.</p>