Manuel Matuzović - BlogMy Blog about accessibility, performance, and CSS architecture and layout.2023-09-07T07:15:54Zhttps://matuzo.at/Manuel Matuzovićmanuel@matuzo.atGetting started with CSS Font Loading2016-10-12T00:00:00+00:00https://www.matuzo.at/blog/getting-started-with-css-font-loading
<p>I was in the mood to learn something new and so I decided to take a look at the CSS Font Loading API.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CGetting+started+with+CSS+Font+Loading%E2%80%9D">blog@matuzo.at</a>.</p> I totally forgot about print style sheets2016-11-15T00:00:00+00:00https://www.matuzo.at/blog/i-totally-forgot-about-print-style-sheets
<p>A small collection of useful CSS techniques and a quick reminder that print style sheets are still a thing.</p><div class="quote">
<blockquote>another old post (who cares?) that was originally on medium and now is on the author's domain blah blah</blockquote>
<p>-Random reply guy on Hacker News</p>
</div>
<p><a href="https://twitter.com/AaronGustafson">Aaron Gustafson</a> recently sent a <a href="https://twitter.com/AaronGustafson/status/788073583528538112">tweet</a> to <a href="http://indiegogo.com">Indiegogo</a> in which he pointed out that their order details pages aren’t usable when printed.</p>
<p><img src="/images/aaron_tweet.png" alt="Tweet: "Dear @Indiegogo
, please take a look at how your Order Details pages print. It really sucks right now." and a screenshot of a print preview filled with huge icons." /></p>
<p>When I saw this tweet it struck me, because I realized that it has been a long time since I have optimized a page for print or even spared a thought on checking.<br />
Maybe it’s due to the constant resizing of our browsers and making sure that our sites work perfectly in all shapes and sizes or maybe it’s just because I rarely print web pages myself. Whatever it is, I totally forgot about print style sheets and that’s bad.</p>
<p>Optimizing web pages for print is important because we want our sites to be as accessible as possible, no matter the medium. We <a href="https://adactio.com/journal/11409">shouldn’t make assumptions</a> about our users and their behavior. People still print web pages. Just think about articles or blog posts, recipes, contact information, and directions or real estate sites. Someone somewhere will eventually try to print one of the pages you made.</p>
<blockquote>
<p><em>I gave up on home printers a long time ago because they always seemed to break after ten minutes of use. But not everyone is like me,…</em></p>
</blockquote>
<p><em>-Heydon Pickering (Inclusive Design Patterns)</em></p>
<p>If you see yourself in a similar position as I did, this post will hopefully serve you well as a quick refresher. If you haven’t yet optimized pages for print with CSS, the following tips will get you started.</p>
<h2>1. Embedding print style sheets</h2>
<p>The best way to embed print styles is to declare the <code>@media</code> rule in your CSS.</p>
<pre><code class="language-css">body {
font-size: 18px;
}
@media print {
/* print styles go here */
body {
font-size: 28px;
}
}</code></pre>
<p>Alternatively you can embed your print styles in HTML, but this will give you an extra request.</p>
<pre><code class="language-html"><link media="print" href="print.css" /></code></pre>
<h2>2. Testing</h2>
<p>You don’t have to print a page every time you make a small change. Depending on your browser you can export the page as a PDF, show a print preview or even debug directly in the browser.</p>
<p><strong>Update November 6, 2019</strong>: Here’s a detailed post (<a href="https://css-tricks.com/can-you-view-print-stylesheets-applied-directly-in-the-browser/">Can you view print stylesheets applied directly in the browser? </a>) on how to emulate print styles in 2019 by <a href="https://twitter.com/chriscoyier">Chris</a>.</p>
<p>For debugging print styles in Firefox open the <em>Developer Toolbar</em> (Shift + F2 or Tools > Web Developer > Developer Toolbar) and enter media emulate print in the input field at the bottom of the browser window and press enter. The active tab will act as if the <em>media type</em> was print until you close it or refresh the page. <em>(<strong>Update October 20, 2018</strong>: This doesn’t work anymore in Firefox 63+ since the Developer Toolbar has been removed)</em></p>
<p><img src="/images/ff.png" alt="print style emulation in Firefox" /></p>
<p>In Chrome open DevTools (CMD + Opt + I (macOS) or Ctrl + Shift + I (Windows) or View > Developer > Developer Tools) and show the console drawer (Esc), open the rendering pane, check <em>Emulate CSS Media</em> and select <em>Print</em>.</p>
<p><img src="/images/chrome.png" alt="print style emulation in Chrome" /><em>print style emulation in Chrome</em></p>
<h2>3. Absolute units</h2>
<p>Absolute units are bad for screens but great for print. In print style sheets it’s completely safe and recommended to use absolute <a href="https://developer.mozilla.org/de/docs/Web/CSS/length">units</a> like cm, mm, in, pt or pc.</p>
<pre><code class="language-css">section {
margin-bottom: 2cm;
}</code></pre>
<h2>4. Page specific rules</h2>
<p>It’s possible to define properties specific to the page like dimensions, orientation, and margins with the <code>@page</code> rule. This comes in very handy if you want all pages to have a certain margin.</p>
<pre><code class="language-css">@media print {
@page {
margin: 1cm;
}
}</code></pre>
<p>The <code>@page</code> rule is part of the <a href="https://drafts.csswg.org/css-page-3/">Paged Media Module</a>, which offers all kinds of awesome stuff, e.g. selecting the first printed page or blank pages, positioning elements in the corners of a page and <a href="https://www.smashingmagazine.com/2015/01/designing-for-print-with-css/">more</a>. You can even use it to <a href="https://alistapart.com/article/building-books-with-css3">make books</a>.</p>
<h2>5. Controlling page breaks</h2>
<p>Since printed pages aren’t endless like web pages, content will eventually break on one page and continue on the next page. We have 5 properties to control what happens in that case.</p>
<h3>Page breaks before an element</h3>
<p>If we want an element to always be at the beginning of a page, we can force a page break with page-break-before.</p>
<pre><code class="language-css">section {
page-break-before: always;
}</code></pre>
<p><a href="https://developer.mozilla.org/en/docs/Web/CSS/page-break-before">page-break-before on MDN</a></p>
<h3><strong>Page breaks after an element</strong></h3>
<p>page-break-after lets us force or avoid page breaks after an element.</p>
<pre><code class="language-css">h2 {
page-break-after: always;
}</code></pre>
<p><a href="https://developer.mozilla.org/en/docs/Web/CSS/page-break-after">page-break-after on MDN</a></p>
<h3><strong>Page breaks inside an element</strong></h3>
<p>This property is great if you want to avoid an element to get split between two pages</p>
<pre><code class="language-css">ul {
page-break-inside: avoid;
}</code></pre>
<p><a href="https://developer.mozilla.org/en/docs/Web/CSS/page-break-inside">page-break-inside on MDN</a></p>
<h3><strong>Widows and Orphans</strong></h3>
<p>Sometimes you may not want to force a page break, but at least control how many lines are displayed on the current or the next page.<br />
For example, if the last line of a paragraph doesn’t fit on the current page, the last two lines of that paragraph will be printed on the next page, even though just the last one didn’t fit. That’s because the property that controls this – widows – is set to 2 by default. We can change that.</p>
<pre><code class="language-css">p {
widows: 4;
}</code></pre>
<p>If it’s the other way around and only one line fits on the current page, the whole paragraph will be printed on the next page. The property responsible for this behavior is orphans and its default value is 2 as well.</p>
<pre><code class="language-css">p {
orphans: 3;
}</code></pre>
<p>The code above means that at least 3 lines have to fit on the current page for the paragraph not to move to the next page as a whole.</p>
<p>There’s a <a href="http://codepen.io/matuzo/pen/oYvBjN">CodePen</a> ready with some examples. (<a href="http://s.codepen.io/matuzo/debug/oYvBjN">Debug version</a> for easier testing)</p>
<p><a href="http://caniuse.com/#feat=css-page-break">Not all properties and values work in every browser</a>, you should check your print styles in different browsers.</p>
<h2>6. Resetting styles</h2>
<p>It makes sense to reset some styles like background-color, box-shadow or color for print.</p>
<p>Here’s an excerpt from the <a href="https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/main.css">HTML5 Boilerplate print style sheet</a>:</p>
<pre><code class="language-css">*,
*:before,
*:after,
*:first-letter,
p:first-line,
div:first-line,
blockquote:first-line,
li:first-line {
background: transparent !important;
color: #000 !important;
box-shadow: none !important;
text-shadow: none !important;
}</code></pre>
<p>Print style sheets are one of the few exceptions where it’s OK to use !important ;).</p>
<h2>7. Removing unnecessary content</h2>
<p>To avoid unnecessary waste of ink you should remove irrelevant stuff like presentational content, ads, navigation, etc. with display: none.</p>
<p>You may even want to just show the main content and hide everything else.</p>
<pre><code class="language-css">body > *:not(main) {
display: none;
}</code></pre>
<h2>8. Revealing URLs in links</h2>
<p>Printed Links are completely useless if you don’t know where there are leading.</p>
<p>It’s pretty easy to display a links target next to its text.</p>
<pre><code class="language-css">a[href]:after {
content: ' (' attr(href) ')';
}</code></pre>
<p>Of course, this will display relative links, absolute links to your site, anchors, etc. as well. Something like this might serve better:</p>
<pre><code class="language-css">a[href^='http']:not([href*='mywebsite.com']):after {
content: ' (' attr(href) ')';
}</code></pre>
<p>Looks <em>interesting</em>, I know. These lines mean: Display the value of the <code>href</code> attribute next to every link that has a <code>href</code> attribute, which starts with <em>http</em>, but doesn't have <em>mywebsite.com</em> in its value.</p>
<h2>9. Revealing expansions in abbreviations</h2>
<p>Abbreviations should be wrapped in <code><abbr></code> elements and their expansions included in the title attribute. It makes sense to display those on printed pages.</p>
<pre><code class="language-css">abbr[title]:after {
content: ' (' attr(title) ')';
}</code></pre>
<h2>10. Forcing background printing</h2>
<p>Usually, browsers won’t print background colors and background images if you don’t tell them to, but sometimes you may want to force printing them. The not standardized <a href="https://developer.mozilla.org/de/docs/Web/CSS/-webkit-print-color-adjust">property</a> print-color-adjust lets you overwrite the default settings for some browsers.</p>
<pre><code class="language-css">header {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}</code></pre>
<h2>11. Media Queries</h2>
<p>If you write your media queries like in the following example be aware that the CSS rules in this media query won’t apply to the print style sheet.</p>
<pre><code class="language-css">@media screen and (min-width: 48em) {
/* screen only */
}</code></pre>
<p>Why you ask? Because the CSS rules only apply if the <code>min-width</code> is <code>48em</code> and the media-type <code>screen</code>. By getting rid of the screen keyword the media query is only limited by the <code>min-width</code>.</p>
<pre><code class="language-css">@media (min-width: 48em) {
/* all media types */
}</code></pre>
<h2>12. Printing Maps</h2>
<p>Current versions of Firefox and Chrome are able to print maps, but Safari for example, isn’t. Some services provide <a href="http://staticmapmaker.com/">static maps</a> which you could use instead.</p>
<pre><code class="language-css">.map {
width: 400px;
height: 300px;
background-image: url('http://maps.googleapis.com/maps/api/staticmap?center=Wien+Floridsdorf&zoom=13&scale=false&size=400x300&maptype=roadmap&format=png&visual_refresh=true');
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}</code></pre>
<h2>13. QR codes</h2>
<p><a href="https://www.smashingmagazine.com/2013/03/tips-and-tricks-for-print-style-sheets/#print-qr-codes-for-easy-url-references">This Smashing Magazine article</a> has some nice tips. One of them is to provide a QR Code for printed pages so that users don’t have to type the whole URL to get to the live version.</p>
<h2>Bonus: Printing non-optimized pages</h2>
<p>During my research, I found a great tool which helps you print non-optimized pages. With the <a href="https://css-tricks.github.io/The-Printliminator/">Printliminator</a> you can remove elements simply by clicking on them.</p>
<p>There’s a <a href="https://www.youtube.com/watch?v=Dt8hpqEIL1c">Demo on YouTube</a> and the project on <a href="https://github.com/CSS-Tricks/The-Printliminator">Github</a>.</p>
<h2>Bonus II: Gutenberg</h2>
<p>If you are into frameworks, you may like <a href="https://github.com/BafS/Gutenberg">Gutenberg</a> which makes optimizing for print a little easier.</p>
<h2>Bonus III: Hartija</h2>
<p>There’s another framework for print style sheets by <a href="undefined">Vladimir Carrer</a> called <a href="https://github.com/vladocar/Hartija---CSS-Print-Framework">Hartija</a>.</p>
<p>That’s it! Here’s the link to the <a href="http://codepen.io/matuzo/pen/oYvBjN?editors=1100">CodePen</a> I made (<a href="http://s.codepen.io/matuzo/debug/oYvBjN">Debug version</a>) if you want to see some of this stuff in action. I hope that you have enjoyed this article.</p>
<p>PS: Thanks to <a href="https://twitter.com/eva_trostlos">Eva</a> for redacting my writing and Mario for the Gutenberg tip.</p>
<h3>Resources</h3>
<ul>
<li>
<p><a href="https://www.youtube.com/watch?v=jF-OQ-BrIAM">https://www.youtube.com/watch?v=jF-OQ-BrIAM</a></p>
</li>
<li>
<p><a href="https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/main.css#L217">https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/main.css#L217</a></p>
</li>
<li>
<p><a href="https://www.smashingmagazine.com/2013/03/tips-and-tricks-for-print-style-sheets/">https://www.smashingmagazine.com/2013/03/tips-and-tricks-for-print-style-sheets/</a></p>
</li>
<li>
<p><a href="http://stackoverflow.com/questions/9519556/faster-way-to-develop-and-test-print-stylesheets-avoid-print-preview-every-time">http://stackoverflow.com/questions/9519556/faster-way-to-develop-and-test-print-stylesheets-avoid-print-preview-every-time</a></p>
</li>
<li>
<p><a href="https://www.smashingmagazine.com/2015/01/designing-for-print-with-css/">https://www.smashingmagazine.com/2015/01/designing-for-print-with-css/</a></p>
</li>
<li>
<p><a href="https://github.com/BafS/Gutenberg">https://github.com/BafS/Gutenberg</a></p>
</li>
<li>
<p><a href="https://helloanselm.com/2014/unified-media-queries/">https://helloanselm.com/2014/unified-media-queries/</a></p>
</li>
</ul>
<p><em>Update November 17, 2016: Added Hartija Framework.</em><br />
<em>Update October 20, 2018: Info that the Developer Toolbar in Firefox has been removed.</em><br />
<em>Update November 6, 2019: Added link to browser emulation post on CSS Tricks.</em></p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CI+totally+forgot+about+print+style+sheets%E2%80%9D">blog@matuzo.at</a>.</p> Writing HTML with accessibility in mind2016-12-13T00:00:00+00:00https://www.matuzo.at/blog/writing-html-with-accessibility-in-mind
<p>An introduction to web accessibility. Tips on how to improve your markup and provide users with more and betters ways to navigate and interact with your site.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWriting+HTML+with+accessibility+in+mind%E2%80%9D">blog@matuzo.at</a>.</p> Writing JavaScript with accessibility in mind2017-02-12T00:00:00+00:00https://www.matuzo.at/blog/writing-javascript-with-accessibility-in-mind
<p>Tips on how to improve the accessibility of your JavaScript components and provide users with more and better ways to interact with your website or web app.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWriting+JavaScript+with+accessibility+in+mind%E2%80%9D">blog@matuzo.at</a>.</p> A Collection of Interesting Facts about CSS Grid Layout2017-07-21T00:00:00+00:00https://www.matuzo.at/blog/a-collection-of-interesting-facts-about-css-grid-layout
<p>A few weeks ago I held a CSS Grid Layout workshop. Since I'm, like most of us, also pretty new to the topic, I learned a lot while preparing the slides and demos.</p>
<p>I decided to share some of the stuff that was particularly interesting to me, with you.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CA+Collection+of+Interesting+Facts+about+CSS+Grid+Layout%E2%80%9D">blog@matuzo.at</a>.</p> Progressively Enhancing CSS Layout: From Floats To Flexbox To Grid2017-07-24T00:00:00+00:00https://www.matuzo.at/blog/progressively-enhancing-css-layout-from-floats-to-flexbox-to-grid
<p>When can I start using CSS grid layout?” “Too bad that it’ll take some more years before we can use grid in production.” “Do I need Modernizr in order to make websites with CSS grid layout?” “If I wanted to use grid today, I’d have to build two to three versions of my website.” The CSS grid layout module is one of the most exciting developments since responsive design. We should try to get the best out of it as soon as possible, if it makes sense for us and our projects.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CProgressively+Enhancing+CSS+Layout%3A+From+Floats+To+Flexbox+To+Grid%E2%80%9D">blog@matuzo.at</a>.</p> The Difference Between Explicit and Implicit Grids2017-08-10T00:00:00+00:00https://www.matuzo.at/blog/the-difference-between-explicit-and-implicit-grids
<p>Grid Layout finally gives us the ability to define grids in CSS and place items into grid cells. This on its own is great, but the fact that we don't have to specify each track and we don't have to place every item manually makes the new module even better. Grids are flexible enough to adapt to their items.</p>
<p>This is all handled by the so called explicit and implicit grid.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CThe+Difference+Between+Explicit+and+Implicit+Grids%E2%80%9D">blog@matuzo.at</a>.</p> Writing CSS with Accessibility in Mind2017-09-17T00:00:00+00:00https://www.matuzo.at/blog/writing-css-with-accessibility-in-mind
<p>About a year ago I started to focus more on web accessibility. The most effective method of learning for me is teaching others. That’s one of the reasons why I’m talking at meetups and conferences and why I’m writing articles about the topic. I wrote about Progressive Enhancement for Smashing Magazine and about accessibility basics here on Medium. This article is the third in a series of collections of accessibility tips. They’re in no particular order, you can read Writing HTML with accessibility in mind and Writing JavaScript with accessibility in mind now or later if you’re interested.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWriting+CSS+with+Accessibility+in+Mind%E2%80%9D">blog@matuzo.at</a>.</p> My Accessibility Journey: What I’ve Learned So Far2018-02-06T00:00:00+00:00https://www.matuzo.at/blog/my-accessibility-journey-what-ive-learned-so-far
<p>Last year I gave a talk about CSS and accessibility at the stahlstadt.js meetup in Linz, Austria. Afterward, an attendee asked why I was interested in accessibility: Did I or someone in my life have a disability?</p>
<p>I’m used to answering this question—to which the answer is no—because I get it all the time. A lot of people seem to assume that a personal connection is the only reason someone would care about accessibility.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CMy+Accessibility+Journey%3A+What+I%E2%80%99ve+Learned+So+Far%E2%80%9D">blog@matuzo.at</a>.</p> Another Collection of Interesting Facts about CSS Grid Layout2018-04-13T00:00:00+00:00https://www.matuzo.at/blog/another-collection-of-interesting-facts-about-css-grid-layout
<p>Last year, I assembled A Collection of Interesting Facts about CSS Grid Layout after giving a workshop. This year, I worked on another workshop and I've learned some more exciting facts about the layout spec we all so love.</p>
<p>Of course, I'm not going to keep my knowledge to myself. I'm happy to share my findings once again with you, the CSS-Tricks community.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CAnother+Collection+of+Interesting+Facts+about+CSS+Grid+Layout%E2%80%9D">blog@matuzo.at</a>.</p> I Threw Away my Mouse2018-12-16T00:00:00+00:00https://www.matuzo.at/blog/i-threw-away-my-mouse
<p>Last year I attended JS Conf Budapest and I watched many great talks but “YES! Your site can (and should) be accessible” by Laura Carvajal was the most thought-provoking talk for me. Laura explained how the Financial Times made accessibility a core part of their development process and she shared several lessons she and her team had learned. In her third lesson Throw away your mouse, Laura mentioned that just testing with the keyboard wasn’t enough and that only going keyboard-only all the time made a difference.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CI+Threw+Away+my+Mouse%E2%80%9D">blog@matuzo.at</a>.</p> Hello World!2019-01-04T00:00:00+00:00https://www.matuzo.at/blog/hello-world
<p>It happened. I finally have a website.</p>
<p>Of course, it's not my first website but the first one in a long time. My very first personal site went online about 17 years ago. It was a table-based layout with no CSS at all. All styling happened by adding HTML attributes.</p><pre><code class="language-html"><font face="arial" size="4">
<table
border="2"
cellspacing="2"
cellpadding="0"
width="550"
bordercolor="#000000"
bgcolor="#004600"
>
...
</table>
</font></code></pre>
<p>It was online for about 2 years. After that, the only thing I did was to change the under construction graphic periodically. As it turns out, I'm really bad at getting things done.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/w_700/articles/matuzoat2022.png" alt="My first personal website. A all-green table-based layout." /></p>
<p>Fast forward 10 years, I was at my second conference ever, at Smashing Conf Freiburg 2014. I was so inspired by the great talks I had seen, that I decided to set up a WordPress-based blog in one of the breaks to share my notes. The theme was slow and the design was boring but I had a website, which was great because I had the chance to share my knowledge.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/w_700/articles/matuzoat2014.png" alt="My second personal site. A web development blog in German." /></p>
<p>Again, I lost interest after 2 years because I didn’t like the design, the site was slow and there where cooler platforms to publish your articles at. I moved to <a href="https://medium.com/@matuzo/">Medium</a>.</p>
<p>Medium is great for reach but its accessibility isn’t great and there’s an important thing missing: personality. Medium is just content. Besides writing, you can’t express yourself and show your style and your skills. You can’t experiment, you can’t try out new things, optimize performance, accessibility and user experience.</p>
<p>As already mentioned, I'm bad at getting things done. I always wanted to have a nice looking, fast, and all in all cool website but it just didn’t happen. Not until now! In 2019 I finally launched my new blog. It's by far not done but it's live and will <a href="https://github.com/matuzo/matuzoat">work on it regularly</a> and post articles.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CHello+World%21%E2%80%9D">blog@matuzo.at</a>.</p> The Dark Side of the Grid (Part 1)2019-02-07T00:00:00+00:00https://www.matuzo.at/blog/the-dark-side-of-the-grid
<p><a href="https://www.w3.org/TR/css-grid-1/">CSS Grid Layout</a> is one of the most exciting recent CSS specifications because of its flexibility, extent, and power. It makes our lives so much easier but it also creates new dangers regarding user experience and accessibility.</p><div class="post" data-theme="dark-side-of-the-grid">
<h2>Preface</h2>
<p>It has already been two years since the first browsers, Chromium 57 and Firefox 52, shipped CSS Grid Layout un-prefixed. Many developers have experimented with it or are using it in production already. More will come as soon as support for Internet Explorer 10 and 11 becomes less important.\<br />
Grid offers many ways of building layouts and it challenges us to rethink the way we approach them. This flexibility is great for our development experience, but it may come at the cost of user experience and accessibility if we don’t use it responsibly.</p>
<p>This series of articles will give you an overview of potential implementation pitfalls, or in other words, the dark side of the grid.</p>
<div class="demo u-full-width js-demo">
<div class="a-title">
<div class="a-title-white-light"></div>
<div class="a-title-grid"><div class="a-title-grid-inner"></div></div>
<div class="a-title-rainbow"></div>
<div class="a-title-text"><svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1220.82 157.8"><g fill="#FFF"><path d="M30.33 48.56c.09-.19.21-.66.37-1.4s.33-1.54.52-2.4c.18-.86.36-1.66.53-2.4.17-.74.3-1.21.39-1.4v-.36c0-.09-.01-.21-.03-.39-.02-.17-.06-.33-.13-.47-.06-.14-.15-.24-.26-.31-.11-.06-.25-.04-.42.06l-24.24 8.9.77.71-7.83.45c-.02-.37.05-.78.21-1.26.16-.47.42-.83.79-1.06 2.47-.79 5.06-1.53 7.78-2.21 2.72-.68 5.42-1.41 8.12-2.21 2.7-.79 5.32-1.69 7.88-2.69 2.56-1 4.92-2.22 7.09-3.66 0-.19-.01-.51-.03-.95-.02-.44-.03-.94-.03-1.5s.02-1.14.05-1.74c.03-.6.11-1.15.23-1.66.12-.5.27-.92.47-1.24s.45-.49.77-.52c.09 0 .21.01.39.03.17.02.34.06.5.13s.31.14.44.23.19.17.19.26l.35 5.99c.28 0 .54-.01.77-.03.21-.02.42-.03.63-.03.2 0 .36-.01.47-.03.09 0 .27-.05.56-.15s.6-.2.92-.32c.32-.12.62-.23.9-.32.28-.1.47-.15.58-.15l60.07-11.73c.62-.09 1.59-.22 2.9-.4 1.31-.18 2.84-.38 4.58-.58 1.74-.2 3.62-.42 5.64-.64 2.02-.23 4.04-.45 6.07-.66 2.03-.21 4-.4 5.9-.56s3.6-.28 5.11-.37l3.38-.19.77.71c-2.69 1.1-5.49 1.89-8.43 2.38-2.93.49-5.92.84-8.98 1.05-3.05.2-6.13.34-9.25.42-3.12.08-6.21.24-9.28.5-3.07.26-6.09.68-9.06 1.27-2.96.59-5.82 1.5-8.57 2.72L44.6 35.7l-8.86 2.77-.71.81c-.15.73-.4 1.79-.74 3.19-.34 1.4-.76 3.03-1.26 4.9-.49 1.87-1.05 3.92-1.66 6.14s-1.25 4.52-1.92 6.9c-.67 2.37-1.34 4.76-2.03 7.17-.69 2.41-1.35 4.72-2 6.94s-1.26 4.3-1.84 6.24-1.1 3.62-1.55 5.06c-.45 1.44-.83 2.57-1.13 3.4-.3.83-.48 1.24-.55 1.24-.21-3.54-.06-7.07.47-10.59.53-3.51 1.28-6.99 2.26-10.43.98-3.44 2.11-6.84 3.38-10.2 1.28-3.36 2.57-6.68 3.88-9.94l-.01-.74zm117.46-30.14c-.64.39-1.4.66-2.26.82-.86.16-1.73.28-2.61.35-.88.08-1.75.13-2.59.16-.85.03-1.6.09-2.24.18v-1.51h9.7z"/><path d="M74.87 64.49c-.18-.46-.42-.83-.71-1.1-.29-.27-.65-.45-1.08-.53-.43-.09-.96-.08-1.58.03-2.23.39-4.2 1.02-5.9 1.89-1.7.87-3.18 1.91-4.45 3.13-1.27 1.21-2.34 2.56-3.22 4.04-.88 1.48-1.61 3.03-2.19 4.64s-1.03 3.25-1.35 4.93-.56 3.32-.71 4.93c-.04.54-.16.91-.35 1.11s-.42.28-.68.24c-.26-.04-.53-.19-.82-.45s-.58-.58-.85-.98c-.28-.4-.53-.85-.74-1.35-.21-.5-.37-1.03-.45-1.56-.04-.17-.08-.33-.1-.48-.02-.15-.04-.28-.06-.39l-.06-.39L78.6 7.98c0 1.83-.2 3.82-.6 5.99-.4 2.17-.92 4.43-1.58 6.78-.66 2.35-1.41 4.75-2.27 7.2-.86 2.45-1.75 4.87-2.67 7.27-.92 2.4-1.85 4.72-2.79 6.96-.93 2.25-1.8 4.34-2.61 6.28-.81 1.94-1.51 3.7-2.13 5.27-.61 1.57-1.07 2.86-1.37 3.87l-1.42 4.9c.92-1.14 1.75-2.13 2.47-2.96.72-.84 1.47-1.55 2.24-2.14.77-.59 1.64-1.07 2.61-1.45.97-.38 2.16-.69 3.58-.95 1.05-.19 2.01-.15 2.87.13.86.28 1.63.71 2.3 1.31.68.59 1.28 1.3 1.8 2.13.53.83.98 1.69 1.35 2.59.38.9.69 1.8.93 2.71.25.9.44 1.72.56 2.45.19 1.05.28 2.29.27 3.71-.01 1.42-.12 2.9-.34 4.43-.21 1.54-.54 3.07-.98 4.59-.44 1.53-1.01 2.92-1.71 4.17-.7 1.26-1.53 2.32-2.5 3.19s-2.08 1.41-3.35 1.63c.58-1.83 1.07-3.44 1.47-4.85.4-1.41.71-2.7.95-3.87s.39-2.26.47-3.25c.08-1 .09-2 .05-3.01-.04-1.01-.15-2.07-.31-3.17-.16-1.11-.36-2.35-.6-3.72-.09-.66-.23-1.22-.42-1.68zm43.69-64.5v1.13c.11-.06.18-.14.23-.23.04-.09.09-.18.15-.27.05-.1.11-.2.18-.31.06-.11.17-.21.32-.32h-.88zm10.73 2.09c.09 0 .13-.03.13-.1s-.04-.1-.13-.1-.13.03-.13.1.04.1.13.1zm.87 2.05c0 .1.03.15.1.15.09 0 .13-.05.13-.15s-.04-.15-.13-.15c-.07.01-.1.05-.1.15zM86.87 84.13c.34-.49.9-.96 1.66-1.4.76-.44 1.63-.85 2.59-1.22.97-.38 2-.74 3.09-1.08 1.1-.34 2.15-.67 3.16-.98 1.01-.31 1.94-.6 2.8-.87.86-.27 1.54-.53 2.03-.79 1.01-2.73 2.07-5.12 3.19-7.17s2.29-3.84 3.51-5.37c1.22-1.53 2.51-2.81 3.87-3.87 1.35-1.05 2.77-1.95 4.25-2.71 1.48-.75 3.03-1.39 4.66-1.92 1.62-.53 3.32-1 5.11-1.43.17-.04.5-.06 1-.06.49 0 1.03.01 1.6.02.57.01 1.1.02 1.6.03.49.01.84.02 1.03.02.56.19.95.54 1.18 1.03.23.49.36 1.02.4 1.58.04.56.04 1.09-.02 1.6-.05.5-.08.85-.08 1.05 0 1.5-.39 2.81-1.16 3.93-.77 1.12-1.78 2.12-3.01 3.01-1.24.89-2.61 1.71-4.12 2.45s-3.01 1.47-4.5 2.19c-1.48.72-2.86 1.48-4.12 2.27-1.27.79-2.28 1.69-3.03 2.67l3 1.51c.13 0 .53.04 1.19.13.67.09 1.48.19 2.43.32.96.13 2.01.27 3.16.42 1.15.15 2.27.29 3.35.42s2.08.24 3 .32c.91.09 1.62.13 2.11.13 3.16 0 6.26-.13 9.3-.4 3.04-.27 6.06-.56 9.07-.89 3.01-.32 6.03-.62 9.07-.89s6.14-.4 9.3-.4h6.74c-4.17 1.25-8.41 2.34-12.73 3.27s-8.56 1.72-12.73 2.35c-4.17.63-8.21 1.11-12.12 1.42-3.91.31-7.54.47-10.89.47-1.87 0-3.69-.08-5.46-.24s-3.52-.33-5.25-.52c-1.73-.18-3.44-.35-5.12-.52-1.69-.16-3.37-.24-5.04-.24-.99 0-1.96.1-2.9.31-.95.2-1.87.46-2.79.76-.91.3-1.82.63-2.71.98-.89.35-1.79.68-2.71.98-.91.3-1.84.55-2.79.76-.95.2-1.91.31-2.9.31-.56 0-1.03-.14-1.42-.42s-.68-.61-.87-1-.29-.79-.29-1.22c-.01-.43.1-.8.31-1.1zm39.93-23.55c-.99 0-2.1.12-3.34.37-1.24.25-2.49.6-3.75 1.05-1.27.45-2.5 1.01-3.71 1.68-1.2.67-2.28 1.41-3.22 2.24-.95.83-1.7 1.74-2.27 2.72-.57.99-.85 2.04-.85 3.16v1.51c.62-.26 1.48-.61 2.56-1.06 1.08-.45 2.27-.98 3.54-1.58 1.28-.6 2.57-1.28 3.87-2.03 1.3-.75 2.48-1.56 3.54-2.42 1.06-.86 1.93-1.76 2.61-2.71.68-.95 1.02-1.92 1.02-2.93zm54.43 17.2h-3l1.48-1.48 1.52 1.48zm8.93-2.22c.17 0 .3.08.39.23.09.15.13.32.13.52s-.04.37-.13.52c-.09.15-.21.23-.39.23-.19 0-.33-.08-.42-.23-.09-.15-.13-.32-.13-.52s.04-.37.13-.52c.08-.15.22-.23.42-.23z"/><path d="M158.3 98.44c4.17-1.53 7.94-2.88 11.33-4.08s6.45-2.33 9.18-3.42c2.74-1.08 5.2-2.18 7.38-3.29s4.16-2.35 5.95-3.72c1.78-1.37 3.41-2.95 4.88-4.74 1.47-1.78 2.86-3.89 4.17-6.32s2.58-5.24 3.82-8.44c1.24-3.2 2.51-6.92 3.82-11.15-.99-.32-2.21-.5-3.67-.53-1.46-.03-2.98.04-4.56.23-1.58.18-3.13.45-4.66.81-1.53.35-2.87.77-4.03 1.24l.81 1.22-2.45 1.71c-.52.37-1.02.52-1.5.47-.48-.05-.93-.24-1.35-.56s-.8-.74-1.14-1.24c-.34-.5-.63-1.03-.85-1.58s-.39-1.08-.48-1.6-.11-.95-.05-1.29c.02-.13.05-.28.08-.45.03-.17.1-.33.19-.48.1-.15.23-.27.39-.37s.37-.12.63-.08l24.49-1.39 5.19-16.66c1.18.39 2.05.93 2.61 1.64.56.71.9 1.53 1.02 2.45.12.92.08 1.91-.13 2.95-.2 1.04-.48 2.08-.82 3.13-.34 1.04-.71 2.05-1.11 3.01-.4.97-.71 1.83-.95 2.58 2.08.21 3.94.38 5.56.5 1.62.12 3.11.23 4.46.32s2.64.19 3.85.29c1.21.1 2.47.21 3.75.35 1.29.14 2.68.32 4.17.55 1.49.23 3.2.51 5.11.85.52.09 1.14.21 1.87.39.73.17 1.49.39 2.27.66s1.56.59 2.32.97c.76.38 1.42.81 1.98 1.31.56.49.98 1.06 1.27 1.69.29.63.37 1.35.24 2.14-.21 1.16-.62 2.2-1.21 3.11-.59.91-1.31 1.74-2.14 2.47-.84.73-1.76 1.38-2.76 1.95-1 .57-2.01 1.09-3.05 1.56-3.46 1.27-6.88 2.7-10.26 4.3-3.38 1.6-6.77 3.19-10.17 4.77-3.4 1.58-6.82 3.05-10.28 4.42-3.46 1.36-6.98 2.45-10.57 3.27-.17.52-.39 1.24-.64 2.16-.26.92-.58 1.84-.97 2.76-.39.91-.85 1.72-1.39 2.42-.54.7-1.15 1.07-1.84 1.11-.28-.49-.5-1.04-.66-1.63-.16-.59-.29-1.19-.37-1.8-.09-.61-.14-1.22-.16-1.84-.02-.61-.01-1.2.03-1.76-1.38.19-2.85.6-4.43 1.21s-3.22 1.34-4.91 2.19c-1.7.85-3.44 1.77-5.22 2.77-1.78 1-3.58 1.99-5.38 2.98-1.8.99-3.61 1.93-5.41 2.82-1.8.89-3.57 1.64-5.29 2.26-1.72.61-3.38 1.05-4.98 1.32-1.6.27-3.12.27-4.56.02.11-.52.31-1 .61-1.47.31-.47.63-.94.97-1.44zM214.73 54c-.34 1.57-.81 3.12-1.39 4.66-.58 1.54-1.2 3.07-1.87 4.59-.67 1.53-1.32 3.05-1.97 4.58-.64 1.53-1.2 3.04-1.68 4.54-.04.17-.16.47-.34.89-.18.42-.4.89-.64 1.42-.25.53-.52 1.08-.81 1.68-.29.59-.56 1.16-.82 1.69-.26.54-.48 1.01-.68 1.42-.19.41-.31.7-.35.87 3.18-1.16 6.01-2.2 8.49-3.11 2.48-.91 4.71-1.74 6.67-2.48s3.74-1.43 5.32-2.06c1.58-.63 3.06-1.25 4.45-1.84 1.39-.59 2.73-1.19 4.03-1.79 1.3-.6 2.66-1.26 4.08-1.98s2.95-1.51 4.59-2.37c1.64-.86 3.51-1.85 5.59-2.96.15-.11.36-.26.63-.47.27-.2.54-.44.82-.69.28-.26.53-.52.76-.79.23-.27.37-.53.44-.79-.06-.73-.34-1.36-.82-1.89-.48-.53-1.09-.97-1.82-1.34-.73-.37-1.54-.67-2.42-.92-.88-.25-1.73-.45-2.55-.6-.82-.15-1.57-.26-2.26-.34-.69-.08-1.2-.15-1.55-.21-1.83-.32-3.74-.63-5.75-.93-2.01-.3-4.04-.49-6.09-.56-2.05-.08-4.1.01-6.14.26-2.03.24-4.01.75-5.92 1.52zM279.63 85c-.3-.15-.76-.24-1.37-.26-.61-.02-1.31.01-2.08.08-.77.08-1.6.17-2.47.29-.87.12-1.71.23-2.53.32-.82.1-1.57.17-2.26.23-.69.05-1.25.05-1.68-.02-.77-.13-1.5-.29-2.18-.5-.68-.2-1.25-.51-1.71-.92-.46-.41-.79-.92-1-1.55-.2-.62-.23-1.42-.08-2.38l.32-1.93c.52-.56 1.39-1.14 2.61-1.76s2.6-1.24 4.14-1.87c1.54-.63 3.13-1.27 4.79-1.92 1.65-.64 3.17-1.28 4.56-1.9 1.39-.62 2.54-1.23 3.46-1.82.92-.59 1.43-1.15 1.51-1.69.17-1.05.11-2-.19-2.85-.3-.85-.75-1.61-1.34-2.27-.59-.67-1.31-1.25-2.14-1.76-.84-.5-1.7-.93-2.59-1.29-.89-.35-1.79-.64-2.69-.87s-1.72-.4-2.45-.53c-1.27-.19-2.36-.32-3.27-.39-.91-.06-1.75-.05-2.5.05s-1.45.28-2.09.55c-.64.27-1.31.64-2 1.11-.69.47-1.45 1.05-2.27 1.72-.83.68-1.8 1.47-2.92 2.37.21.69.28 1.35.19 1.98-.09.63-.29 1.21-.61 1.72s-.75.97-1.27 1.35c-.53.39-1.13.69-1.82.9-.75-.28-1.28-.67-1.58-1.16-.3-.49-.47-1.03-.5-1.6-.03-.57.02-1.15.15-1.74.13-.59.24-1.13.32-1.63.21-1.35.72-2.58 1.51-3.67.79-1.1 1.78-2.06 2.95-2.88 1.17-.83 2.48-1.53 3.92-2.09 1.44-.57 2.9-1 4.38-1.31 1.48-.3 2.93-.48 4.35-.55s2.71 0 3.87.19c2.41.39 4.48.9 6.22 1.53 1.74.63 3.21 1.42 4.42 2.35 1.2.93 2.18 2.03 2.93 3.29.75 1.26 1.35 2.69 1.8 4.29.45 1.6.79 3.4 1.02 5.4s.42 4.2.6 6.61c-.04.32.04.64.24.95.2.31.47.48.79.5 2.66.11 4.87.21 6.61.31s3.21.19 4.42.27c1.2.09 2.22.17 3.06.24.84.08 1.67.12 2.5.15.83.02 1.74.02 2.74 0 1-.02 2.27-.08 3.8-.16 1.54-.09 3.43-.21 5.69-.39 2.26-.17 5.05-.39 8.38-.64.15-.02.37-.01.66.03.29.04.59.09.9.15.31.05.61.11.9.18.29.06.52.11.69.13-3.12.56-5.94 1.02-8.46 1.39-2.53.37-4.83.69-6.93.97-2.09.28-4 .54-5.72.77-1.72.24-3.34.5-4.87.81-1.53.3-2.98.65-4.35 1.05-1.38.4-2.77.9-4.17 1.5-1.41.6-2.85 1.33-4.33 2.19-1.48.86-3.09 1.9-4.83 3.13l-4.96-.39c-.62-.11-.98-.2-1.08-.29-.1-.09-.09-.19.03-.31s.28-.27.48-.45c.2-.18.29-.42.27-.71-.02-.29-.24-.64-.64-1.06-.41-.42-1.16-.94-2.25-1.54zm-.07-7.83c-.47-.06-1.05-.08-1.74-.05s-1.4.11-2.14.24-1.47.3-2.19.5-1.34.42-1.85.66l1.87.32c.62.11 1.19.19 1.71.24.52.05 1.04.08 1.56.08.53 0 1.09-.04 1.69-.13.6-.09 1.32-.23 2.16-.42.06-.3-.02-.61-.24-.92-.23-.3-.51-.47-.83-.52z"/><path d="M310.88 76.14c.11-.43.21-.85.32-1.27s.24-.82.4-1.19c.16-.38.36-.73.6-1.05s.54-.6.9-.84l3.74-18.21c.04-.3.18-.52.4-.64.22-.13.47-.2.72-.21.26-.01.51.01.76.05s.42.1.53.16c.77.15 1.35.46 1.72.92.38.46.61 1.01.69 1.64.09.63.07 1.33-.05 2.09-.12.76-.27 1.52-.47 2.27-.19.75-.4 1.47-.61 2.14s-.37 1.25-.45 1.72c.77.15 1.69.09 2.76-.18 1.06-.27 2.2-.67 3.4-1.21 1.2-.54 2.43-1.17 3.67-1.9 1.25-.73 2.43-1.47 3.54-2.22 1.12-.75 2.13-1.48 3.03-2.19s1.61-1.31 2.13-1.8c3.09-1.22 5.99-2.33 8.7-3.32 2.71-.99 5.28-1.89 7.72-2.69s4.8-1.53 7.07-2.16c2.28-.63 4.54-1.21 6.78-1.74 2.25-.53 4.52-1 6.82-1.43 2.3-.43 4.7-.84 7.2-1.22 2.5-.39 5.14-.76 7.93-1.11 2.78-.35 5.78-.71 9.01-1.08l1.19.55c-1.33.06-3.13.26-5.4.58s-4.86.77-7.78 1.34c-2.92.57-6.09 1.26-9.51 2.08-3.42.82-6.94 1.75-10.57 2.8-3.63 1.05-7.29 2.22-10.97 3.5-3.69 1.28-7.25 2.66-10.7 4.16-3.45 1.49-6.71 3.09-9.78 4.79-3.07 1.7-5.81 3.5-8.22 5.41-2.41 1.91-4.4 3.92-5.98 6.01s-2.61 4.28-3.08 6.56c-.06.43-.03.91.11 1.43.14.53.3 1.05.48 1.56s.34 1 .48 1.45c.14.45.17.81.08 1.06-.24 1.46-.75 2.53-1.53 3.21s-1.9.87-3.33.56c-1.29-.26-2.29-.75-3-1.48s-1.21-1.59-1.5-2.58c-.29-.99-.41-2.04-.36-3.14.09-1.11.22-2.17.41-3.18zm117.5-38.77c.32.04.73.1 1.22.16s.88.12 1.16.16c-2.64 0-5.34.04-8.11.13-2.76.09-5.43.22-8.01.4-2.58.18-4.98.42-7.22.73-2.23.3-4.15.66-5.74 1.06l-.23-1.06c2.6-.49 5.09-.9 7.46-1.22s4.66-.55 6.87-.69c2.2-.14 4.34-.18 6.43-.13 2.1.05 4.15.2 6.17.46zm8.34-.03c-.32.19-.72.27-1.19.23-.47-.04-.85-.24-1.13-.58-.13-.17-.05-.27.24-.31.29-.03.62-.03.98.02.37.04.68.12.95.24.28.12.33.25.15.4zm3.61-.74c-.02.13-.09.22-.21.27-.12.05-.25.08-.4.06-.15-.01-.28-.06-.39-.15-.11-.09-.15-.19-.13-.32.02-.15.1-.25.23-.31.13-.05.26-.08.4-.06.14.01.26.06.37.16s.15.22.13.35zm5.84.74c-.17.13-.42.21-.76.24s-.68.02-1.03-.03-.69-.13-1-.24c-.31-.11-.53-.24-.66-.39-.11-.11-.08-.18.1-.23.17-.04.4-.06.69-.06.29 0 .61.02.97.06.35.04.68.1.97.18.29.08.51.15.66.23.14.07.16.15.06.24zm4.89-1.1c.13 0 .23.07.29.21.06.14.09.28.06.42-.02.14-.07.27-.15.39-.08.12-.19.16-.34.11-.15-.02-.26-.1-.32-.24-.06-.14-.09-.28-.08-.42.01-.14.06-.26.14-.37.1-.11.23-.14.4-.1z"/><path d="M357.08 79.01c-.79.3-1.61.74-2.43 1.31-.83.57-1.63 1.2-2.42 1.9-.78.7-1.55 1.43-2.29 2.21-.74.77-1.42 1.51-2.05 2.21-.62.7-1.18 1.33-1.68 1.9-.49.57-.89 1-1.19 1.31h-1.8c-.6 0-1.1-.04-1.48-.13-.39-.09-.69-.24-.92-.47-.22-.23-.38-.53-.47-.92s-.13-.88-.13-1.48V85c1.12-2.69 2.33-5.55 3.64-8.59s2.67-6.14 4.09-9.31c1.42-3.17 2.86-6.34 4.32-9.51 1.46-3.17 2.9-6.22 4.32-9.17 1.42-2.94 2.78-5.71 4.09-8.3 1.31-2.59 2.51-4.89 3.61-6.91.11-.41.35-.97.74-1.69s.86-1.54 1.43-2.45c.57-.91 1.19-1.87 1.85-2.87s1.32-1.99 1.95-2.96c.63-.98 1.22-1.89 1.76-2.74.54-.85.97-1.57 1.29-2.18.06.37.08.87.03 1.5-.04.63-.13 1.35-.27 2.14s-.32 1.63-.53 2.5-.45 1.71-.69 2.51c-.25.81-.5 1.55-.76 2.22s-.52 1.21-.77 1.6l-18.05 37.32v1.22c1.7 0 3.34-.27 4.93-.81 1.59-.54 3.15-1.2 4.67-2 1.53-.79 3.03-1.66 4.53-2.61 1.49-.95 3-1.82 4.53-2.61 1.53-.79 3.08-1.46 4.67-2 1.59-.54 3.23-.81 4.93-.81 0 1.29-.23 2.38-.69 3.27-.46.89-1.08 1.66-1.84 2.3s-1.63 1.19-2.61 1.64c-.98.45-1.97.87-2.98 1.24-1.01.38-2 .75-2.98 1.11-.98.37-1.85.79-2.61 1.29-.76.49-1.38 1.07-1.84 1.74-.46.67-.69 1.49-.69 2.48v.58c1.96 1.14 4.23 2.22 6.83 3.24 2.6 1.02 5.37 1.99 8.31 2.92 2.94.92 5.98 1.8 9.1 2.63 3.12.83 6.2 1.61 9.22 2.35 3.02.74 5.9 1.44 8.65 2.09s5.21 1.27 7.4 1.85c2.18.58 4 1.12 5.45 1.61 1.45.49 2.39.96 2.82 1.39l1.19 1.19h-.64c-3.72 0-7.44-.35-11.17-1.06s-7.44-1.6-11.15-2.67c-3.71-1.07-7.39-2.23-11.05-3.48-3.66-1.25-7.28-2.41-10.86-3.48-3.58-1.07-7.09-1.97-10.55-2.67s-6.84-1.06-10.15-1.06h-.61zm-2.25-9.35c-.86 0-1.55.17-2.08.52-.53.34-.79.95-.79 1.8v1.19c.28-.3.59-.6.93-.89s.66-.58.93-.87.52-.58.71-.87.3-.58.3-.88z"/><path d="M423.23 99.99c.47-.34 1.22-.89 2.26-1.63s2.26-1.63 3.69-2.67c1.43-1.04 3.01-2.2 4.74-3.46 1.73-1.27 3.53-2.61 5.4-4.03s3.75-2.88 5.66-4.4c1.9-1.51 3.74-3.02 5.51-4.51 1.77-1.49 3.43-2.96 4.98-4.42s2.89-2.82 4.03-4.11 2.04-2.48 2.71-3.58 1-2.03 1-2.8c0-.45-.14-.82-.44-1.1-.29-.28-.67-.49-1.13-.63-.46-.14-.97-.23-1.52-.27-.55-.04-1.08-.06-1.61-.06s-1 .02-1.43.05-.74.05-.93.05c-2.26 0-4.49.12-6.7.37s-4.42.52-6.62.81c-2.2.29-4.41.56-6.62.81s-4.45.37-6.7.37h-4.71c-.39-.09-.85-.25-1.39-.5-.54-.25-1.05-.54-1.55-.89-.49-.34-.91-.73-1.26-1.16-.34-.43-.52-.89-.52-1.39 0-.54.49-1.26 1.48-2.16s2.33-1.92 4.03-3.05 3.69-2.34 5.98-3.64c2.29-1.3 4.74-2.63 7.35-4 2.61-1.36 5.31-2.73 8.11-4.09 2.79-1.36 5.55-2.67 8.27-3.92 2.72-1.25 5.33-2.41 7.85-3.5 2.51-1.08 4.79-2.02 6.82-2.82 2.03-.79 3.75-1.42 5.17-1.89 1.42-.46 2.41-.69 2.96-.69.3 0 .55.01.76.03.2.02.36.08.48.18s.2.25.26.47c.05.21.08.52.08.9 0 .19-.08.37-.24.53-.16.16-.34.24-.53.24-3.2.52-6.42 1.27-9.67 2.27s-6.48 2.16-9.7 3.5c-3.22 1.33-6.41 2.81-9.55 4.45-3.15 1.63-6.22 3.35-9.22 5.16-3 1.8-5.9 3.66-8.72 5.56s-5.49 3.8-8.02 5.69c.19.39.49.69.9.9.41.21.85.37 1.34.47.48.1.98.16 1.5.18.52.02.97.03 1.35.03 2.45 0 4.92-.12 7.41-.37s4.98-.52 7.48-.81 4.98-.56 7.48-.81c2.49-.25 4.96-.37 7.41-.37.99 0 1.93.04 2.82.13.89.09 1.68.29 2.37.61s1.24.79 1.64 1.42c.41.62.61 1.47.61 2.55 0 1.76-.39 3.34-1.16 4.74-.77 1.4-1.96 2.69-3.54 3.87L427.9 99.22c-.58.77-1.26 1.42-2.05 1.95-.79.53-1.66.79-2.63.79v-1.97zm50.14-61.46c0 .19-.03.39-.1.58s-.29.39-.68.58l.77-1.55v.39zM477.43 78.85c.06-.77.29-1.82.68-3.13.39-1.31.89-2.74 1.5-4.3.61-1.56 1.32-3.16 2.13-4.82.81-1.65 1.64-3.22 2.51-4.71.87-1.48 1.76-2.8 2.66-3.96s1.77-2.02 2.61-2.58c.13.02.31.12.53.31.23.18.44.4.64.64.2.25.38.49.53.74.15.25.23.44.23.56l-.03.55-.03.52-6.35 16.47c-.02.32-.04.61-.06.87-.02.24-.04.47-.05.69-.01.23-.02.4-.02.53-.06.77-.11 1.5-.13 2.18s.05 1.27.21 1.77c.16.5.47.91.93 1.22.46.31 1.14.5 2.05.56 3.07.21 5.7.38 7.9.5 2.19.12 4.1.2 5.74.24 1.63.04 3.07.04 4.32-.02 1.25-.05 2.47-.16 3.66-.31s2.44-.34 3.75-.58c1.31-.24 2.85-.53 4.61-.87 1.76-.34 3.83-.74 6.2-1.19s5.23-.96 8.56-1.51c-2.54.28-4.89.7-7.07 1.26-2.18.56-4.34 1.17-6.48 1.82-2.14.66-4.32 1.31-6.56 1.95s-4.67 1.2-7.3 1.66c-2.63.46-5.53.79-8.7.98-3.17.19-6.75.16-10.75-.1-1.03-.06-2.07-.29-3.11-.69-1.04-.4-1.97-.93-2.77-1.61-.81-.68-1.45-1.49-1.93-2.45-.5-.94-.7-2.01-.61-3.19zm13.92-40.38c.06-.79.28-1.55.64-2.27.37-.72.96-1.32 1.77-1.79.26.11.64.2 1.16.27.52.08 1.08.16 1.69.26.61.1 1.24.22 1.89.37.64.15 1.22.34 1.74.58.52.24.93.54 1.24.9.31.37.46.81.43 1.32-.13.3-.26.58-.39.84-.11.24-.23.46-.37.68-.14.21-.27.39-.4.52-.11.17-.3.4-.56.68-.27.28-.58.59-.94.93-.35.34-.74.69-1.14 1.05s-.8.69-1.18 1c-.38.31-.72.58-1.05.81s-.57.37-.74.44c-.37-.41-.77-.86-1.22-1.35-.45-.49-.88-1.02-1.27-1.56-.4-.55-.73-1.13-.98-1.74s-.36-1.28-.32-1.94z"/><path d="M512.8 81.49c0-.64 0-1.22.02-1.72.01-.5.06-1 .15-1.48s.21-.98.39-1.5.42-1.1.74-1.74c.15-.15.5-.55 1.06-1.19s1.26-1.43 2.11-2.37 1.81-1.98 2.88-3.13c1.07-1.15 2.21-2.31 3.42-3.48 1.2-1.17 2.45-2.31 3.74-3.42 1.29-1.11 2.55-2.09 3.79-2.95 1.23-.86 2.43-1.55 3.59-2.06s2.22-.77 3.19-.77c.32 0 .64.07.97.21.32.14.64.29.97.45s.64.31.97.44.64.19.97.19c1.03 0 2.06-.37 3.09-1.11s2.05-1.74 3.06-3 2-2.7 2.96-4.33c.97-1.63 1.9-3.34 2.8-5.12.9-1.78 1.76-3.58 2.56-5.4.8-1.82 1.55-3.53 2.24-5.14s1.31-3.05 1.85-4.32c.55-1.27 1.02-2.26 1.4-2.98.39-.72.68-1.09.89-1.11.2-.02.29.42.27 1.32-.02 1.25-.22 2.69-.6 4.33-.38 1.64-.87 3.41-1.48 5.29s-1.32 3.82-2.11 5.83c-.79 2.01-1.61 3.99-2.45 5.93-.84 1.94-1.67 3.81-2.48 5.61-.82 1.79-1.57 3.42-2.26 4.87s-1.27 2.68-1.76 3.69c-.48 1.01-.8 1.71-.95 2.09-.49.82-.9 1.53-1.22 2.14s-.58 1.2-.77 1.76c-.19.56-.33 1.14-.42 1.76s-.13 1.32-.13 2.11c0 1.46.23 2.62.68 3.46.45.85 1.08 1.49 1.89 1.93.8.44 1.76.73 2.85.85s2.29.19 3.58.19c5.03 0 10-.41 14.91-1.24 4.91-.83 9.79-1.86 14.65-3.11 4.86-1.25 9.71-2.59 14.55-4.04 4.84-1.45 9.72-2.8 14.61-4.04 4.9-1.25 9.83-2.28 14.81-3.11 4.97-.83 10.04-1.24 15.2-1.24-1.68.09-3.48.26-5.4.53-1.92.27-3.9.6-5.93 1s-4.08.83-6.15 1.31c-2.07.47-4.08.96-6.03 1.47-1.94.5-3.8 1-5.57 1.5-1.77.49-3.38.96-4.82 1.39-1.44.43-2.67.81-3.71 1.14-1.03.33-1.78.58-2.26.73-.97.32-2.21.77-3.72 1.35-1.52.58-3.22 1.23-5.11 1.95-1.89.72-3.94 1.49-6.16 2.32-2.21.83-4.51 1.64-6.88 2.45s-4.79 1.57-7.23 2.3c-2.45.73-4.86 1.38-7.23 1.95-2.38.57-4.68 1.02-6.91 1.35-2.23.33-4.32.5-6.25.5-1.83 0-3.41-.15-4.75-.44-1.34-.29-2.51-.72-3.5-1.29-.99-.57-1.83-1.28-2.51-2.14-.69-.86-1.27-1.86-1.76-3.01-.48-1.15-.9-2.44-1.26-3.87-.36-1.43-.69-3-1.02-4.72-.75.75-1.67 1.72-2.74 2.9-1.07 1.18-2.26 2.44-3.54 3.79-1.29 1.34-2.65 2.69-4.09 4.03-1.44 1.34-2.89 2.56-4.35 3.64-1.46 1.08-2.91 1.97-4.33 2.66-1.43.69-2.79 1.03-4.08 1.03-.82 0-1.47-.15-1.97-.44s-.89-.68-1.18-1.16c-.29-.48-.48-1.04-.58-1.66-.11-.63-.16-1.26-.16-1.91zm24.43-20.66c-.75 0-1.73.3-2.93.89-1.2.59-2.5 1.36-3.88 2.32-1.38.96-2.78 2.04-4.17 3.25-1.4 1.21-2.66 2.44-3.8 3.69-1.14 1.25-2.06 2.45-2.77 3.61s-1.06 2.17-1.06 3.03c1.07 0 2.28-.31 3.61-.93 1.33-.62 2.7-1.43 4.11-2.42 1.41-.99 2.79-2.1 4.16-3.34 1.36-1.24 2.61-2.48 3.72-3.72 1.12-1.25 2.06-2.43 2.84-3.56s1.27-2.07 1.48-2.82h-1.31zm110.57 0h-1.29v-1.29l1.29 1.29zm6.45-1.29c-.32.64-.67 1.02-1.05 1.13-.38.11-.89.16-1.53.16h-1.29c.32-.64.67-1.02 1.05-1.13.38-.11.89-.16 1.53-.16h1.29z"/><path d="M554.79 84.13c.34-.49.9-.96 1.66-1.4.76-.44 1.63-.85 2.59-1.22.97-.38 2-.74 3.09-1.08 1.1-.34 2.15-.67 3.16-.98 1.01-.31 1.94-.6 2.8-.87.86-.27 1.54-.53 2.03-.79 1.01-2.73 2.07-5.12 3.19-7.17s2.29-3.84 3.51-5.37c1.22-1.53 2.51-2.81 3.87-3.87 1.35-1.05 2.77-1.95 4.25-2.71 1.48-.75 3.03-1.39 4.66-1.92 1.62-.53 3.33-1 5.11-1.43.17-.04.5-.06 1-.06s1.03.01 1.6.02c.57.01 1.1.02 1.59.03.49.01.84.02 1.03.02.56.19.95.54 1.18 1.03.22.49.36 1.02.4 1.58.04.56.04 1.09-.02 1.6-.05.5-.08.85-.08 1.05 0 1.5-.39 2.81-1.16 3.93-.77 1.12-1.78 2.12-3.01 3.01-1.24.89-2.61 1.71-4.12 2.45-1.52.74-3.01 1.47-4.5 2.19-1.48.72-2.86 1.48-4.12 2.27-1.27.79-2.28 1.69-3.03 2.67l3 1.51c.13 0 .53.04 1.19.13.67.09 1.48.19 2.43.32.96.13 2.01.27 3.16.42 1.15.15 2.27.29 3.35.42s2.08.24 3 .32c.91.09 1.62.13 2.11.13 3.16 0 6.26-.13 9.3-.4 3.04-.27 6.06-.56 9.07-.89s6.03-.62 9.07-.89 6.14-.4 9.3-.4h6.74c-4.17 1.25-8.41 2.34-12.73 3.27s-8.56 1.72-12.73 2.35c-4.17.63-8.21 1.11-12.12 1.42-3.91.31-7.54.47-10.89.47-1.87 0-3.69-.08-5.46-.24-1.77-.16-3.52-.33-5.25-.52-1.73-.18-3.44-.35-5.12-.52-1.69-.16-3.37-.24-5.04-.24-.99 0-1.96.1-2.9.31-.95.2-1.88.46-2.79.76-.91.3-1.82.63-2.71.98s-1.79.68-2.71.98c-.91.3-1.84.55-2.79.76s-1.91.31-2.9.31c-.56 0-1.03-.14-1.42-.42s-.68-.61-.87-1-.29-.79-.29-1.22c-.01-.43.1-.8.32-1.1zm39.93-23.55c-.99 0-2.1.12-3.33.37-1.24.25-2.49.6-3.75 1.05s-2.5 1.01-3.71 1.68c-1.2.67-2.28 1.41-3.22 2.24-.95.83-1.7 1.74-2.27 2.72-.57.99-.85 2.04-.85 3.16v1.51c.62-.26 1.48-.61 2.56-1.06s2.27-.98 3.54-1.58c1.28-.6 2.57-1.28 3.87-2.03 1.3-.75 2.48-1.56 3.54-2.42 1.06-.86 1.93-1.76 2.61-2.71.67-.95 1.01-1.92 1.01-2.93zm54.43 17.2h-3l1.48-1.48 1.52 1.48zm8.92-2.22c.17 0 .3.08.39.23.09.15.13.32.13.52s-.04.37-.13.52c-.09.15-.21.23-.39.23-.19 0-.33-.08-.42-.23-.09-.15-.13-.32-.13-.52s.04-.37.13-.52c.09-.15.23-.23.42-.23z"/><path d="M656.74 82.55c-.58-1.95-.78-3.9-.6-5.85.18-1.94.66-3.82 1.42-5.62s1.77-3.52 3.01-5.14c1.25-1.62 2.65-3.09 4.21-4.42s3.23-2.47 5.01-3.45c1.78-.98 3.6-1.73 5.45-2.27 1.48-.41 2.85-.71 4.11-.9 1.26-.19 2.47-.16 3.63.1s2.31.8 3.46 1.63c1.15.83 2.35 2.06 3.59 3.69l.42 1.48c.54 1.83.6 3.65.19 5.48-.41 1.83-1.13 3.59-2.16 5.3s-2.3 3.33-3.82 4.87-3.13 2.92-4.83 4.16c-1.71 1.24-3.43 2.3-5.17 3.19-1.74.89-3.35 1.54-4.83 1.95-1.1.32-2.32.56-3.66.71-1.34.15-2.64.11-3.9-.13-1.26-.24-2.39-.73-3.4-1.47s-1.72-1.84-2.13-3.31zm4.99-4.64c.21.75.54 1.28.98 1.6.44.31.94.48 1.5.52s1.14-.02 1.74-.16c.6-.14 1.18-.29 1.74-.47 1.65-.47 3.14-1.05 4.46-1.72 1.32-.68 2.52-1.49 3.59-2.45 1.07-.96 2.04-2.06 2.9-3.3.86-1.25 1.65-2.66 2.38-4.25-.82-.75-1.7-1.3-2.66-1.64-.96-.34-1.94-.55-2.95-.61s-2.03-.02-3.05.15-1.99.37-2.92.63c-1.1.32-2.19.88-3.27 1.66-1.08.78-2.02 1.71-2.8 2.77-.79 1.06-1.35 2.23-1.69 3.5-.32 1.24-.31 2.51.05 3.77z"/><path d="M714.83 69.89c.06-.28.22-.73.47-1.35.25-.62.52-1.32.82-2.08.3-.76.59-1.56.87-2.4s.49-1.61.63-2.32c.14-.71.17-1.31.1-1.8-.08-.49-.33-.78-.76-.87l-.42-.1c-.24.06-.58.13-1.03.21-.45.08-.92.15-1.4.21-.48.06-.92.13-1.32.21-.4.08-.66.16-.79.24-.52.11-1.25.28-2.19.52-.95.24-2.04.52-3.27.84-1.24.32-2.57.67-4.01 1.05s-2.89.76-4.37 1.14-2.93.78-4.37 1.18c-1.44.4-2.78.77-4.01 1.11-1.24.34-2.34.65-3.3.92-.97.27-1.71.49-2.22.66-.13.06-.28.16-.47.27-.18.12-.37.24-.56.37s-.38.26-.55.39-.31.23-.42.29l-1.19 1.51c-.58 0-1.05-.08-1.42-.24s-.64-.37-.84-.63-.31-.55-.35-.87-.03-.63.03-.93c.04-.11.09-.24.15-.39.05-.15.12-.29.21-.42.09-.15.18-.3.29-.45l38.8-9.47c-.06-1.87-.07-3.5-.02-4.9.05-1.4.15-2.71.29-3.93.14-1.22.33-2.45.56-3.69.24-1.24.52-2.65.84-4.24.56-2.66 1.27-5.17 2.14-7.51.87-2.34 1.92-4.47 3.14-6.4 1.22-1.92 2.64-3.6 4.25-5.04 1.61-1.44 3.43-2.57 5.45-3.4 2.02-.83 4.25-1.32 6.7-1.48s5.15.06 8.09.66c.21.04.54.11.97.19.43.09.88.18 1.35.27s.92.19 1.35.29.75.16.97.18c1.16.69 1.88 1.57 2.16 2.64.28 1.07.28 2.25 0 3.51-.84-.17-1.57-.48-2.21-.93-.63-.45-1.26-.93-1.89-1.45s-1.28-1-1.98-1.47c-.7-.46-1.52-.8-2.47-1.02-2.66-.56-5.09-.69-7.27-.4-2.18.29-4.15.9-5.9 1.82s-3.3 2.13-4.64 3.61-2.52 3.14-3.53 4.96c-1.01 1.83-1.86 3.79-2.56 5.88s-1.26 4.23-1.69 6.4c-.49 2.36-.85 4.62-1.06 6.78s-.21 4.37 0 6.62l.81.13c.43.09 1.19.03 2.29-.18 1.1-.2 2.37-.49 3.82-.87 1.45-.38 3-.79 4.66-1.24s3.25-.87 4.8-1.26 2.96-.7 4.24-.95c1.28-.25 2.26-.35 2.95-.31.77-.26 1.55-.52 2.32-.79.77-.27 1.67-.55 2.69-.85 1.02-.3 2.21-.63 3.58-1 1.36-.37 3.02-.77 4.98-1.22s4.26-.96 6.93-1.53c2.66-.57 5.79-1.2 9.39-1.9 3.6-.7 7.72-1.48 12.38-2.34 4.65-.86 9.95-1.82 15.9-2.87l23.82-2.26-23.82 3.51c-4.64.92-8.9 1.77-12.78 2.53-3.88.76-7.49 1.48-10.84 2.16s-6.52 1.33-9.49 1.97c-2.98.63-5.89 1.28-8.73 1.93-2.85.66-5.69 1.35-8.54 2.09s-5.83 1.55-8.94 2.43c-3.12.88-6.43 1.85-9.94 2.92-3.51 1.06-7.34 2.26-11.49 3.59-.47.15-.76.34-.87.56s-.2.55-.29.98c-.32 1.48-.7 3-1.14 4.54s-.84 3.18-1.21 4.9c-.34 1.68-.95 3.76-1.82 6.25-.87 2.49-1.94 5.25-3.22 8.28s-2.73 6.25-4.37 9.65c-1.63 3.4-3.39 6.85-5.27 10.34-1.88 3.49-3.86 6.96-5.93 10.39-2.07 3.44-4.18 6.7-6.33 9.78-2.15 3.08-4.32 5.92-6.51 8.51-2.19 2.59-4.34 4.79-6.45 6.59s-4.14 3.15-6.11 4.03c-1.97.88-3.81 1.14-5.53.77.09-.41.57-1.26 1.45-2.55s2.05-2.91 3.5-4.87c1.45-1.96 3.13-4.19 5.04-6.7s3.95-5.2 6.11-8.07 4.39-5.87 6.69-9.01c2.3-3.14 4.56-6.3 6.78-9.47 2.22-3.18 4.36-6.34 6.4-9.47 2.04-3.14 3.88-6.14 5.51-9.02s3.01-5.59 4.14-8.12c1.12-2.5 1.89-4.75 2.29-6.73zM833.07 33.6c-.11.3-.26.52-.47.64-.21.13-.42.2-.66.23-.24.02-.48.02-.74-.02s-.49-.06-.71-.08c-.24.02-.53.02-.87-.02s-.62-.05-.84-.05c.11 0 .33-.04.66-.11.33-.08.69-.16 1.08-.26s.75-.19 1.1-.27c.34-.09.57-.13.68-.13h.32l.45.07zm12.86-1.45c-1.48.62-3.06 1.04-4.74 1.24-1.68.2-3.38.29-5.12.24l-1.26-.06c.73-.41 1.52-.71 2.37-.92.85-.2 1.71-.35 2.59-.44.88-.09 1.77-.13 2.67-.13s1.78.01 2.64.03l.85.04z"/><path d="M784.27 56.68c-.73 0-1.61.19-2.64.56-1.03.38-2.15.88-3.35 1.5-1.2.62-2.47 1.33-3.8 2.11-1.33.78-2.66 1.58-4 2.38s-2.64 1.6-3.92 2.37c-1.28.77-2.47 1.46-3.58 2.05-1.11.59-2.11 1.05-3 1.39-.89.33-1.62.47-2.17.4-.49-.13-.88-.31-1.14-.55-.27-.24-.46-.5-.58-.81-.12-.3-.19-.63-.21-1-.02-.37-.03-.73-.03-1.1 0-.49.01-.85.03-1.08s.09-.4.21-.52.31-.23.58-.32.65-.27 1.14-.53c.56-.19 1.34-.47 2.35-.84s2.17-.79 3.48-1.27 2.72-1.01 4.24-1.58c1.51-.57 3.06-1.14 4.62-1.72s3.11-1.15 4.62-1.72c1.52-.57 2.93-1.1 4.24-1.58s2.47-.91 3.48-1.27c1.01-.37 1.79-.64 2.35-.84.45-.26.83-.83 1.14-1.72.31-.89.6-1.92.85-3.09.26-1.17.52-2.4.79-3.69s.6-2.48.98-3.56c.39-1.08.85-1.98 1.4-2.69.55-.71 1.23-1.06 2.05-1.06.41 0 .63.35.68 1.06s-.03 1.6-.21 2.66-.43 2.22-.74 3.46c-.31 1.25-.62 2.42-.93 3.51-.31 1.1-.59 2.02-.82 2.77-.24.75-.35 1.17-.35 1.26 7.43-1.8 15-3.56 22.7-5.27 7.7-1.71 15.47-3.36 23.3-4.95 7.83-1.59 15.69-3.1 23.57-4.53 7.88-1.43 15.74-2.77 23.56-4.01 7.82-1.25 15.57-2.39 23.24-3.43 7.67-1.04 15.2-1.96 22.59-2.76h.48c-2.73.45-5.89.93-9.49 1.43-3.6.5-7.54 1.05-11.83 1.64-4.29.59-8.87 1.24-13.76 1.95-4.89.71-9.99 1.5-15.31 2.37-5.32.87-10.81 1.84-16.48 2.9s-11.44 2.23-17.31 3.5c-3.76.73-7.23 1.42-10.41 2.08-3.18.66-6.19 1.29-9.02 1.92-2.84.62-5.56 1.26-8.17 1.9-2.61.64-5.23 1.33-7.85 2.06-2.62.73-5.31 1.53-8.06 2.4s-5.67 1.83-8.77 2.88c-.24.13-.59.29-1.05.48-.46.19-.94.41-1.42.64-.48.24-.92.48-1.32.74s-.66.52-.79.77c-.06.24-.19.78-.37 1.63-.18.85-.42 1.89-.71 3.13s-.63 2.63-1.03 4.19c-.4 1.56-.84 3.17-1.32 4.83-.48 1.67-1.01 3.34-1.58 5.03-.57 1.69-1.18 3.29-1.82 4.82-.64 1.53-1.32 2.91-2.03 4.16-.71 1.25-1.44 2.27-2.19 3.06 0-1.4.17-2.94.5-4.62.33-1.69.76-3.44 1.27-5.25.52-1.82 1.06-3.67 1.64-5.56.58-1.89 1.13-3.73 1.64-5.53.52-1.79.94-3.51 1.27-5.14.33-1.63.5-3.1.5-4.42h-1.93zM762.35 67.7c.13 0 .22.05.27.15.05.1.08.21.08.34s-.03.24-.08.34c-.05.1-.14.15-.27.15s-.23-.05-.29-.15-.1-.21-.1-.34.03-.24.1-.34.16-.15.29-.15z"/><path d="M829.52 64.49c-.18-.46-.42-.83-.71-1.1-.29-.27-.65-.45-1.08-.53-.43-.09-.96-.08-1.58.03-2.23.39-4.2 1.02-5.9 1.89s-3.18 1.91-4.45 3.13c-1.27 1.21-2.34 2.56-3.22 4.04s-1.61 3.03-2.19 4.64-1.03 3.25-1.35 4.93-.56 3.32-.71 4.93c-.04.54-.16.91-.35 1.11s-.42.28-.68.24c-.26-.04-.53-.19-.82-.45-.29-.26-.58-.58-.85-.98-.28-.4-.53-.85-.74-1.35-.21-.5-.37-1.03-.45-1.56-.04-.17-.08-.33-.1-.48-.02-.15-.04-.28-.06-.39l-.06-.39 29.04-74.22c0 1.83-.2 3.82-.6 5.99-.4 2.17-.92 4.43-1.58 6.78s-1.41 4.75-2.27 7.2-1.75 4.87-2.67 7.27c-.92 2.4-1.85 4.72-2.79 6.96-.93 2.25-1.8 4.34-2.61 6.28-.81 1.94-1.52 3.7-2.13 5.27-.61 1.57-1.07 2.86-1.37 3.87l-1.42 4.9c.92-1.14 1.75-2.13 2.47-2.96.72-.84 1.47-1.55 2.24-2.14.77-.59 1.64-1.07 2.61-1.45.97-.38 2.16-.69 3.58-.95 1.05-.19 2.01-.15 2.87.13.86.28 1.63.71 2.3 1.31.68.59 1.28 1.3 1.8 2.13.53.83.98 1.69 1.35 2.59.38.9.69 1.8.93 2.71s.44 1.72.56 2.45c.19 1.05.28 2.29.27 3.71s-.12 2.9-.34 4.43c-.21 1.54-.54 3.07-.98 4.59-.44 1.53-1.01 2.92-1.71 4.17s-1.53 2.32-2.5 3.19-2.08 1.41-3.35 1.63c.58-1.83 1.07-3.44 1.47-4.85.4-1.41.71-2.7.95-3.87s.39-2.26.47-3.25.09-2 .05-3.01c-.04-1.01-.14-2.07-.31-3.17-.16-1.11-.36-2.35-.6-3.72-.1-.66-.25-1.22-.43-1.68zM873.2-.01v1.13c.11-.06.18-.14.23-.23.04-.09.09-.18.15-.27.05-.1.11-.2.18-.31.06-.11.17-.21.32-.32h-.88zm10.74 2.09c.09 0 .13-.03.13-.1s-.04-.1-.13-.1-.13.03-.13.1.04.1.13.1zm.87 2.05c0 .1.03.15.1.15.09 0 .13-.05.13-.15s-.04-.15-.13-.15c-.07.01-.1.05-.1.15zM841.52 84.13c.34-.49.9-.96 1.66-1.4.76-.44 1.63-.85 2.59-1.22.97-.38 2-.74 3.09-1.08 1.1-.34 2.15-.67 3.16-.98 1.01-.31 1.94-.6 2.8-.87.86-.27 1.54-.53 2.03-.79 1.01-2.73 2.07-5.12 3.19-7.17s2.29-3.84 3.51-5.37c1.22-1.53 2.51-2.81 3.87-3.87 1.35-1.05 2.77-1.95 4.25-2.71 1.48-.75 3.03-1.39 4.66-1.92 1.62-.53 3.33-1 5.11-1.43.17-.04.5-.06 1-.06s1.03.01 1.6.02c.57.01 1.1.02 1.59.03.49.01.84.02 1.03.02.56.19.95.54 1.18 1.03.22.49.36 1.02.4 1.58.04.56.04 1.09-.02 1.6-.05.5-.08.85-.08 1.05 0 1.5-.39 2.81-1.16 3.93-.77 1.12-1.78 2.12-3.01 3.01-1.24.89-2.61 1.71-4.12 2.45-1.52.74-3.01 1.47-4.5 2.19-1.48.72-2.86 1.48-4.12 2.27-1.27.79-2.28 1.69-3.03 2.67l3 1.51c.13 0 .53.04 1.19.13.67.09 1.48.19 2.43.32.96.13 2.01.27 3.16.42 1.15.15 2.27.29 3.35.42s2.08.24 3 .32c.91.09 1.62.13 2.11.13 3.16 0 6.26-.13 9.3-.4 3.04-.27 6.06-.56 9.07-.89s6.03-.62 9.07-.89 6.14-.4 9.3-.4h6.74c-4.17 1.25-8.41 2.34-12.73 3.27s-8.56 1.72-12.73 2.35c-4.17.63-8.21 1.11-12.12 1.42-3.91.31-7.54.47-10.89.47-1.87 0-3.69-.08-5.46-.24-1.77-.16-3.52-.33-5.25-.52-1.73-.18-3.44-.35-5.12-.52-1.69-.16-3.37-.24-5.04-.24-.99 0-1.96.1-2.9.31-.95.2-1.88.46-2.79.76-.91.3-1.82.63-2.71.98s-1.79.68-2.71.98c-.91.3-1.84.55-2.79.76s-1.91.31-2.9.31c-.56 0-1.03-.14-1.42-.42s-.68-.61-.87-1-.29-.79-.29-1.22c-.01-.43.1-.8.32-1.1zm39.93-23.55c-.99 0-2.1.12-3.33.37-1.24.25-2.49.6-3.75 1.05s-2.5 1.01-3.71 1.68c-1.2.67-2.28 1.41-3.22 2.24-.95.83-1.7 1.74-2.27 2.72-.57.99-.85 2.04-.85 3.16v1.51c.62-.26 1.48-.61 2.56-1.06s2.27-.98 3.54-1.58c1.28-.6 2.57-1.28 3.87-2.03 1.3-.75 2.48-1.56 3.54-2.42 1.06-.86 1.93-1.76 2.61-2.71.67-.95 1.01-1.92 1.01-2.93zm54.43 17.2h-3l1.48-1.48 1.52 1.48zm8.92-2.22c.17 0 .3.08.39.23.09.15.13.32.13.52s-.04.37-.13.52c-.09.15-.21.23-.39.23-.19 0-.33-.08-.42-.23-.09-.15-.13-.32-.13-.52s.04-.37.13-.52c.09-.15.23-.23.42-.23z"/><path d="M976.02 104.6c-.32-1.98-.32-3.87 0-5.69.32-1.82.81-3.59 1.45-5.32.64-1.73 1.37-3.44 2.18-5.12.8-1.69 1.54-3.38 2.19-5.09.65-1.71 1.14-3.45 1.47-5.22s.32-3.61 0-5.53l-.03-.39c-1.7.84-3.39 1.79-5.08 2.87-1.69 1.07-3.39 2.16-5.11 3.27-1.72 1.11-3.46 2.19-5.22 3.25s-3.57 2.01-5.41 2.84c-1.85.83-3.75 1.49-5.72 2-1.96.5-4 .76-6.11.76-.88 0-1.7-.08-2.45-.24s-1.42-.44-2-.84c-.58-.4-1.07-.92-1.47-1.58-.4-.66-.67-1.48-.82-2.47-.21-1.33-.02-2.77.6-4.32.61-1.55 1.52-3.14 2.71-4.79 1.19-1.64 2.59-3.3 4.21-4.96 1.61-1.66 3.3-3.29 5.08-4.87 1.77-1.58 3.56-3.08 5.35-4.51 1.79-1.43 3.46-2.72 5.01-3.87 1.55-1.15 2.89-2.13 4.03-2.93s1.96-1.38 2.45-1.72c.15-.3.29-.52.43-.64s.29-.23.45-.31c.16-.08.34-.15.53-.23.19-.08.42-.21.68-.4.3-.19.74-.49 1.3-.9.57-.41 1.21-.87 1.92-1.39s1.44-1.06 2.21-1.64 1.49-1.13 2.18-1.64 1.29-.98 1.8-1.4.89-.73 1.13-.92h1.58c1.22-.99 2.52-1.95 3.88-2.88s2.8-1.77 4.3-2.51 3.07-1.34 4.71-1.79 3.34-.68 5.12-.68c.6 0 1.18.08 1.72.23.55.15 1.04.39 1.48.71s.82.73 1.13 1.22c.31.49.53 1.08.66 1.77.06.39.09.91.08 1.58-.01.67-.06 1.38-.16 2.14-.1.76-.23 1.53-.4 2.3s-.38 1.48-.63 2.11c-.25.63-.54 1.15-.87 1.55-.33.4-.69.6-1.08.6h-.74c.11-.49.27-1.15.48-1.98.21-.83.42-1.69.61-2.58.19-.89.35-1.75.48-2.56.13-.82.15-1.47.06-1.97-.09-.49-.25-.92-.5-1.27s-.54-.64-.87-.87-.69-.39-1.08-.48-.77-.15-1.16-.15c-1.53 0-3.17.27-4.95.82-1.77.55-3.6 1.28-5.48 2.21-1.88.92-3.78 1.98-5.71 3.17s-3.81 2.44-5.66 3.75-3.62 2.63-5.32 3.95-3.25 2.57-4.67 3.74c-1.42 1.17-2.67 2.21-3.75 3.13-1.08.91-1.93 1.62-2.53 2.11-.9 1.38-1.98 2.68-3.24 3.92-1.26 1.24-2.58 2.45-3.96 3.66-1.38 1.2-2.79 2.41-4.21 3.63-1.42 1.21-2.73 2.5-3.95 3.85-1.21 1.35-2.27 2.8-3.16 4.33-.89 1.54-1.51 3.23-1.85 5.08.04.3.21.51.52.63.3.12.64.19 1.02.21.38.02.72.02 1.05-.02s.54-.05.64-.05c2.11 0 4.32-.23 6.65-.68s4.66-1.08 6.99-1.9c2.33-.82 4.62-1.79 6.86-2.92s4.34-2.36 6.29-3.71c1.94-1.34 3.68-2.77 5.22-4.29 1.54-1.51 2.77-3.07 3.69-4.66h.39c.49 0 .93.09 1.3.27.38.18.7.42.97.71s.48.63.64 1.02c.16.39.27.77.34 1.16.28 1.63.29 3.5.05 5.59s-.66 4.3-1.24 6.62-1.27 4.69-2.08 7.12c-.81 2.43-1.63 4.8-2.48 7.11-.85 2.31-1.67 4.51-2.45 6.61s-1.43 3.96-1.95 5.59c.02.11-.03.21-.14.31-.12.1-.26.18-.42.26-.16.08-.33.13-.5.18-.17.04-.3.06-.39.06-.19 0-.39-.08-.58-.24s-.31-.35-.35-.56c-.04-.34-.1-.68-.16-1-.04-.26-.09-.52-.13-.77s-.05-.45-.05-.55zm27.26-51.89c.17 0 .26.12.26.35 0 .24-.09.35-.26.35s-.26-.12-.26-.35c0-.23.09-.35.26-.35zM1013.13 76.14c.11-.43.21-.85.32-1.27s.24-.82.4-1.19c.16-.38.36-.73.6-1.05s.54-.6.9-.84l3.74-18.21c.04-.3.18-.52.4-.64.22-.13.47-.2.72-.21.26-.01.51.01.76.05s.42.1.53.16c.77.15 1.35.46 1.72.92.38.46.61 1.01.69 1.64.09.63.07 1.33-.05 2.09-.12.76-.27 1.52-.47 2.27-.19.75-.4 1.47-.61 2.14s-.37 1.25-.45 1.72c.77.15 1.69.09 2.76-.18 1.06-.27 2.2-.67 3.4-1.21 1.2-.54 2.43-1.17 3.67-1.9 1.25-.73 2.43-1.47 3.54-2.22 1.12-.75 2.13-1.48 3.03-2.19s1.61-1.31 2.13-1.8c3.09-1.22 5.99-2.33 8.7-3.32 2.71-.99 5.28-1.89 7.72-2.69s4.8-1.53 7.07-2.16c2.28-.63 4.54-1.21 6.78-1.74 2.25-.53 4.52-1 6.82-1.43 2.3-.43 4.7-.84 7.2-1.22 2.5-.39 5.14-.76 7.93-1.11 2.78-.35 5.78-.71 9.01-1.08l1.19.55c-1.33.06-3.13.26-5.4.58s-4.86.77-7.78 1.34c-2.92.57-6.09 1.26-9.51 2.08-3.42.82-6.94 1.75-10.57 2.8-3.63 1.05-7.29 2.22-10.97 3.5-3.69 1.28-7.25 2.66-10.7 4.16-3.45 1.49-6.71 3.09-9.78 4.79-3.07 1.7-5.81 3.5-8.22 5.41-2.41 1.91-4.4 3.92-5.98 6.01s-2.61 4.28-3.08 6.56c-.06.43-.03.91.11 1.43.14.53.3 1.05.48 1.56s.34 1 .48 1.45c.14.45.17.81.08 1.06-.24 1.46-.75 2.53-1.53 3.21s-1.9.87-3.33.56c-1.29-.26-2.29-.75-3-1.48s-1.21-1.59-1.5-2.58c-.29-.99-.41-2.04-.36-3.14.09-1.11.22-2.17.41-3.18zm117.5-38.77c.32.04.73.1 1.22.16s.88.12 1.16.16c-2.64 0-5.34.04-8.11.13-2.76.09-5.43.22-8.01.4-2.58.18-4.98.42-7.22.73-2.23.3-4.15.66-5.74 1.06l-.23-1.06c2.6-.49 5.09-.9 7.46-1.22s4.66-.55 6.87-.69c2.2-.14 4.34-.18 6.43-.13 2.1.05 4.15.2 6.17.46zm8.35-.03c-.32.19-.72.27-1.19.23-.47-.04-.85-.24-1.13-.58-.13-.17-.05-.27.24-.31.29-.03.62-.03.98.02.37.04.68.12.95.24.27.12.32.25.15.4zm3.61-.74c-.02.13-.09.22-.21.27-.12.05-.25.08-.4.06-.15-.01-.28-.06-.39-.15-.11-.09-.15-.19-.13-.32.02-.15.1-.25.23-.31.13-.05.26-.08.4-.06.14.01.26.06.37.16s.15.22.13.35zm5.83.74c-.17.13-.42.21-.76.24s-.68.02-1.03-.03-.69-.13-1-.24c-.31-.11-.53-.24-.66-.39-.11-.11-.08-.18.1-.23.17-.04.4-.06.69-.06.29 0 .61.02.97.06.35.04.68.1.97.18.29.08.51.15.66.23.15.07.17.15.06.24zm4.9-1.1c.13 0 .23.07.29.21.06.14.09.28.06.42-.02.14-.07.27-.15.39-.08.12-.19.16-.34.11-.15-.02-.26-.1-.32-.24-.06-.14-.09-.28-.08-.42.01-.14.06-.26.14-.37.1-.11.23-.14.4-.1z"/><path d="M1044 78.85c.06-.77.29-1.82.68-3.13.39-1.31.89-2.74 1.5-4.3.61-1.56 1.32-3.16 2.13-4.82.81-1.65 1.64-3.22 2.51-4.71.87-1.48 1.76-2.8 2.66-3.96s1.77-2.02 2.61-2.58c.13.02.31.12.53.31.23.18.44.4.64.64.2.25.38.49.53.74.15.25.23.44.23.56l-.03.55-.03.52-6.35 16.47c-.02.32-.04.61-.06.87-.02.24-.04.47-.05.69-.01.23-.02.4-.02.53-.06.77-.11 1.5-.13 2.18s.05 1.27.21 1.77c.16.5.47.91.93 1.22.46.31 1.14.5 2.05.56 3.07.21 5.7.38 7.9.5 2.19.12 4.1.2 5.74.24 1.63.04 3.07.04 4.32-.02 1.25-.05 2.47-.16 3.66-.31s2.44-.34 3.75-.58c1.31-.24 2.85-.53 4.61-.87 1.76-.34 3.83-.74 6.2-1.19s5.23-.96 8.56-1.51c-2.54.28-4.89.7-7.07 1.26-2.18.56-4.34 1.17-6.48 1.82-2.14.66-4.32 1.31-6.56 1.95s-4.67 1.2-7.3 1.66c-2.63.46-5.53.79-8.7.98-3.17.19-6.75.16-10.75-.1-1.03-.06-2.07-.29-3.11-.69-1.04-.4-1.97-.93-2.77-1.61-.81-.68-1.45-1.49-1.93-2.45-.5-.94-.7-2.01-.61-3.19zm13.92-40.38c.06-.79.28-1.55.64-2.27.37-.72.96-1.32 1.77-1.79.26.11.64.2 1.16.27.52.08 1.08.16 1.69.26.61.1 1.24.22 1.89.37.64.15 1.22.34 1.74.58.52.24.93.54 1.24.9.31.37.46.81.43 1.32-.13.3-.26.58-.39.84-.11.24-.23.46-.37.68-.14.21-.27.39-.4.52-.11.17-.3.4-.56.68-.27.28-.58.59-.94.93-.35.34-.74.69-1.14 1.05s-.8.69-1.18 1c-.38.31-.72.58-1.05.81s-.57.37-.74.44c-.37-.41-.77-.86-1.22-1.35-.45-.49-.88-1.02-1.27-1.56-.4-.55-.73-1.13-.98-1.74s-.36-1.28-.32-1.94z"/><path d="M1079.37 81.49c0-.64 0-1.22.02-1.72.01-.5.06-1 .15-1.48s.21-.98.39-1.5.42-1.1.74-1.74c.15-.15.5-.55 1.06-1.19s1.26-1.43 2.11-2.37 1.81-1.98 2.88-3.13c1.07-1.15 2.21-2.31 3.42-3.48 1.2-1.17 2.45-2.31 3.74-3.42 1.29-1.11 2.55-2.09 3.79-2.95 1.23-.86 2.43-1.55 3.59-2.06s2.22-.77 3.19-.77c.32 0 .64.07.97.21.32.14.64.29.97.45s.64.31.97.44.64.19.97.19c1.03 0 2.06-.37 3.09-1.11s2.05-1.74 3.06-3 2-2.7 2.96-4.33c.97-1.63 1.9-3.34 2.8-5.12.9-1.78 1.76-3.58 2.56-5.4.8-1.82 1.55-3.53 2.24-5.14s1.31-3.05 1.85-4.32c.55-1.27 1.02-2.26 1.4-2.98.39-.72.68-1.09.89-1.11.2-.02.29.42.27 1.32-.02 1.25-.22 2.69-.6 4.33-.38 1.64-.87 3.41-1.48 5.29s-1.32 3.82-2.11 5.83c-.79 2.01-1.61 3.99-2.45 5.93-.84 1.94-1.67 3.81-2.48 5.61-.82 1.79-1.57 3.42-2.26 4.87s-1.27 2.68-1.76 3.69c-.48 1.01-.8 1.71-.95 2.09-.49.82-.9 1.53-1.22 2.14s-.58 1.2-.77 1.76c-.19.56-.33 1.14-.42 1.76s-.13 1.32-.13 2.11c0 1.46.23 2.62.68 3.46.45.85 1.08 1.49 1.89 1.93.8.44 1.76.73 2.85.85s2.29.19 3.58.19c5.03 0 10-.41 14.91-1.24 4.91-.83 9.79-1.86 14.65-3.11 4.86-1.25 9.71-2.59 14.55-4.04 4.84-1.45 9.72-2.8 14.61-4.04 4.9-1.25 9.83-2.28 14.81-3.11 4.97-.83 10.04-1.24 15.2-1.24-1.68.09-3.48.26-5.4.53-1.92.27-3.9.6-5.93 1s-4.08.83-6.15 1.31c-2.07.47-4.08.96-6.03 1.47-1.94.5-3.8 1-5.57 1.5-1.77.49-3.38.96-4.82 1.39-1.44.43-2.67.81-3.71 1.14-1.03.33-1.78.58-2.26.73-.97.32-2.21.77-3.72 1.35-1.52.58-3.22 1.23-5.11 1.95-1.89.72-3.94 1.49-6.16 2.32-2.21.83-4.51 1.64-6.88 2.45s-4.79 1.57-7.23 2.3c-2.45.73-4.86 1.38-7.23 1.95-2.38.57-4.68 1.02-6.91 1.35-2.23.33-4.32.5-6.25.5-1.83 0-3.41-.15-4.75-.44-1.34-.29-2.51-.72-3.5-1.29-.99-.57-1.83-1.28-2.51-2.14-.69-.86-1.27-1.86-1.76-3.01-.48-1.15-.9-2.44-1.26-3.87-.36-1.43-.69-3-1.02-4.72-.75.75-1.67 1.72-2.74 2.9-1.07 1.18-2.26 2.44-3.54 3.79-1.29 1.34-2.65 2.69-4.09 4.03-1.44 1.34-2.89 2.56-4.35 3.64-1.46 1.08-2.91 1.97-4.33 2.66-1.43.69-2.79 1.03-4.08 1.03-.82 0-1.47-.15-1.97-.44s-.89-.68-1.18-1.16c-.29-.48-.48-1.04-.58-1.66-.11-.63-.16-1.26-.16-1.91zm24.43-20.66c-.75 0-1.73.3-2.93.89-1.2.59-2.5 1.36-3.88 2.32-1.38.96-2.78 2.04-4.17 3.25-1.4 1.21-2.66 2.44-3.8 3.69-1.14 1.25-2.06 2.45-2.77 3.61s-1.06 2.17-1.06 3.03c1.07 0 2.28-.31 3.61-.93 1.33-.62 2.7-1.43 4.11-2.42 1.41-.99 2.79-2.1 4.16-3.34 1.36-1.24 2.61-2.48 3.72-3.72 1.12-1.25 2.06-2.43 2.84-3.56s1.27-2.07 1.48-2.82h-1.31zm110.57 0h-1.29v-1.29l1.29 1.29zm6.45-1.29c-.32.64-.67 1.02-1.05 1.13-.38.11-.89.16-1.53.16h-1.29c.32-.64.67-1.02 1.05-1.13.38-.11.89-.16 1.53-.16h1.29z"/></g></svg>
</div>
</div>
</div>
<h2>Overview</h2>
<ol>
<li><a href="#whats-css-grid-layout">What’s CSS Grid Layout?</a> (in this article)</li>
<li><a href="#name-and-theme-of-this-article">Name and theme of this article</a> (in this article)</li>
<li><a href="#floyd-fact-1">Pink Floyd Fun Fact 1</a> (in this article)</li>
<li><a href="#compromising-on-semantics">Compromising on Semantics</a> (in this article)</li>
<li><a href="/blog/the-dark-side-of-the-grid-part-2#floyd-fact-1">Pink Floyd Fun Fact 2</a> (in part 2)</li>
<li><a href="/blog/the-dark-side-of-the-grid-part-2#visual-order">Changing Visual Order</a> (in part 2)</li>
<li>Cross Browser Support</li>
<li>Pink Floyd Fun Fact 3</li>
<li>Whose responsibility is it?</li>
<li>Pink Floyd Fun Fact 4</li>
</ol>
<hr>
<h2>What’s CSS Grid Layout?</h2>
<p>CSS Grid Layout is a grid-based layout system designed for two-dimensional layouts. It’s the first true layout method in CSS. Properties like <code>float</code>, <code>display: inline-block</code>, <code>position</code>, and <code>display: table</code> were not originally intended for building layouts.\<br />
Grid is a great choice if you're not working on just one axis but one two axes.</p>
<div class="demo u-full-width js-demo">
<ul class="a-layout">
<li class="a-layout__item"></li>
<li class="a-layout__item"></li>
<li class="a-layout__item"></li>
<li class="a-layout__item"></li>
<li class="a-layout__item"></li>
<li class="a-layout__item"></li>
</ul>
</div>
<p>Of course, there’s Flexbox, but its strength lies in distributing available space and placing items <em>either</em> vertically or horizontally. Flexbox loses a lot of its flexibility as soon as you’re applying <code>flex-wrap</code> and add widths to your flex items.</p>
<figure class="figure">
<blockquote>If you are adding widths to all your flex items, you probably need grid.
</blockquote>
<footer>
<cite>
Rachel Andrew | <a href="https://www.youtube.com/watch?v=tjHOLtouElA" rel="noopener">Render 2017</a>
</cite>
</footer>
</figure>
<p>This article assumes that you have at least a basic knowledge of CSS Grid Layout. If you're new to the topic, I suggest you check out <a href="https://gridbyexample.com/">gridbyexample</a> by <a href="https://twitter.com/rachelandrew">Rachel Andrew</a> or <a href="http://cssgridgarden.com">Grid Garden</a> before you continue reading.</p>
<h2>Name and theme of this article</h2>
<p>Before we dive into the dark side of the grid, I wanted to quickly address the name and theme of this article. They’re based on the LP <a href="https://en.wikipedia.org/wiki/The_Dark_Side_of_the_Moon"><cite>The Dark Side of the Moon</cite></a> by <a href="https://de.wikipedia.org/wiki/Pink_Floyd">Pink Floyd</a>, released in 1973.</p>
<figure class="figure figure--full">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1549440257/articles/floyd.jpg" rel="noopener"><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549440257/articles/floyd.jpg" alt="The “The Dark Side of the Moon LP” in front of some of my other records."></a>
<figcaption>
I found this LP in a record store in Warsaw after talking about the dark side of the grid at Frontend Con 2018.
</figcaption>
</figure>
<p>Now you might think I’m a huge Pink Floyd fan. Well, I’m sorry to disappoint you, I’m not, I just like the design. However, I can’t borrow their design without telling you about them.</p>
<p>Therefore, I present to you: <strong>Pink Floyd Fact #1</strong>.</p>
<div class="fact lazy u-full-width">
<div class="fact__inner">
<h2 id="floyd-fact-1" class="fact__heading">Pink Floyd Fun Fact #1</h2>
<p><cite>The Dark Side of the Moon</cite> is, with over 45 million copies sold, the <a href="https://en.wikipedia.org/wiki/List_of_best-selling_albums" rel="noopener">fourth best-selling album worldwide</a>. Only <em>Back in Black</em> by AC/DC (50 Million), <em>Their Greatest Hits</em> (1971–1975) by The Eagles (51 Million) and, <em>of course</em>, <em>Thriller</em> by Michael Jackson (66 Million) have sold more often.</p>
</div>
</div>
<h2>Compromising on Semantics</h2>
<p>Even before grid shipped in any browser, experts like Rachel Andrew were already fearful that developers would compromise on semantics and flatten out document structures to use CSS Grid.</p>
<figure class="figure">
<blockquote>I believe there will be a strong temptation, especially with Grid, to flatten out document structure in order that all elements become a child of the element with the Grid declared.<br>
Making layout simple, but at what cost?
</blockquote>
<footer>
<cite>
Rachel Andrew | <a href="https://www.rachelandrew.co.uk/archives/2015/07/28/modern-css-layout-power-and-responsibility/" rel="noopener">Modern CSS Layout, power and responsibility</a>
</cite>
</footer>
</figure>
<p>I’ll show you why in a simple example (I know that there are different solutions for this particular task but this is just a demo to illustrate the issue).\<br />
\<br />
Let’s say we have a <code>section</code> with a heading and a list of items.</p>
<pre><code class="language-html"><section>
<h2>Pink Floyd discography</h2>
<ul>
<li>The Piper at the Gates of Dawn</li>
<li>A Saucerful of Secrets</li>
<li>More</li>
<li>Ummagumma</li>
<li>Atom Heart Mother</li>
<li>Meddle</li>
<li>Obscured by Clouds</li>
<li>The Dark Side of the Moon</li>
</ul>
</section></code></pre>
<p>The <code>section</code> forms a 3-column grid, we want the heading to span all columns, and each <code>li</code> should fill one cell.<br />
It should look something like this:</p>
<p><a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1549441106/articles/Screen_Shot_2019-02-06_at_09.14.05.png" rel="noopener" class="no-line"><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549441106/articles/Screen_Shot_2019-02-06_at_09.14.05.png" alt="The headings spans the whole with and the list items are split in 3 columns"></a></p>
<p>That shouldn't be too hard. We select the <code>section</code>, set <code>display: grid</code>, add 3 even columns, a <code>10px</code> gutter and we make the heading span all 3 columns.</p>
<pre><code class="language-css">section {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
}
h2 {
grid-column: 1 / -1;
}</code></pre>
<p>And this is what we get:</p>
<div class="skip-link-container">
<a href="#codepen1-skip" class="skip-link skip-link--inline">Skip CodePen</a>
</div>
<p class="codepen" data-height="400" data-theme-id="6054" data-default-tab="result" data-preview="true" data-user="matuzo" data-slug-hash="QYgjZe" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="QYgjZe">
<span>See the Pen <a href="https://codepen.io/matuzo/pen/QYgjZe/">
QYgjZe</a> by Manuel Matuzovic (<a href="https://codepen.io/matuzo">@matuzo</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><a id="codepen1-skip">Doesn’t exactly look as expected.</a> The thing is, only direct children of the grid container will align with the grid. In our example, those are the <code>h2</code> and the <code>ul</code> – but we want all the <code>li</code> elements to fill cells in the grid.</p>
<p>Okay, let’s try to fix that.</p>
<p>PS: I'm well aware that I could apply <code>display: grid</code> to the <code>ul</code> directly but this is just a simplified example. There are other (more complicated) use cases where you can’t do that easily.</p>
<h3>Solution #1: Flattening the document structure.</h3>
<p>If the placement algorithm only affects direct child elements, we’ll just make our <code>li</code> direct children by removing the <code>ul</code> and swapping the <code>li</code>s for <code>div</code>s to avoid invalid HTML. This is a solution, but it’s a bad solution because we’re compromising on semantics for design reasons.</p>
<pre><code class="language-html"><section>
<h2>Pink Floyd discography</h2>
<div>The Piper at the Gates of Dawn</div>
<div>A Saucerful of Secrets</div>
<div>More</div>
<div>Ummagumma</div>
<div>Atom Heart Mother</div>
<div>Meddle</div>
<div>Obscured by Clouds</div>
<div>The Dark Side of the Moon</div>
</section></code></pre>
<p>Flattening the document structure may have bad effects on the semantics of your document, which is especially bad for screen reader users. For example, when you’re using a list, <a href="https://www.scottohara.me/blog/2019/01/12/lists-and-safari.html">screen readers usually announce the number of list items</a> which helps with navigation and overview.\<br />
Also, a flat document might be harder to read when displayed without CSS.</p>
<div class="info">
<h4><span class="info__label">Wait! What?</span>
<span class="info__heading">Why would someone disable CSS?</span></h4>
<p>It’s unlikely that users disable CSS on purpose but sometimes an error occurs or the connection is just so slow that only the HTML displays successfully. If you’ve ever been on vacation in Italy and had to use public WIFI, you know what I’m talking about.</p>
</div>
<p>Please don’t flatten document structures.</p>
<h3>Solution #2: Creating a subgrid</h3>
<p>The arguably best solution would be to use <a href="https://www.w3.org/TR/css-grid-2/#subgrids">subgrids</a>. A grid item can itself be a grid container with its own column and row definitions. It can also be a grid container but defer the definitions of rows and columns to its parent grid container.</p>
<pre><code class="language-css">ul {
display: grid;
grid-template-columns: subgrid;
}</code></pre>
<p>By setting the value of <code>grid-template-columns</code> to <code>subgrid</code> on the unordered list, the list items now align with the parent grid. This is super cool!</p>
<p>Unfortunately, support for subgrid is so bad, it doesn’t even have a <a href="https://caniuse.com/#search=subgrid">caniuse page</a>. Subgrids are part of <a href="https://www.w3.org/TR/css-grid-2/#subgrids">level 2 of the CSS Grid Layout specification</a> which is still a working draft.</p>
<figure class="figure figure--full">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/s_B782E7B1F35388692E5B4FD985531DE5ABBF6EAD14964927E69E4CE6006AEDE1_1547011977841_Screen_Shot_2019-01-09_at_06.32.36.png" rel="noopener"><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/s_B782E7B1F35388692E5B4FD985531DE5ABBF6EAD14964927E69E4CE6006AEDE1_1547011977841_Screen_Shot_2019-01-09_at_06.32.36.png" alt="Browsersupport table on caniuse.com for subgrids."></a>
<figcaption>
At the moment, subgrids aren’t a standard yet and not supported in any browser.
</figcaption>
</figure>
<p>Use subgrids as soon as they’re available.</p>
<h3>Solution #3: Using display: contents</h3>
<p>An alternative to using subgrids is a different property that has a similar effect. If you set the <code>display</code> value of an element to <code>contents</code>, it will act as if it got replaced by its child items.</p>
<pre><code class="language-css">ul {
display: contents;
}</code></pre>
<p>In our example, this causes the list items to take part in the alignment of the sections grid because for them the parent <code>ul</code> doesn’t exist anymore. This is exactly what we want, and it works perfectly fine but, (yeah I’m sorry, there’s a <em>but</em>) Edge doesn’t support it.</p>
<figure class="figure figure--full">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1549211279/articles/Screen_Shot_2019-02-03_at_17.26.50.png" rel="noopener"><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549211279/articles/Screen_Shot_2019-02-03_at_17.26.50.png" alt="Browsersupport table on caniuse.com for display: contents; Supported by alt major desktop browsers but only in Firefox without bugs."></a>
<figcaption>
Edge doesn’t support <code>display: contents</code> due to an accessibility bug in Chrome, Safari and Opera.
</figcaption>
</figure>
<p>The lack of support per se isn’t the issue but rather why it’s not supported. There’s a bug in Chrome, Opera, and Safari that removes an element with a <code>display</code> value of <code>contents</code> from the accessibility tree, <a href="http://adrianroselli.com/2018/05/display-contents-is-not-a-css-reset.html">making it inaccessible to screen reader users</a>. It’s like applying <code>display: none</code> – the element just doesn’t exist anymore for assistive technology.</p>
<figure class="figure figure--full">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1549454338/articles/contents_devtools.jpg" rel="noopener"><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549454338/articles/contents_devtools.jpg" alt="The accessibility panel in Chrome DevTools."></a>
<figcaption>
The <code>ul</code> should have a <code>role</code> of <code>list</code> but inspecting the element shows that it's not exposed to the accessibility tree at all.
</figcaption>
</figure>
<p><a href="https://github.com/MicrosoftEdge/Status/issues/608#issuecomment-394521198">Microsoft Edge will consider adding the feature</a> as soon as blink and webkit-based browsers fix the bug. Consider not using <code>contents</code> until then.</p>
<h3>Solution #4: Nesting grids</h3>
<p>As already mentioned, a grid item can also be a grid container. We can select the unordered list, make it span the whole width, and inherit values from the parent grid.</p>
<pre><code class="language-css">ul {
grid-column: 1 / -1;
display: inherit;
grid-template-columns: inherit;
grid-gap: inherit;
}</code></pre>
<p>Nesting grids isn’t a perfect solution and sometimes it might not work, but in this simple example it’s good enough.</p>
<div class="skip-link-container">
<a href="#recap" class="skip-link skip-link--inline">Skip CodePen</a>
</div>
<p class="codepen" data-height="400" data-theme-id="6054" data-default-tab="result" data-preview="true" data-user="matuzo" data-slug-hash="VgMdGO" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="Grid nesting issue solution">
<span>See the Pen <a href="https://codepen.io/matuzo/pen/VgMdGO/">
Grid nesting issue solution</a> by Manuel Matuzovic (<a href="https://codepen.io/matuzo">@matuzo</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<h2>Recap</h2>
<p>The situation regarding subgrids is anything but perfect. The <code>subgrid</code> value isn’t a standard yet, <code>display: contents</code> is buggy, and nesting grids will only work in specific use cases. If you see yourself compromising on semantics just to use CSS Grid Layout, don’t use it or try to workaround the problem until browsers fix the <code>display: contents</code> bug or ship subgrids.</p>
<p>This was part 1 of the dark side of the grid. In part two I’ll show you how easy it is to confuse users unintentionally, why it’s bad, and how to avoid it.</p>
<p>Thank you to <a href="https://twitter.com/AaronGustafson">Aaron</a> and <a href="https://twitter.com/mxbck">Max</a> for reviewing this article.</p>
</div>
<h2>Resources</h2>
<ul>
<li><a href="https://alistapart.com/article/the-story-of-css-grid-from-its-creators">The Story of CSS Grid, from Its Creators</a> by <a href="https://www.aaron-gustafson.com/">Aaron Gustafson</a></li>
<li><a href="https://rachelandrew.co.uk/archives/2015/07/28/modern-css-layout-power-and-responsibility/">Modern CSS Layout, power and responsibility</a> by <a href="https://rachelandrew.co.uk">Rachel Andrew</a></li>
<li><a href="https://www.smashingmagazine.com/2018/07/css-grid-2/">CSS Grid Level 2: Here Comes Subgrid</a> by <a href="https://rachelandrew.co.uk">Rachel Andrew</a></li>
<li><a href="https://drafts.csswg.org/css-grid-2/">CSS Grid Layout Module Level 2</a></li>
<li><a href="http://fantasai.inkedblade.net/style/discuss/subgrid-markup/">Grid vs. Subgrid: An Elemental Example</a> by <a href="http://fantasai.inkedblade.net/">fantasai</a></li>
<li><a href="http://adrianroselli.com/2018/05/display-contents-is-not-a-css-reset.html">Display: Contents Is Not a CSS Reset</a> by <a href="http://adrianroselli.com">Adrian Roselli</a></li>
<li><a href="https://github.com/MicrosoftEdge/Status/issues/608#issuecomment-394521198">Add display:contents #608</a></li>
<li><a href="http://leicestershirelalala.com/so-you-thought-youd-like-to-go-to-the-show-to-feel-the-warm-thrill-of-confusion-that-space-cadet-glow">Pink Floyd photo</a></li>
</ul>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
<script>
const demos = document.querySelectorAll('.js-demo');
let demoObserver;
function demoAddButton(demo) {
const controls = document.createElement('div');
controls.classList.add('demo__controls');
const button = document.createElement('button');
const button_inner = document.createElement('span');
button_inner.classList.add('btn__inner');
button_inner.textContent = 'Replay animation';
button.appendChild(button_inner);
button.classList.add('btn');
button.classList.add('demo__btn');
button.addEventListener('click', function() {
demo.classList.remove('demo--playing');
setTimeout(() => {
demo.classList.add('demo--playing');
}, 600);
});
controls.appendChild(button);
demo.appendChild(controls);
}
function demoVisible(entries) {
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
if (entry.intersectionRatio === 1) {
setTimeout(() => {
entry.target.classList.add('demo--playing');
demoObserver.unobserve(entry.target);
}, 400);
}
}
}
function enhanceDemos() {
if (demos.length) {
for (let i = 0; i < demos.length; i++) {
const demo = demos[i];
demoAddButton(demo);
demoObserver = new IntersectionObserver(demoVisible, {
threshold: [1]
});
if (demo.classList.contains('demo--playing')) {
continue;
}
demoObserver.observe(demo);
}
}
}
enhanceDemos();
</script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CThe+Dark+Side+of+the+Grid+%28Part+1%29%E2%80%9D">blog@matuzo.at</a>.</p> Improving the keyboard accessibility of Embedded CodePens2019-03-07T00:00:00+00:00https://www.matuzo.at/blog/improving-the-keyboard-accessibility-of-codepen-embeds
<p>I'm a <a href="https://xkcd.com/1378/">huge fan</a> of <a href="https://codepen.io/">CodePen</a> (No, they didn’t pay me to write this). I'm using it for prototyping, experimenting, sharing code, and in my latest blog post, <a href="https://www.matuzo.at/blog/the-dark-side-of-the-grid/">The Dark Side of the Grid</a>, I'm also making use of their <a href="https://codepen.io/embeds/">Embedded Pens</a>.</p>
<p>CodePen allows you to customize syntax highlighting, and background and text colors of UI elements in Embedded Pens.<br />
As a PRO user, I can also add custom CSS, which gives me the ability to improve Pens not just visually but in terms of accessibility.</p><figure class="figure ">
<span class="content__image-wrapper">
<img class="content__image" src="https://res.cloudinary.com/dp3mem7or/image/upload/v1551942694/articles/s_4A526A782EC430ED0BB5AD619387A9B6FCCD655511B65196DD333E88F585466C_1550745903247_Screen_Shot_2019-02-12_at_06.40.12.png" alt="The Embed Theme Builder with several options to change the look and feel of embedded Pens.">
</span>
<figcaption>CodePen Embed Theme Builder</figcaption>
</figure>
<h2>Accessibility wins</h2>
<p>Before I tell you where I see room for improvement, I want to highlight what they did well.</p>
<ul>
<li>You can customize colors and make sure that <a href="https://webaim.org/resources/contrastchecker/">contrast ratios are high enough</a>.</li>
<li>There’s a click-to-load option. Pens can be in a preview state where they need to be clicked to loaded, which is good for performance.</li>
<li>All buttons and links in Pens are HTML <code><button></code> and <code><a></code> elements with actual text (What a time we live in that this makes me happy).</li>
<li>Embedded Pens are <code>iframe</code>s. There’s a <code>title</code> attribute on each <code>iframe</code> with the title of the Pen as a value. This is important because screen readers announce this value when the <code>iframe</code> is focussed. If the attribute is omitted, VoiceOver for example, falls back to the <code>name</code> attribute. This can be annoying, especially if the value of the <code>name</code> is a long hash.</li>
</ul>
<figure class="figure">
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/x0OF9-4ABDQ?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="iframe title attribute demo"></iframe>
</div>
</div>
<figcaption>The CodePen is named “Buttons demo”. VoiceOver annouces the name because it’s the value of the <code>title</code> attribute. It falls back to the <code>name</code> attribute if the <code>title</code> is omitted.</figcaption>
</figure>
<h2>Accessibility improvements</h2>
<p><a href="https://www.24a11y.com/2018/i-threw-away-my-mouse/">I use the keyboard a lot</a>, even on the web, and I want to make sure that other keyboard users get the best possible experience on my website.</p>
<h3>Focus styling</h3>
<p>Depending on the colors in a CodePen theme and the browser used, focus styles are more or less visible. To make sure that focusable elements are sufficiently highlighted all the time, I added these lines to my custom CSS file:</p>
<pre><code class="language-css">button:focus,
a:focus {
/* Highlighting on focus */
outline: 5px solid #f23c50;
outline-offset: 2px;
/* Prevent items from overlapping the outline */
transform: rotate(0);
}</code></pre>
<figure class="figure">
<span class="content__image-wrapper">
<img class="content__image" src="https://res.cloudinary.com/dp3mem7or/image/upload/v1551942659/articles/Screen_Shot_2019-02-22_at_07.09.44.png" alt="Embedded Pen with HTML on the left side and a preview on the right side. Multiple buttons with a single letter each form the sentence 'I love buttons'.">
</span>
<figcaption>The <code>outline</code> property highlights the “CSS” button on focus.</figcaption>
</figure>
<div class="info">
<h4><span class="info__label">Wait! What?</span><span class="info__heading">Why did you add <code>transform: rotate(0);</code>?</span></h4>
<p>To make sure that other items don't overlap the outline of the focused item, I create a new <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context" rel="noopener">stacking context</a> on focus by applying the <code>transform</code> property with a value that doesn't change anything else visually. <code>transform</code> is just one of <a href="https://codepen.io/matuzo/pen/aERqyg" rel="noopener">many other properties that create a new stacking context</a>.</p></div>
<p>As part of my testing, I discovered that in Firefox on macOS 10.13.6 a <code>pre</code> element within the <code>iframe</code> receives focus as well. That's why added the following lines.</p>
<pre><code class="language-css">pre:focus {
/* Highlighting on focus */
outline: 5px solid #f23c50;
/* The negative value insets the outline */
outline-offset: -5px;
/* Fallback for browsers that don't support outline-offset */
border: 5px solid #f23c50;
}
@supports (outline-offset: -5px) {
pre:focus {
/* Removes the border in browsers that support outline-offset */
border: none;
}
}</code></pre>
<p><strong>Note:</strong> I’m setting the <code>outline</code> property to a negative value because otherwise the outline wouldn’t be visible due to a <code>overflow: hidden</code> rule on one of the <code>pre</code> elements parents.</p>
<p>This is how the Pen looks like in Firefox:</p>
<figure class="figure">
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/EDdACjbi64M?rel=0" title="Embedded CodePen with custom focus styling" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
</div>
<figcaption>All focusable elements are now clearly highlighted.</figcaption>
</figure>
<h3>Skip links</h3>
<p>Let’s say, you're not interested in the embedded Pen. As a mouse user, you just keep scrolling. As a keyboard user, you have to press <kbd>Tab</kbd> at least 9 times until you get to the next focusable element. Or worse, you get trapped in the Pen. This happens if the Pen is editable. You can pass the buttons in the top bar but the journey ends as soon as you get to the <code>pre</code> element where the code is displayed. Once you're in there, there’s no way of getting out.</p>
<figure class="figure">
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/3UK70AcUb84?rel=0" title="Trapped in an embedded iframe" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
</div>
<figcaption>There’s no way of escaping the code area in an editable Pen.</figcaption>
</figure>
<p>We can’t fix that issue but we can give users more options. You might have heard of skip links. If not, please read <a href="https://knowbility.org/blog/2019/skip-links/">Skip links are important</a> by <a href="https://twitter.com/vavroom">Nicolas Steenhout</a>.<br />
A skip link is a link that’s usually only visible on focus and lets users skip large parts or repetitive blocks in a page. They're often the very first item in a page.</p>
<figure class="figure figure--full">
<span class="content__image-wrapper">
<img class="content__image" src="https://res.cloudinary.com/dp3mem7or/image/upload/v1551942659/articles/Screen_Shot_2019-02-22_at_07.51.17.png" alt="A skip link at the top center of the page that says 'skip to main content'">
</span>
<figcaption>Skip link on Max Böcks website.</figcaption>
</figure>
<p>Sometimes it also makes sense to add in-page skip links. For example when you want to give users the ability to skip components or widgets in your page with a lot of links, like social media embeds, videos, or CodePens. That’s exactly what I did in my blog post.</p>
<h4>In-page skip links</h4>
<p>Directly before the embed code for the Pen, I added a <code>div</code> with an anchor link to an element that comes after the Pen in the DOM. By clicking this link, users can skip everything between the link and the target.</p>
<pre><code class="language-html"><div class="skip-link-container">
<a href="#codepen1-skip" class="skip-link"> Skip CodePen </a>
</div>
<!-- CODEPEN EMBED CODE -->
<!-- The target: -->
<h2 id="codepen1-skip">Subsequent element</h2></code></pre>
<p>By default, this link should be visually hidden and only visible on focus. It’s not enough to just apply <code>display: none</code> and remove it on focus. To ensure that the link is still accessible to screen reader users, it's necessary to get more creative.</p>
<pre><code class="language-css">.skip-link-container {
position: relative;
}
/* All the properties in this declaration block only apply if the link isn’t focused. */
.skip-link:not(:focus) {
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
left: 0;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
top: 0;
white-space: nowrap;
width: 1px;
}</code></pre>
<p>This is a great improvement because we’ve reduced the number of key presses needed to skip the Pen from 9 to 1. There’s just one more thing I want to add. Since the page <em>jumps</em> from the anchor link to the target and the skipped area might not be in the viewport anymore, users might become lost. To help with that, we can give them a visual feedback after the jump has happened.</p>
<pre><code class="language-css">/* “The :target CSS pseudo-class represents a unique element (the target element) with an id matching the URL's fragment.”
https://developer.mozilla.org/en-US/docs/Web/CSS/:target */
*:target {
transition: background 0.2s;
animation: target-highlight 1.2s ease-in-out;
}
@keyframes target-highlight {
50% {
background: #f23c50;
}
100% {
background: none;
}
}</code></pre>
<p>And this is how the final result looks like. Watch the video or <a href="http://dev.matuzo.at/codepen/">try it yourself</a>.</p>
<figure class="figure">
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" title="Embedded CodePen with custom focus styles and in-page skip link" src="https://www.youtube.com/embed/g2B_bZYR4Kc?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
</div>
<figcaption>Focus styles are clearly visible and users can skip the entire Pen.</figcaption>
</figure>
<h3>What’s next?</h3>
<p>Keyboard users benefit the most from my proposed improvements. I haven't tested how accessible embedded CodePens are for screen reader users but <a href="https://twitter.com/scottohara">Scott O'Hara</a>, who reviewed this article, told me that he had difficulties using Pens with VoiceOver in the past. So, the next thing I (or you? 🙂) could do is to test CodePens with several screen readers and share the results.</p>
<p>Until then, keep on making awesome websites for everyone. If you have any questions or feedback, please write me an <a href="mailto:manuel@matuzo.at">e-mail</a>.</p>
<p>PS: Thank you for reviewing this post, Scott. ❤️</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CImproving+the+keyboard+accessibility+of+Embedded+CodePens%E2%80%9D">blog@matuzo.at</a>.</p> 12 Tips for More Accessible React Apps (Slides, React Finland 2019)2019-04-25T00:00:00+00:00https://www.matuzo.at/blog/12-tips-for-more-accessible-react-apps-slides-react-finland-2019
<p>If you want to improve the accessibility of your React apps but you don't know how or where to start, this talk is just what you need. Manuel shares 12 tips that will help you build web sites and applications that can be used by anyone. Each tip fits on one slide and you'll be able to put them into practice right away without having to learn anything fundamentally new. The tips include testing, HTML, JS techniques, and general best practices.</p><h2>Recording of the talk</h2>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/NL6XKcX4sxc" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="The full talk at React Finland 2019"></iframe></div></div>
<h2>Slides</h2>
<p>The slides are online on this page slide by slide with descriptions.</p>
<h3>Introduction</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.001.jpg" alt="" /></p>
<p>Hello React Finland!</p>
<h3>About me</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.002.jpg" alt="" /></p>
<p>My name is Manuel Matuzovic, I'm a frontend developer from Vienna.<br />
I work for the City of Vienna and I'm specialised in HTML, CSS, and accessibility.</p>
<h3>Link to the Slides</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.003.jpeg" alt="" /></p>
<p>The <a href="https://bit.ly/react-tips">slides for this talk</a> are already online if you want to follow along on your laptop.</p>
<p>I'm not a React developer but I know how awesome React is. Today I'm here to give you tips that will help you create better apps and reach more people.</p>
<h3>About this talk</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.004.jpeg" alt="" /></p>
<p>This talk is called <cite>12 Tips For More Accessible React Apps</cite>. At the time when I picked the title I didn’t know how much time I will have so I just picked an arbitrary number and I thought that 12 will be fine.</p>
<p>As it turns out, I only have 20 minutes so let’s get started with my 8 tips for more accessible react apps. 😄</p>
<h3><abbr title="acessibility">a11y</abbr> tip #1: Create a sound document outline</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.005.jpeg" alt="" /></p>
<p>My first tip is: Create a sound document outline.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.006.jpeg" alt="<h1>Yo! I'm the title of your page.</h1><h2>I'm very important.</h2><h3>My parent is very important.</h3><h3>My parent is very important.</h3><h4>I exist.</h4><h2>I'm very important.</h2><h3>My parent is very important.</h3>" /></p>
<p>What I mean by that is that you should start your document with an <code>h1</code> and the title of your page.</p>
<p>Large sections, thematic groupings of content, in your page start with an <code>h2</code>. If there are subsections use <code>h3</code>, <code>h4</code>, etc. If there’s another large thematic section you go back to the <code>h2</code>.</p>
<div class="content__video-wrapper"><div class="video-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/gbHCgiktPNc" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: VoiceOver navigation by headings"></iframe></div></div>
<p>This is important because screen reader users don't just use the software by reading the contents on a page from top to bottom. There are additional ways of navigation, for example by listing all headings and jumping directly to a specific heading.</p>
<p>In VoiceOver you get a list of all headings. The level is announced with the text of the heading to give users context and understand the page hierarchy.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.007.jpeg" alt="<Heading.H>I will be an h1</Heading.H><Heading.LevelBoundary><Heading.H>I will be an h2</Heading.H><Heading.LevelBoundary><Heading.H>I will be an h3</Heading.H></Heading.LevelBoundary><Heading.H>I will be an h2</Heading.H></Heading.LevelBoundary>" /></p>
<p>Sometimes that's hard to get right, especially when you're working with nested components. An <code>h2</code> in a component is correct in one place but might be wrong when it's nested in another.</p>
<p><a href="https://www.tenon-ui.info/">Tenon UI</a>, an accessible React components library, has a component that takes care of that. You start with a <code>Heading.H</code> component which will automatically become an <code>h1</code> and then you use the <code>Heading.LevelBoundary</code> component to create a new section and allow automatic level calculation.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.008.jpeg" alt="<h1>I will be an h1</h1><h2>I will be an h2</h2><h3>I will be an h3</h3><h2>I will be an h2</h2>" /></p>
<p>The result is an automatically calculated correct document outline.</p>
<div class="content__video-wrapper">
<div class="video-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/tLSEWdpmYrc?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: Testing the document outline with tota11y"></iframe>
</div>
</div>
<p>There are many ways of testing the document outline. A quick and easy way is a browser extension called <a href="https://khan.github.io/tota11y/">tota11y</a>.<br />
tota11y annotates headings and shows the document outline. It displays errors if the outline is not correct.</p>
<h4>Summary of the first tip</h4>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.009.jpeg" alt="" /></p>
<p>Create a sound document outline because it gives your document structure, helps screen reader users with navigation, and it's important for SEO.</p>
<p>Check out <a href="https://www.tenon-ui.info/headings">Tenon UI's headings component</a>. Test your document outline with <a href="https://khan.github.io/tota11y/">tota11y</a> or <a href="https://wave.webaim.org/">wave</a>.</p>
<h3><abbr title="acessibility">a11y</abbr> tip #2: Hide content correctly</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.010.jpeg" alt="" /></p>
<p>My second tip is: Hide content correctly.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.011.jpeg" alt="" /></p>
<p>The React Finland website starts with an <code>h1</code> and has a well structured document outline. It's almost perfect, there’s just one thing I'd want to improve. There should be a "Sponsors" <code>h2</code> that groups the different types of sponsorships.</p>
<p>I guess it's missing because it hasn't been considered in the design either. There might be a reason to leave it away in the design but it should be in the document. The design shouldn't dictate the outline but the content should. What we need here is a heading that's hidden from sighted users but accessible to screen reader users.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.012.jpeg" alt="" /></p>
<p><code>display: none;</code>, <code>visibility: hidden</code>; and the <code>hidden</code> attribute are not suitable for hiding content visually because they remove content from the accessibility tree making it inaccessible to screen reader users.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.013.jpeg" alt=".u-vh { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; white-space: nowrap;}" /></p>
<p>We need a little bit more than that. A <a href="https://medium.com/@matuzo/writing-css-with-accessibility-in-mind-8514a0007939#81ec">combination of properties</a> that makes sure that content is still accessible to screen readers but not visible or focusable.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.014.jpeg" alt="<h2 className="u-vh">Sponsors</h2>" /></p>
<p>To improve the React Finland website we add a visually hidden <code>h2</code> and transform the existing <code>h2</code>s to <code>h3</code>s.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.015.jpeg" alt="<button><span class="u-vh">Save</span><svg aria-hidden width="32" height="32"><path …></path></svg></button>" /></p>
<p>You can use this technique as well when you have an icon button without text. You just put the text in a u-vh <code>span</code> inside the <code>button</code> and you've got yourself an accessible button.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.016.jpeg" alt="<button><VisuallyHidden>Save</VisuallyHidden><svg aria-hidden width="32" height="32"><path …></path></svg></button>" /></p>
<p>You could write a component for that or use the existing <a href="https://github.com/reach/reach-ui/tree/master/packages/u-vh">VisuallyHidden component</a> from the <a href="https://github.com/reach/reach-ui/">Reach UI React library</a>.</p>
<h4>Summary of the second tip</h4>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.017.jpeg" alt="" /></p>
<p><code>display: none;</code>, <code>visibility: hidden;</code> and the <code>hidden</code> attribute remove content from the accessibility tree.<br />
Every item needs a textual representation, even if it isn’t visible. Check out Reach UI's VisuallyHidden component.</p>
<h3><abbr title="acessibility">a11y</abbr> tip #3: Use <code><button></code> if you need a button.</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.018.jpeg" alt="" /></p>
<p>Tip number 3: Use <code><button></code> if you need a button.</p>
<p>It's tempting to use <code>div</code>s as buttons because they come with less default styling than HTML <code>button</code>s but there’s a huge difference when it comes to user experience.</p>
<p>I'll show you the difference in the following demo.</p>
<div class="content__video-wrapper">
<div class="video-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/K1sdW9GNSwE?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: Using a HTML button as button"></iframe>
</div>
</div>
<p>In this example I put a click event on a HTML <code>button</code>. You can see that I can click it with a mouse and focus it using the keyboard. I can trigger the event using the mouse or by pressing the <kbd>Enter</kbd> or <kbd>Space</kbd> key.</p>
<div class="content__video-wrapper">
<div class="video-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/mbGqcHdCIRA?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: Using a div as button"></iframe>
</div>
</div>
<p>This button looks the same but this time I'm using a <code>div</code> instead of a <code>button</code>. I can click the button but I can’t focus it, because divs aren't focusable by default. Even if I could focus it, I wouldn't get the key events I get with the HTML <code>button</code> element.</p>
<p>A fake button is inaccessible to keyboard and screen reader users.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.019.jpeg" alt="" /><br />
Summary of the third tip: <code><button></code>s are focusable by default, they come with keyevents for free and they're semantic. A <code><div></code> is just generic text.</p>
<p>Check out <a href="https://www.youtube.com/watch?v=CZGqnp06DnI">Just use button</a> by <a href="https://twitter.com/rob_dodson">Rob Dodson</a> and <a href="https://www.youtube.com/watch?v=8XjwDq9zG4I&t=1son">The Links vs. Buttons Showdown</a> by <a href="https://twitter.com/marcysutton">Marcy Sutton</a> on YouTube.</p>
<h3><abbr title="acessibility">a11y</abbr> tip #4: Use fragments to avoid invalid HTML.</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.020.jpeg" alt="" /></p>
<p>If a component returns multiple elements, they must be wrapped in a wrapper element, for example a <code>div</code>. This might cause invalid HTML or break your layouts.</p>
<p>React 16.2 introduced a nice little feature called <a href="https://reactjs.org/docs/fragments.html">Fragments</a>. Fragments let you group a list of children without adding extra nodes to the DOM.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.021.jpeg" alt="const Table = props => { return ( <table> <tr> <Columns /> </tr> </table> ); }" /></p>
<p>Let’s say we have table component and in each table row there’s a column component.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.022.jpeg" alt="const Columns = props => { return ( <div> <td>Hello</td> <td>World</td> </div> ); }" /></p>
<p>This is how the <code>Columns</code> component looks like. All cells are wrapped in a <code>div</code> because we need a wrapper element.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.023.jpeg" alt="<table> <tr> <div> <td>Hello</td> <td>World</td> </div> </tr> </table>" /></p>
<p>The result is invalid markup because a <code>div</code> is not a valid descendent of <code>tr</code>.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.024.jpeg" alt="const Columns = props => { return ( <React.Fragment> <td>Hello</td> <td>World</td> </React.Fragment> ); }" /></p>
<p>This can be fixed by using a <code>Fragment</code> instead of a <code>div</code>. All you have to do is to replace <code><div></div></code> with <code><React.Fragment></React.Fragment></code>.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.025.jpeg" alt="<table> <tr> <td>Hello</td> <td>World</td> </tr> </table>" /></p>
<p>As a result the component returns the contents without extra markup.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.026.jpeg" alt="" /></p>
<p>Summary of tip 4: Fragments help you write valid HTML and they reduce bloat. There’s also a shorter syntax, you can write <code><></></code> instead of <code><React.Fragment></React.Fragment></code>.</p>
<p>Check out the Fragments docs for more details and examples.</p>
<h3><abbr title="acessibility">a11y</abbr> tip #5: Take care of focus management.</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.027.jpeg" alt="" /></p>
<p>My fifth tip is that you should take care of focus management.</p>
<p>React applications continuously modify the HTML DOM during runtime, sometimes leading to keyboard focus being lost or set to an unexpected element. In order to fix this, we need to manually move focus in the right direction.</p>
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/eGyEQT8EDLs?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: bad focusmanagement"></iframe>
</div>
</div>
<p>If I focus this button and press Enter, a modal window pop ups. I would expect that I can access the content in the modal with my keyboard. Instead the focus is still behind the modal window because tab order always follows DOM order and the modal window is at the very end of the page. I'd have to tab through all items on the page until I reach the modal.</p>
<p>This is a situation were we have to move focus manually from the button to the modal window.</p>
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/9Z3imL-fqdU?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: good focusmanagement"></iframe>
</div>
</div>
<p>If we do that, we can access the elements in the modal by pressing the <kbd>Tab</kbd> key as we would expect it. Of course, if the user closes the modal, we have to make sure to move focus back to the button.</p>
<p>To set focus in React, we can use <code>refs</code>.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.028.jpeg" alt="class Button2 extends React.Component { constructor(props) { super(props); this.btn = React.createRef(); } setFocus(){ this.btn.current.focus(); } render() { return ( <button className="btn" ref={ this.btn }> { this.props.children } </button> ) }}" /></p>
<ol>
<li>First we create a ref using React.createRef().</li>
<li>We attach the ref to a DOM element, in our example a button, via the ref attribute.</li>
<li>This gives us access to a reference to the node.</li>
<li>Now we can focus the button using the raw DOM API.</li>
</ol>
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/BpP2-hwRbM0?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: inaccessible modal on vice.com"></iframe>
</div>
</div>
<p>Not taking care of focus can be a real problem.</p>
<p>On vice.com the focus should be on this dialog when the site is accessed for the first time to allow users to close it but instead it's behind the modal and keyboard users have no chance to interact with it. Neither pressing Tab nor pressing Escape will help.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.029.jpeg" alt="" /></p>
<p>Summary of tip 5: Focus management is important because it's essential for keyboard and screen reader users.<br />
Take advantage of refs in React to manage focus.</p>
<p>Check out the <a href="https://github.com/edenspiekermann/a11y-dialog">A11y dialog on Github</a> and the <a href="https://reactjs.org/docs/accessibility.html">accessibility docs on reactjs.org</a>.</p>
<h3><abbr title="acessibility">a11y</abbr> tip #6: Make notifications accessible to everyone.</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.030.jpeg" alt="" /></p>
<p>Tip number 6: Make notifications accessible to everyone.</p>
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/IkVjAnFp6GI?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: inaccessible notification"></iframe>
</div>
</div>
<p>I'm using VoiceOver on this page. If I click the button, a notification pops up that tells me that everything has been saved successfully.<br />
The problem is that there’s only visual feedback. The app doesn't provide screen readers with the information.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.031.jpeg" alt="<div className="alert" role="alert">Saved successfully</div>" /></p>
<p>The notification looks something like this. In order to make it accessible we have to add one more attribute.<br />
If an element has the role attribute with alert as a value, it becomes a live region. Screen readers will than watch and announce content that has changed inside this element.</p>
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/VtPqRyBUz5w?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: accessible notification"></iframe>
</div>
</div>
<p>And now, with the <code>role</code> attribute in place, the notification will be announced.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.032.jpeg" alt="" /></p>
<p>Summary of tip 6: If you add <code>role="alert"</code> or <code>role="status"</code> to an element you're transforming it to a so called live region.</p>
<p>The difference between <code>alert</code> and <code>status</code> is that <code>alert</code> will interrupt the screen reader if it's in the course of announcing something else. <code>status</code> will wait until the screen reader has finished announcing.<br />
Use live regions only for significant changes that you need to communicate.</p>
<p>Check out <a href="https://ui.reach.tech/alert/">Reach UI's alert component</a>, <a href="https://github.com/AlmeroSteyn/react-aria-live">react-aria-live</a> on Github, and read <a href="https://almerosteyn.com/2017/09/aria-live-regions-in-react">ARIA live regions in React</a> on Almero Steyns blog.</p>
<h3><abbr title="acessibility">a11y</abbr> tip #7: Announce page changes.</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.033.jpeg" alt="" /></p>
<p>Tip number 7 is important: routing. If you use a screen reader on a server side rendered page and you click a link, the whole page loads, the title of the page is announced and the focus is on the document. With single page applications that's a little bit different.</p>
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/cZqdtkG-Z4M?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: inaccessible routing (no announcement)"></iframe>
</div>
</div>
<p>If I tab to the "about" link the screen reader announces the text in the link. When I click it, the page changes but the screen reader doesn't announce the change and focus stays where it is.</p>
<p>The issue with the fact the focus doesn't move is especially visible if I click a link in the footer. I'm on the "about" page, I tab down to the footer links and click the "blog" link. The content of the page changes but I don't hear and see it.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.034.jpeg" alt="class About extends React.Component { constructor(props) { super(props); this.section = React.createRef(); } componentDidMount(){ this.section.current.focus(); document.title = "About"; } render() { return ( <section tabIndex="-1" ref={ this.section }> <h2>About</h2><p>We are…</p><p>Lorem ipsum…</p> </section> ) } }" /></p>
<p>In order to fix it I again create a <code>ref</code>. This time to get access to a reference to the <code>section</code> DOM element.<br />
I add the <code>tabindex</code> attribute with the value <code>-1</code> to make the <code>section</code> focusable. I focus it on <code>componentDidMount</code>.</p>
<p>And while I'm at it I also update the document title.</p>
<p>VoiceOver will now announce the whole region, so the heading and the text. I'm not sure if this is the best way to do it, I prefer to be as close to the native behaviour as possible. That's why I want to announce just the title of the page.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.035.jpeg" alt="render() {return (<section aria-labelledby="pageTitle" tabIndex="-1" ref= { this.section }><h2 id="pageTitle">About</h2><p>We are…</p><p>Lorem ipsum…</p></section>) }" /></p>
<p>One way to do that is to label the focused region with the text in the heading.</p>
<p>I give the heading an <code>id</code> and reference it in the section by adding the <code>aria-labelledby</code> attribute with the <code>id</code> as a value. Now the <code>section</code> is labelled by the content of the heading. The difference is that screen readers will on focus just read the content of the heading instead of everything.</p>
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/8YTtqT4JvuU?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="React demo: accessible routing"></iframe>
</div>
</div>
<p>If I do all that, the focus moves to the <code>section</code>, announces the content of the heading as well the information that focus is on a region.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.036.jpeg" alt="" /></p>
<p>There’s a router called Reach Router that does these things and more out-of-the-box.</p>
<p><a href="https://reach.tech/router">https://reach.tech/router</a></p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.037.jpeg" alt="" /></p>
<p>Summary of tip 7: Announce page changes. Use <code>ref</code>s to manage focus. If necessary, make items focusable by applying <code>tabindex="-1"</code></p>
<p>Check out <a href="https://reach.tech/router">Reach Router</a>.</p>
<h3><abbr title="acessibility">a11y</abbr> tip #8: Test your React code automatically.</h3>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.038.jpeg" alt="" /></p>
<p>My last tip, tip number 8: Test your React code automatically.</p>
<p>Don't get me wrong, you have to do manual testing as well, but automatic testing is a good first step.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.039.jpeg" alt="var React = require('react'); var ReactDOM = require('react-dom'); if (process.env.NODE_ENV !== 'production') { var axe = require('react-axe'); axe(React, ReactDOM, 1000); }" /></p>
<p>There’s a great tool called <a href="https://github.com/dequelabs/react-axe">React-Axe</a>. It uses the axe-core accessibility testing library. Results will show in the Chrome DevTools console.</p>
<p>Call the exported function passing in the React and ReactDOM objects as well as a timing delay in milliseconds.<br />
Be sure to only run the module in your development environment.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.040.jpeg" alt="" /></p>
<p>The advantage compared to other tools is that react-axe tests the accessibility of the rendered DOM.</p>
<p>If you open the console of your dev tools you'll see the errors that axe has detected. For example, the <code><html></code> element must a have a <code>lang</code> attribute, because it defines the natural language of the document.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.041.jpeg" alt="" /></p>
<p>Summary of tip 7: Automatic tests help you notice low hanging fruits. Automatic testing is only the first step. Manual testing is necessary.<br />
Check out React-axe and <a href="https://github.com/evcohen/eslint-plugin-jsx-a11y">eslint-plugin-jsx-a11y</a> on Github.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1549208913/articles/react-finland/accessible_react_apps.042.jpeg" alt="" /></p>
<p>Thank you ❤️</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9C12+Tips+for+More+Accessible+React+Apps+%28Slides%2C+React+Finland+2019%29%E2%80%9D">blog@matuzo.at</a>.</p> The Dark Side of the Grid (Part 2)2019-05-11T00:00:00+00:00https://www.matuzo.at/blog/the-dark-side-of-the-grid-part-2
<p>CSS Grid layout is powerful and flexible. It's great for our development experience, but it may come at the cost of user experience and accessibility if we don’t use it responsibly.</p>
<p>This article series gives you an overview of potential implementation pitfalls; or, in other words, the dark side of the grid.</p><script>document.querySelector('main').classList.add('dark')</script>
<div class="post" data-theme="dark-side-of-the-grid">
<div class="demo u-full-width js-demo">
<div class="a-title">
<div class="a-title-white-light"></div>
<div class="a-title-grid"><div class="a-title-grid-inner"></div></div>
<div class="a-title-rainbow"></div>
<div class="a-title-text"><svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1220.82 157.8"><g fill="#FFF"><path d="M30.33 48.56c.09-.19.21-.66.37-1.4s.33-1.54.52-2.4c.18-.86.36-1.66.53-2.4.17-.74.3-1.21.39-1.4v-.36c0-.09-.01-.21-.03-.39-.02-.17-.06-.33-.13-.47-.06-.14-.15-.24-.26-.31-.11-.06-.25-.04-.42.06l-24.24 8.9.77.71-7.83.45c-.02-.37.05-.78.21-1.26.16-.47.42-.83.79-1.06 2.47-.79 5.06-1.53 7.78-2.21 2.72-.68 5.42-1.41 8.12-2.21 2.7-.79 5.32-1.69 7.88-2.69 2.56-1 4.92-2.22 7.09-3.66 0-.19-.01-.51-.03-.95-.02-.44-.03-.94-.03-1.5s.02-1.14.05-1.74c.03-.6.11-1.15.23-1.66.12-.5.27-.92.47-1.24s.45-.49.77-.52c.09 0 .21.01.39.03.17.02.34.06.5.13s.31.14.44.23.19.17.19.26l.35 5.99c.28 0 .54-.01.77-.03.21-.02.42-.03.63-.03.2 0 .36-.01.47-.03.09 0 .27-.05.56-.15s.6-.2.92-.32c.32-.12.62-.23.9-.32.28-.1.47-.15.58-.15l60.07-11.73c.62-.09 1.59-.22 2.9-.4 1.31-.18 2.84-.38 4.58-.58 1.74-.2 3.62-.42 5.64-.64 2.02-.23 4.04-.45 6.07-.66 2.03-.21 4-.4 5.9-.56s3.6-.28 5.11-.37l3.38-.19.77.71c-2.69 1.1-5.49 1.89-8.43 2.38-2.93.49-5.92.84-8.98 1.05-3.05.2-6.13.34-9.25.42-3.12.08-6.21.24-9.28.5-3.07.26-6.09.68-9.06 1.27-2.96.59-5.82 1.5-8.57 2.72L44.6 35.7l-8.86 2.77-.71.81c-.15.73-.4 1.79-.74 3.19-.34 1.4-.76 3.03-1.26 4.9-.49 1.87-1.05 3.92-1.66 6.14s-1.25 4.52-1.92 6.9c-.67 2.37-1.34 4.76-2.03 7.17-.69 2.41-1.35 4.72-2 6.94s-1.26 4.3-1.84 6.24-1.1 3.62-1.55 5.06c-.45 1.44-.83 2.57-1.13 3.4-.3.83-.48 1.24-.55 1.24-.21-3.54-.06-7.07.47-10.59.53-3.51 1.28-6.99 2.26-10.43.98-3.44 2.11-6.84 3.38-10.2 1.28-3.36 2.57-6.68 3.88-9.94l-.01-.74zm117.46-30.14c-.64.39-1.4.66-2.26.82-.86.16-1.73.28-2.61.35-.88.08-1.75.13-2.59.16-.85.03-1.6.09-2.24.18v-1.51h9.7z"/><path d="M74.87 64.49c-.18-.46-.42-.83-.71-1.1-.29-.27-.65-.45-1.08-.53-.43-.09-.96-.08-1.58.03-2.23.39-4.2 1.02-5.9 1.89-1.7.87-3.18 1.91-4.45 3.13-1.27 1.21-2.34 2.56-3.22 4.04-.88 1.48-1.61 3.03-2.19 4.64s-1.03 3.25-1.35 4.93-.56 3.32-.71 4.93c-.04.54-.16.91-.35 1.11s-.42.28-.68.24c-.26-.04-.53-.19-.82-.45s-.58-.58-.85-.98c-.28-.4-.53-.85-.74-1.35-.21-.5-.37-1.03-.45-1.56-.04-.17-.08-.33-.1-.48-.02-.15-.04-.28-.06-.39l-.06-.39L78.6 7.98c0 1.83-.2 3.82-.6 5.99-.4 2.17-.92 4.43-1.58 6.78-.66 2.35-1.41 4.75-2.27 7.2-.86 2.45-1.75 4.87-2.67 7.27-.92 2.4-1.85 4.72-2.79 6.96-.93 2.25-1.8 4.34-2.61 6.28-.81 1.94-1.51 3.7-2.13 5.27-.61 1.57-1.07 2.86-1.37 3.87l-1.42 4.9c.92-1.14 1.75-2.13 2.47-2.96.72-.84 1.47-1.55 2.24-2.14.77-.59 1.64-1.07 2.61-1.45.97-.38 2.16-.69 3.58-.95 1.05-.19 2.01-.15 2.87.13.86.28 1.63.71 2.3 1.31.68.59 1.28 1.3 1.8 2.13.53.83.98 1.69 1.35 2.59.38.9.69 1.8.93 2.71.25.9.44 1.72.56 2.45.19 1.05.28 2.29.27 3.71-.01 1.42-.12 2.9-.34 4.43-.21 1.54-.54 3.07-.98 4.59-.44 1.53-1.01 2.92-1.71 4.17-.7 1.26-1.53 2.32-2.5 3.19s-2.08 1.41-3.35 1.63c.58-1.83 1.07-3.44 1.47-4.85.4-1.41.71-2.7.95-3.87s.39-2.26.47-3.25c.08-1 .09-2 .05-3.01-.04-1.01-.15-2.07-.31-3.17-.16-1.11-.36-2.35-.6-3.72-.09-.66-.23-1.22-.42-1.68zm43.69-64.5v1.13c.11-.06.18-.14.23-.23.04-.09.09-.18.15-.27.05-.1.11-.2.18-.31.06-.11.17-.21.32-.32h-.88zm10.73 2.09c.09 0 .13-.03.13-.1s-.04-.1-.13-.1-.13.03-.13.1.04.1.13.1zm.87 2.05c0 .1.03.15.1.15.09 0 .13-.05.13-.15s-.04-.15-.13-.15c-.07.01-.1.05-.1.15zM86.87 84.13c.34-.49.9-.96 1.66-1.4.76-.44 1.63-.85 2.59-1.22.97-.38 2-.74 3.09-1.08 1.1-.34 2.15-.67 3.16-.98 1.01-.31 1.94-.6 2.8-.87.86-.27 1.54-.53 2.03-.79 1.01-2.73 2.07-5.12 3.19-7.17s2.29-3.84 3.51-5.37c1.22-1.53 2.51-2.81 3.87-3.87 1.35-1.05 2.77-1.95 4.25-2.71 1.48-.75 3.03-1.39 4.66-1.92 1.62-.53 3.32-1 5.11-1.43.17-.04.5-.06 1-.06.49 0 1.03.01 1.6.02.57.01 1.1.02 1.6.03.49.01.84.02 1.03.02.56.19.95.54 1.18 1.03.23.49.36 1.02.4 1.58.04.56.04 1.09-.02 1.6-.05.5-.08.85-.08 1.05 0 1.5-.39 2.81-1.16 3.93-.77 1.12-1.78 2.12-3.01 3.01-1.24.89-2.61 1.71-4.12 2.45s-3.01 1.47-4.5 2.19c-1.48.72-2.86 1.48-4.12 2.27-1.27.79-2.28 1.69-3.03 2.67l3 1.51c.13 0 .53.04 1.19.13.67.09 1.48.19 2.43.32.96.13 2.01.27 3.16.42 1.15.15 2.27.29 3.35.42s2.08.24 3 .32c.91.09 1.62.13 2.11.13 3.16 0 6.26-.13 9.3-.4 3.04-.27 6.06-.56 9.07-.89 3.01-.32 6.03-.62 9.07-.89s6.14-.4 9.3-.4h6.74c-4.17 1.25-8.41 2.34-12.73 3.27s-8.56 1.72-12.73 2.35c-4.17.63-8.21 1.11-12.12 1.42-3.91.31-7.54.47-10.89.47-1.87 0-3.69-.08-5.46-.24s-3.52-.33-5.25-.52c-1.73-.18-3.44-.35-5.12-.52-1.69-.16-3.37-.24-5.04-.24-.99 0-1.96.1-2.9.31-.95.2-1.87.46-2.79.76-.91.3-1.82.63-2.71.98-.89.35-1.79.68-2.71.98-.91.3-1.84.55-2.79.76-.95.2-1.91.31-2.9.31-.56 0-1.03-.14-1.42-.42s-.68-.61-.87-1-.29-.79-.29-1.22c-.01-.43.1-.8.31-1.1zm39.93-23.55c-.99 0-2.1.12-3.34.37-1.24.25-2.49.6-3.75 1.05-1.27.45-2.5 1.01-3.71 1.68-1.2.67-2.28 1.41-3.22 2.24-.95.83-1.7 1.74-2.27 2.72-.57.99-.85 2.04-.85 3.16v1.51c.62-.26 1.48-.61 2.56-1.06 1.08-.45 2.27-.98 3.54-1.58 1.28-.6 2.57-1.28 3.87-2.03 1.3-.75 2.48-1.56 3.54-2.42 1.06-.86 1.93-1.76 2.61-2.71.68-.95 1.02-1.92 1.02-2.93zm54.43 17.2h-3l1.48-1.48 1.52 1.48zm8.93-2.22c.17 0 .3.08.39.23.09.15.13.32.13.52s-.04.37-.13.52c-.09.15-.21.23-.39.23-.19 0-.33-.08-.42-.23-.09-.15-.13-.32-.13-.52s.04-.37.13-.52c.08-.15.22-.23.42-.23z"/><path d="M158.3 98.44c4.17-1.53 7.94-2.88 11.33-4.08s6.45-2.33 9.18-3.42c2.74-1.08 5.2-2.18 7.38-3.29s4.16-2.35 5.95-3.72c1.78-1.37 3.41-2.95 4.88-4.74 1.47-1.78 2.86-3.89 4.17-6.32s2.58-5.24 3.82-8.44c1.24-3.2 2.51-6.92 3.82-11.15-.99-.32-2.21-.5-3.67-.53-1.46-.03-2.98.04-4.56.23-1.58.18-3.13.45-4.66.81-1.53.35-2.87.77-4.03 1.24l.81 1.22-2.45 1.71c-.52.37-1.02.52-1.5.47-.48-.05-.93-.24-1.35-.56s-.8-.74-1.14-1.24c-.34-.5-.63-1.03-.85-1.58s-.39-1.08-.48-1.6-.11-.95-.05-1.29c.02-.13.05-.28.08-.45.03-.17.1-.33.19-.48.1-.15.23-.27.39-.37s.37-.12.63-.08l24.49-1.39 5.19-16.66c1.18.39 2.05.93 2.61 1.64.56.71.9 1.53 1.02 2.45.12.92.08 1.91-.13 2.95-.2 1.04-.48 2.08-.82 3.13-.34 1.04-.71 2.05-1.11 3.01-.4.97-.71 1.83-.95 2.58 2.08.21 3.94.38 5.56.5 1.62.12 3.11.23 4.46.32s2.64.19 3.85.29c1.21.1 2.47.21 3.75.35 1.29.14 2.68.32 4.17.55 1.49.23 3.2.51 5.11.85.52.09 1.14.21 1.87.39.73.17 1.49.39 2.27.66s1.56.59 2.32.97c.76.38 1.42.81 1.98 1.31.56.49.98 1.06 1.27 1.69.29.63.37 1.35.24 2.14-.21 1.16-.62 2.2-1.21 3.11-.59.91-1.31 1.74-2.14 2.47-.84.73-1.76 1.38-2.76 1.95-1 .57-2.01 1.09-3.05 1.56-3.46 1.27-6.88 2.7-10.26 4.3-3.38 1.6-6.77 3.19-10.17 4.77-3.4 1.58-6.82 3.05-10.28 4.42-3.46 1.36-6.98 2.45-10.57 3.27-.17.52-.39 1.24-.64 2.16-.26.92-.58 1.84-.97 2.76-.39.91-.85 1.72-1.39 2.42-.54.7-1.15 1.07-1.84 1.11-.28-.49-.5-1.04-.66-1.63-.16-.59-.29-1.19-.37-1.8-.09-.61-.14-1.22-.16-1.84-.02-.61-.01-1.2.03-1.76-1.38.19-2.85.6-4.43 1.21s-3.22 1.34-4.91 2.19c-1.7.85-3.44 1.77-5.22 2.77-1.78 1-3.58 1.99-5.38 2.98-1.8.99-3.61 1.93-5.41 2.82-1.8.89-3.57 1.64-5.29 2.26-1.72.61-3.38 1.05-4.98 1.32-1.6.27-3.12.27-4.56.02.11-.52.31-1 .61-1.47.31-.47.63-.94.97-1.44zM214.73 54c-.34 1.57-.81 3.12-1.39 4.66-.58 1.54-1.2 3.07-1.87 4.59-.67 1.53-1.32 3.05-1.97 4.58-.64 1.53-1.2 3.04-1.68 4.54-.04.17-.16.47-.34.89-.18.42-.4.89-.64 1.42-.25.53-.52 1.08-.81 1.68-.29.59-.56 1.16-.82 1.69-.26.54-.48 1.01-.68 1.42-.19.41-.31.7-.35.87 3.18-1.16 6.01-2.2 8.49-3.11 2.48-.91 4.71-1.74 6.67-2.48s3.74-1.43 5.32-2.06c1.58-.63 3.06-1.25 4.45-1.84 1.39-.59 2.73-1.19 4.03-1.79 1.3-.6 2.66-1.26 4.08-1.98s2.95-1.51 4.59-2.37c1.64-.86 3.51-1.85 5.59-2.96.15-.11.36-.26.63-.47.27-.2.54-.44.82-.69.28-.26.53-.52.76-.79.23-.27.37-.53.44-.79-.06-.73-.34-1.36-.82-1.89-.48-.53-1.09-.97-1.82-1.34-.73-.37-1.54-.67-2.42-.92-.88-.25-1.73-.45-2.55-.6-.82-.15-1.57-.26-2.26-.34-.69-.08-1.2-.15-1.55-.21-1.83-.32-3.74-.63-5.75-.93-2.01-.3-4.04-.49-6.09-.56-2.05-.08-4.1.01-6.14.26-2.03.24-4.01.75-5.92 1.52zM279.63 85c-.3-.15-.76-.24-1.37-.26-.61-.02-1.31.01-2.08.08-.77.08-1.6.17-2.47.29-.87.12-1.71.23-2.53.32-.82.1-1.57.17-2.26.23-.69.05-1.25.05-1.68-.02-.77-.13-1.5-.29-2.18-.5-.68-.2-1.25-.51-1.71-.92-.46-.41-.79-.92-1-1.55-.2-.62-.23-1.42-.08-2.38l.32-1.93c.52-.56 1.39-1.14 2.61-1.76s2.6-1.24 4.14-1.87c1.54-.63 3.13-1.27 4.79-1.92 1.65-.64 3.17-1.28 4.56-1.9 1.39-.62 2.54-1.23 3.46-1.82.92-.59 1.43-1.15 1.51-1.69.17-1.05.11-2-.19-2.85-.3-.85-.75-1.61-1.34-2.27-.59-.67-1.31-1.25-2.14-1.76-.84-.5-1.7-.93-2.59-1.29-.89-.35-1.79-.64-2.69-.87s-1.72-.4-2.45-.53c-1.27-.19-2.36-.32-3.27-.39-.91-.06-1.75-.05-2.5.05s-1.45.28-2.09.55c-.64.27-1.31.64-2 1.11-.69.47-1.45 1.05-2.27 1.72-.83.68-1.8 1.47-2.92 2.37.21.69.28 1.35.19 1.98-.09.63-.29 1.21-.61 1.72s-.75.97-1.27 1.35c-.53.39-1.13.69-1.82.9-.75-.28-1.28-.67-1.58-1.16-.3-.49-.47-1.03-.5-1.6-.03-.57.02-1.15.15-1.74.13-.59.24-1.13.32-1.63.21-1.35.72-2.58 1.51-3.67.79-1.1 1.78-2.06 2.95-2.88 1.17-.83 2.48-1.53 3.92-2.09 1.44-.57 2.9-1 4.38-1.31 1.48-.3 2.93-.48 4.35-.55s2.71 0 3.87.19c2.41.39 4.48.9 6.22 1.53 1.74.63 3.21 1.42 4.42 2.35 1.2.93 2.18 2.03 2.93 3.29.75 1.26 1.35 2.69 1.8 4.29.45 1.6.79 3.4 1.02 5.4s.42 4.2.6 6.61c-.04.32.04.64.24.95.2.31.47.48.79.5 2.66.11 4.87.21 6.61.31s3.21.19 4.42.27c1.2.09 2.22.17 3.06.24.84.08 1.67.12 2.5.15.83.02 1.74.02 2.74 0 1-.02 2.27-.08 3.8-.16 1.54-.09 3.43-.21 5.69-.39 2.26-.17 5.05-.39 8.38-.64.15-.02.37-.01.66.03.29.04.59.09.9.15.31.05.61.11.9.18.29.06.52.11.69.13-3.12.56-5.94 1.02-8.46 1.39-2.53.37-4.83.69-6.93.97-2.09.28-4 .54-5.72.77-1.72.24-3.34.5-4.87.81-1.53.3-2.98.65-4.35 1.05-1.38.4-2.77.9-4.17 1.5-1.41.6-2.85 1.33-4.33 2.19-1.48.86-3.09 1.9-4.83 3.13l-4.96-.39c-.62-.11-.98-.2-1.08-.29-.1-.09-.09-.19.03-.31s.28-.27.48-.45c.2-.18.29-.42.27-.71-.02-.29-.24-.64-.64-1.06-.41-.42-1.16-.94-2.25-1.54zm-.07-7.83c-.47-.06-1.05-.08-1.74-.05s-1.4.11-2.14.24-1.47.3-2.19.5-1.34.42-1.85.66l1.87.32c.62.11 1.19.19 1.71.24.52.05 1.04.08 1.56.08.53 0 1.09-.04 1.69-.13.6-.09 1.32-.23 2.16-.42.06-.3-.02-.61-.24-.92-.23-.3-.51-.47-.83-.52z"/><path d="M310.88 76.14c.11-.43.21-.85.32-1.27s.24-.82.4-1.19c.16-.38.36-.73.6-1.05s.54-.6.9-.84l3.74-18.21c.04-.3.18-.52.4-.64.22-.13.47-.2.72-.21.26-.01.51.01.76.05s.42.1.53.16c.77.15 1.35.46 1.72.92.38.46.61 1.01.69 1.64.09.63.07 1.33-.05 2.09-.12.76-.27 1.52-.47 2.27-.19.75-.4 1.47-.61 2.14s-.37 1.25-.45 1.72c.77.15 1.69.09 2.76-.18 1.06-.27 2.2-.67 3.4-1.21 1.2-.54 2.43-1.17 3.67-1.9 1.25-.73 2.43-1.47 3.54-2.22 1.12-.75 2.13-1.48 3.03-2.19s1.61-1.31 2.13-1.8c3.09-1.22 5.99-2.33 8.7-3.32 2.71-.99 5.28-1.89 7.72-2.69s4.8-1.53 7.07-2.16c2.28-.63 4.54-1.21 6.78-1.74 2.25-.53 4.52-1 6.82-1.43 2.3-.43 4.7-.84 7.2-1.22 2.5-.39 5.14-.76 7.93-1.11 2.78-.35 5.78-.71 9.01-1.08l1.19.55c-1.33.06-3.13.26-5.4.58s-4.86.77-7.78 1.34c-2.92.57-6.09 1.26-9.51 2.08-3.42.82-6.94 1.75-10.57 2.8-3.63 1.05-7.29 2.22-10.97 3.5-3.69 1.28-7.25 2.66-10.7 4.16-3.45 1.49-6.71 3.09-9.78 4.79-3.07 1.7-5.81 3.5-8.22 5.41-2.41 1.91-4.4 3.92-5.98 6.01s-2.61 4.28-3.08 6.56c-.06.43-.03.91.11 1.43.14.53.3 1.05.48 1.56s.34 1 .48 1.45c.14.45.17.81.08 1.06-.24 1.46-.75 2.53-1.53 3.21s-1.9.87-3.33.56c-1.29-.26-2.29-.75-3-1.48s-1.21-1.59-1.5-2.58c-.29-.99-.41-2.04-.36-3.14.09-1.11.22-2.17.41-3.18zm117.5-38.77c.32.04.73.1 1.22.16s.88.12 1.16.16c-2.64 0-5.34.04-8.11.13-2.76.09-5.43.22-8.01.4-2.58.18-4.98.42-7.22.73-2.23.3-4.15.66-5.74 1.06l-.23-1.06c2.6-.49 5.09-.9 7.46-1.22s4.66-.55 6.87-.69c2.2-.14 4.34-.18 6.43-.13 2.1.05 4.15.2 6.17.46zm8.34-.03c-.32.19-.72.27-1.19.23-.47-.04-.85-.24-1.13-.58-.13-.17-.05-.27.24-.31.29-.03.62-.03.98.02.37.04.68.12.95.24.28.12.33.25.15.4zm3.61-.74c-.02.13-.09.22-.21.27-.12.05-.25.08-.4.06-.15-.01-.28-.06-.39-.15-.11-.09-.15-.19-.13-.32.02-.15.1-.25.23-.31.13-.05.26-.08.4-.06.14.01.26.06.37.16s.15.22.13.35zm5.84.74c-.17.13-.42.21-.76.24s-.68.02-1.03-.03-.69-.13-1-.24c-.31-.11-.53-.24-.66-.39-.11-.11-.08-.18.1-.23.17-.04.4-.06.69-.06.29 0 .61.02.97.06.35.04.68.1.97.18.29.08.51.15.66.23.14.07.16.15.06.24zm4.89-1.1c.13 0 .23.07.29.21.06.14.09.28.06.42-.02.14-.07.27-.15.39-.08.12-.19.16-.34.11-.15-.02-.26-.1-.32-.24-.06-.14-.09-.28-.08-.42.01-.14.06-.26.14-.37.1-.11.23-.14.4-.1z"/><path d="M357.08 79.01c-.79.3-1.61.74-2.43 1.31-.83.57-1.63 1.2-2.42 1.9-.78.7-1.55 1.43-2.29 2.21-.74.77-1.42 1.51-2.05 2.21-.62.7-1.18 1.33-1.68 1.9-.49.57-.89 1-1.19 1.31h-1.8c-.6 0-1.1-.04-1.48-.13-.39-.09-.69-.24-.92-.47-.22-.23-.38-.53-.47-.92s-.13-.88-.13-1.48V85c1.12-2.69 2.33-5.55 3.64-8.59s2.67-6.14 4.09-9.31c1.42-3.17 2.86-6.34 4.32-9.51 1.46-3.17 2.9-6.22 4.32-9.17 1.42-2.94 2.78-5.71 4.09-8.3 1.31-2.59 2.51-4.89 3.61-6.91.11-.41.35-.97.74-1.69s.86-1.54 1.43-2.45c.57-.91 1.19-1.87 1.85-2.87s1.32-1.99 1.95-2.96c.63-.98 1.22-1.89 1.76-2.74.54-.85.97-1.57 1.29-2.18.06.37.08.87.03 1.5-.04.63-.13 1.35-.27 2.14s-.32 1.63-.53 2.5-.45 1.71-.69 2.51c-.25.81-.5 1.55-.76 2.22s-.52 1.21-.77 1.6l-18.05 37.32v1.22c1.7 0 3.34-.27 4.93-.81 1.59-.54 3.15-1.2 4.67-2 1.53-.79 3.03-1.66 4.53-2.61 1.49-.95 3-1.82 4.53-2.61 1.53-.79 3.08-1.46 4.67-2 1.59-.54 3.23-.81 4.93-.81 0 1.29-.23 2.38-.69 3.27-.46.89-1.08 1.66-1.84 2.3s-1.63 1.19-2.61 1.64c-.98.45-1.97.87-2.98 1.24-1.01.38-2 .75-2.98 1.11-.98.37-1.85.79-2.61 1.29-.76.49-1.38 1.07-1.84 1.74-.46.67-.69 1.49-.69 2.48v.58c1.96 1.14 4.23 2.22 6.83 3.24 2.6 1.02 5.37 1.99 8.31 2.92 2.94.92 5.98 1.8 9.1 2.63 3.12.83 6.2 1.61 9.22 2.35 3.02.74 5.9 1.44 8.65 2.09s5.21 1.27 7.4 1.85c2.18.58 4 1.12 5.45 1.61 1.45.49 2.39.96 2.82 1.39l1.19 1.19h-.64c-3.72 0-7.44-.35-11.17-1.06s-7.44-1.6-11.15-2.67c-3.71-1.07-7.39-2.23-11.05-3.48-3.66-1.25-7.28-2.41-10.86-3.48-3.58-1.07-7.09-1.97-10.55-2.67s-6.84-1.06-10.15-1.06h-.61zm-2.25-9.35c-.86 0-1.55.17-2.08.52-.53.34-.79.95-.79 1.8v1.19c.28-.3.59-.6.93-.89s.66-.58.93-.87.52-.58.71-.87.3-.58.3-.88z"/><path d="M423.23 99.99c.47-.34 1.22-.89 2.26-1.63s2.26-1.63 3.69-2.67c1.43-1.04 3.01-2.2 4.74-3.46 1.73-1.27 3.53-2.61 5.4-4.03s3.75-2.88 5.66-4.4c1.9-1.51 3.74-3.02 5.51-4.51 1.77-1.49 3.43-2.96 4.98-4.42s2.89-2.82 4.03-4.11 2.04-2.48 2.71-3.58 1-2.03 1-2.8c0-.45-.14-.82-.44-1.1-.29-.28-.67-.49-1.13-.63-.46-.14-.97-.23-1.52-.27-.55-.04-1.08-.06-1.61-.06s-1 .02-1.43.05-.74.05-.93.05c-2.26 0-4.49.12-6.7.37s-4.42.52-6.62.81c-2.2.29-4.41.56-6.62.81s-4.45.37-6.7.37h-4.71c-.39-.09-.85-.25-1.39-.5-.54-.25-1.05-.54-1.55-.89-.49-.34-.91-.73-1.26-1.16-.34-.43-.52-.89-.52-1.39 0-.54.49-1.26 1.48-2.16s2.33-1.92 4.03-3.05 3.69-2.34 5.98-3.64c2.29-1.3 4.74-2.63 7.35-4 2.61-1.36 5.31-2.73 8.11-4.09 2.79-1.36 5.55-2.67 8.27-3.92 2.72-1.25 5.33-2.41 7.85-3.5 2.51-1.08 4.79-2.02 6.82-2.82 2.03-.79 3.75-1.42 5.17-1.89 1.42-.46 2.41-.69 2.96-.69.3 0 .55.01.76.03.2.02.36.08.48.18s.2.25.26.47c.05.21.08.52.08.9 0 .19-.08.37-.24.53-.16.16-.34.24-.53.24-3.2.52-6.42 1.27-9.67 2.27s-6.48 2.16-9.7 3.5c-3.22 1.33-6.41 2.81-9.55 4.45-3.15 1.63-6.22 3.35-9.22 5.16-3 1.8-5.9 3.66-8.72 5.56s-5.49 3.8-8.02 5.69c.19.39.49.69.9.9.41.21.85.37 1.34.47.48.1.98.16 1.5.18.52.02.97.03 1.35.03 2.45 0 4.92-.12 7.41-.37s4.98-.52 7.48-.81 4.98-.56 7.48-.81c2.49-.25 4.96-.37 7.41-.37.99 0 1.93.04 2.82.13.89.09 1.68.29 2.37.61s1.24.79 1.64 1.42c.41.62.61 1.47.61 2.55 0 1.76-.39 3.34-1.16 4.74-.77 1.4-1.96 2.69-3.54 3.87L427.9 99.22c-.58.77-1.26 1.42-2.05 1.95-.79.53-1.66.79-2.63.79v-1.97zm50.14-61.46c0 .19-.03.39-.1.58s-.29.39-.68.58l.77-1.55v.39zM477.43 78.85c.06-.77.29-1.82.68-3.13.39-1.31.89-2.74 1.5-4.3.61-1.56 1.32-3.16 2.13-4.82.81-1.65 1.64-3.22 2.51-4.71.87-1.48 1.76-2.8 2.66-3.96s1.77-2.02 2.61-2.58c.13.02.31.12.53.31.23.18.44.4.64.64.2.25.38.49.53.74.15.25.23.44.23.56l-.03.55-.03.52-6.35 16.47c-.02.32-.04.61-.06.87-.02.24-.04.47-.05.69-.01.23-.02.4-.02.53-.06.77-.11 1.5-.13 2.18s.05 1.27.21 1.77c.16.5.47.91.93 1.22.46.31 1.14.5 2.05.56 3.07.21 5.7.38 7.9.5 2.19.12 4.1.2 5.74.24 1.63.04 3.07.04 4.32-.02 1.25-.05 2.47-.16 3.66-.31s2.44-.34 3.75-.58c1.31-.24 2.85-.53 4.61-.87 1.76-.34 3.83-.74 6.2-1.19s5.23-.96 8.56-1.51c-2.54.28-4.89.7-7.07 1.26-2.18.56-4.34 1.17-6.48 1.82-2.14.66-4.32 1.31-6.56 1.95s-4.67 1.2-7.3 1.66c-2.63.46-5.53.79-8.7.98-3.17.19-6.75.16-10.75-.1-1.03-.06-2.07-.29-3.11-.69-1.04-.4-1.97-.93-2.77-1.61-.81-.68-1.45-1.49-1.93-2.45-.5-.94-.7-2.01-.61-3.19zm13.92-40.38c.06-.79.28-1.55.64-2.27.37-.72.96-1.32 1.77-1.79.26.11.64.2 1.16.27.52.08 1.08.16 1.69.26.61.1 1.24.22 1.89.37.64.15 1.22.34 1.74.58.52.24.93.54 1.24.9.31.37.46.81.43 1.32-.13.3-.26.58-.39.84-.11.24-.23.46-.37.68-.14.21-.27.39-.4.52-.11.17-.3.4-.56.68-.27.28-.58.59-.94.93-.35.34-.74.69-1.14 1.05s-.8.69-1.18 1c-.38.31-.72.58-1.05.81s-.57.37-.74.44c-.37-.41-.77-.86-1.22-1.35-.45-.49-.88-1.02-1.27-1.56-.4-.55-.73-1.13-.98-1.74s-.36-1.28-.32-1.94z"/><path d="M512.8 81.49c0-.64 0-1.22.02-1.72.01-.5.06-1 .15-1.48s.21-.98.39-1.5.42-1.1.74-1.74c.15-.15.5-.55 1.06-1.19s1.26-1.43 2.11-2.37 1.81-1.98 2.88-3.13c1.07-1.15 2.21-2.31 3.42-3.48 1.2-1.17 2.45-2.31 3.74-3.42 1.29-1.11 2.55-2.09 3.79-2.95 1.23-.86 2.43-1.55 3.59-2.06s2.22-.77 3.19-.77c.32 0 .64.07.97.21.32.14.64.29.97.45s.64.31.97.44.64.19.97.19c1.03 0 2.06-.37 3.09-1.11s2.05-1.74 3.06-3 2-2.7 2.96-4.33c.97-1.63 1.9-3.34 2.8-5.12.9-1.78 1.76-3.58 2.56-5.4.8-1.82 1.55-3.53 2.24-5.14s1.31-3.05 1.85-4.32c.55-1.27 1.02-2.26 1.4-2.98.39-.72.68-1.09.89-1.11.2-.02.29.42.27 1.32-.02 1.25-.22 2.69-.6 4.33-.38 1.64-.87 3.41-1.48 5.29s-1.32 3.82-2.11 5.83c-.79 2.01-1.61 3.99-2.45 5.93-.84 1.94-1.67 3.81-2.48 5.61-.82 1.79-1.57 3.42-2.26 4.87s-1.27 2.68-1.76 3.69c-.48 1.01-.8 1.71-.95 2.09-.49.82-.9 1.53-1.22 2.14s-.58 1.2-.77 1.76c-.19.56-.33 1.14-.42 1.76s-.13 1.32-.13 2.11c0 1.46.23 2.62.68 3.46.45.85 1.08 1.49 1.89 1.93.8.44 1.76.73 2.85.85s2.29.19 3.58.19c5.03 0 10-.41 14.91-1.24 4.91-.83 9.79-1.86 14.65-3.11 4.86-1.25 9.71-2.59 14.55-4.04 4.84-1.45 9.72-2.8 14.61-4.04 4.9-1.25 9.83-2.28 14.81-3.11 4.97-.83 10.04-1.24 15.2-1.24-1.68.09-3.48.26-5.4.53-1.92.27-3.9.6-5.93 1s-4.08.83-6.15 1.31c-2.07.47-4.08.96-6.03 1.47-1.94.5-3.8 1-5.57 1.5-1.77.49-3.38.96-4.82 1.39-1.44.43-2.67.81-3.71 1.14-1.03.33-1.78.58-2.26.73-.97.32-2.21.77-3.72 1.35-1.52.58-3.22 1.23-5.11 1.95-1.89.72-3.94 1.49-6.16 2.32-2.21.83-4.51 1.64-6.88 2.45s-4.79 1.57-7.23 2.3c-2.45.73-4.86 1.38-7.23 1.95-2.38.57-4.68 1.02-6.91 1.35-2.23.33-4.32.5-6.25.5-1.83 0-3.41-.15-4.75-.44-1.34-.29-2.51-.72-3.5-1.29-.99-.57-1.83-1.28-2.51-2.14-.69-.86-1.27-1.86-1.76-3.01-.48-1.15-.9-2.44-1.26-3.87-.36-1.43-.69-3-1.02-4.72-.75.75-1.67 1.72-2.74 2.9-1.07 1.18-2.26 2.44-3.54 3.79-1.29 1.34-2.65 2.69-4.09 4.03-1.44 1.34-2.89 2.56-4.35 3.64-1.46 1.08-2.91 1.97-4.33 2.66-1.43.69-2.79 1.03-4.08 1.03-.82 0-1.47-.15-1.97-.44s-.89-.68-1.18-1.16c-.29-.48-.48-1.04-.58-1.66-.11-.63-.16-1.26-.16-1.91zm24.43-20.66c-.75 0-1.73.3-2.93.89-1.2.59-2.5 1.36-3.88 2.32-1.38.96-2.78 2.04-4.17 3.25-1.4 1.21-2.66 2.44-3.8 3.69-1.14 1.25-2.06 2.45-2.77 3.61s-1.06 2.17-1.06 3.03c1.07 0 2.28-.31 3.61-.93 1.33-.62 2.7-1.43 4.11-2.42 1.41-.99 2.79-2.1 4.16-3.34 1.36-1.24 2.61-2.48 3.72-3.72 1.12-1.25 2.06-2.43 2.84-3.56s1.27-2.07 1.48-2.82h-1.31zm110.57 0h-1.29v-1.29l1.29 1.29zm6.45-1.29c-.32.64-.67 1.02-1.05 1.13-.38.11-.89.16-1.53.16h-1.29c.32-.64.67-1.02 1.05-1.13.38-.11.89-.16 1.53-.16h1.29z"/><path d="M554.79 84.13c.34-.49.9-.96 1.66-1.4.76-.44 1.63-.85 2.59-1.22.97-.38 2-.74 3.09-1.08 1.1-.34 2.15-.67 3.16-.98 1.01-.31 1.94-.6 2.8-.87.86-.27 1.54-.53 2.03-.79 1.01-2.73 2.07-5.12 3.19-7.17s2.29-3.84 3.51-5.37c1.22-1.53 2.51-2.81 3.87-3.87 1.35-1.05 2.77-1.95 4.25-2.71 1.48-.75 3.03-1.39 4.66-1.92 1.62-.53 3.33-1 5.11-1.43.17-.04.5-.06 1-.06s1.03.01 1.6.02c.57.01 1.1.02 1.59.03.49.01.84.02 1.03.02.56.19.95.54 1.18 1.03.22.49.36 1.02.4 1.58.04.56.04 1.09-.02 1.6-.05.5-.08.85-.08 1.05 0 1.5-.39 2.81-1.16 3.93-.77 1.12-1.78 2.12-3.01 3.01-1.24.89-2.61 1.71-4.12 2.45-1.52.74-3.01 1.47-4.5 2.19-1.48.72-2.86 1.48-4.12 2.27-1.27.79-2.28 1.69-3.03 2.67l3 1.51c.13 0 .53.04 1.19.13.67.09 1.48.19 2.43.32.96.13 2.01.27 3.16.42 1.15.15 2.27.29 3.35.42s2.08.24 3 .32c.91.09 1.62.13 2.11.13 3.16 0 6.26-.13 9.3-.4 3.04-.27 6.06-.56 9.07-.89s6.03-.62 9.07-.89 6.14-.4 9.3-.4h6.74c-4.17 1.25-8.41 2.34-12.73 3.27s-8.56 1.72-12.73 2.35c-4.17.63-8.21 1.11-12.12 1.42-3.91.31-7.54.47-10.89.47-1.87 0-3.69-.08-5.46-.24-1.77-.16-3.52-.33-5.25-.52-1.73-.18-3.44-.35-5.12-.52-1.69-.16-3.37-.24-5.04-.24-.99 0-1.96.1-2.9.31-.95.2-1.88.46-2.79.76-.91.3-1.82.63-2.71.98s-1.79.68-2.71.98c-.91.3-1.84.55-2.79.76s-1.91.31-2.9.31c-.56 0-1.03-.14-1.42-.42s-.68-.61-.87-1-.29-.79-.29-1.22c-.01-.43.1-.8.32-1.1zm39.93-23.55c-.99 0-2.1.12-3.33.37-1.24.25-2.49.6-3.75 1.05s-2.5 1.01-3.71 1.68c-1.2.67-2.28 1.41-3.22 2.24-.95.83-1.7 1.74-2.27 2.72-.57.99-.85 2.04-.85 3.16v1.51c.62-.26 1.48-.61 2.56-1.06s2.27-.98 3.54-1.58c1.28-.6 2.57-1.28 3.87-2.03 1.3-.75 2.48-1.56 3.54-2.42 1.06-.86 1.93-1.76 2.61-2.71.67-.95 1.01-1.92 1.01-2.93zm54.43 17.2h-3l1.48-1.48 1.52 1.48zm8.92-2.22c.17 0 .3.08.39.23.09.15.13.32.13.52s-.04.37-.13.52c-.09.15-.21.23-.39.23-.19 0-.33-.08-.42-.23-.09-.15-.13-.32-.13-.52s.04-.37.13-.52c.09-.15.23-.23.42-.23z"/><path d="M656.74 82.55c-.58-1.95-.78-3.9-.6-5.85.18-1.94.66-3.82 1.42-5.62s1.77-3.52 3.01-5.14c1.25-1.62 2.65-3.09 4.21-4.42s3.23-2.47 5.01-3.45c1.78-.98 3.6-1.73 5.45-2.27 1.48-.41 2.85-.71 4.11-.9 1.26-.19 2.47-.16 3.63.1s2.31.8 3.46 1.63c1.15.83 2.35 2.06 3.59 3.69l.42 1.48c.54 1.83.6 3.65.19 5.48-.41 1.83-1.13 3.59-2.16 5.3s-2.3 3.33-3.82 4.87-3.13 2.92-4.83 4.16c-1.71 1.24-3.43 2.3-5.17 3.19-1.74.89-3.35 1.54-4.83 1.95-1.1.32-2.32.56-3.66.71-1.34.15-2.64.11-3.9-.13-1.26-.24-2.39-.73-3.4-1.47s-1.72-1.84-2.13-3.31zm4.99-4.64c.21.75.54 1.28.98 1.6.44.31.94.48 1.5.52s1.14-.02 1.74-.16c.6-.14 1.18-.29 1.74-.47 1.65-.47 3.14-1.05 4.46-1.72 1.32-.68 2.52-1.49 3.59-2.45 1.07-.96 2.04-2.06 2.9-3.3.86-1.25 1.65-2.66 2.38-4.25-.82-.75-1.7-1.3-2.66-1.64-.96-.34-1.94-.55-2.95-.61s-2.03-.02-3.05.15-1.99.37-2.92.63c-1.1.32-2.19.88-3.27 1.66-1.08.78-2.02 1.71-2.8 2.77-.79 1.06-1.35 2.23-1.69 3.5-.32 1.24-.31 2.51.05 3.77z"/><path d="M714.83 69.89c.06-.28.22-.73.47-1.35.25-.62.52-1.32.82-2.08.3-.76.59-1.56.87-2.4s.49-1.61.63-2.32c.14-.71.17-1.31.1-1.8-.08-.49-.33-.78-.76-.87l-.42-.1c-.24.06-.58.13-1.03.21-.45.08-.92.15-1.4.21-.48.06-.92.13-1.32.21-.4.08-.66.16-.79.24-.52.11-1.25.28-2.19.52-.95.24-2.04.52-3.27.84-1.24.32-2.57.67-4.01 1.05s-2.89.76-4.37 1.14-2.93.78-4.37 1.18c-1.44.4-2.78.77-4.01 1.11-1.24.34-2.34.65-3.3.92-.97.27-1.71.49-2.22.66-.13.06-.28.16-.47.27-.18.12-.37.24-.56.37s-.38.26-.55.39-.31.23-.42.29l-1.19 1.51c-.58 0-1.05-.08-1.42-.24s-.64-.37-.84-.63-.31-.55-.35-.87-.03-.63.03-.93c.04-.11.09-.24.15-.39.05-.15.12-.29.21-.42.09-.15.18-.3.29-.45l38.8-9.47c-.06-1.87-.07-3.5-.02-4.9.05-1.4.15-2.71.29-3.93.14-1.22.33-2.45.56-3.69.24-1.24.52-2.65.84-4.24.56-2.66 1.27-5.17 2.14-7.51.87-2.34 1.92-4.47 3.14-6.4 1.22-1.92 2.64-3.6 4.25-5.04 1.61-1.44 3.43-2.57 5.45-3.4 2.02-.83 4.25-1.32 6.7-1.48s5.15.06 8.09.66c.21.04.54.11.97.19.43.09.88.18 1.35.27s.92.19 1.35.29.75.16.97.18c1.16.69 1.88 1.57 2.16 2.64.28 1.07.28 2.25 0 3.51-.84-.17-1.57-.48-2.21-.93-.63-.45-1.26-.93-1.89-1.45s-1.28-1-1.98-1.47c-.7-.46-1.52-.8-2.47-1.02-2.66-.56-5.09-.69-7.27-.4-2.18.29-4.15.9-5.9 1.82s-3.3 2.13-4.64 3.61-2.52 3.14-3.53 4.96c-1.01 1.83-1.86 3.79-2.56 5.88s-1.26 4.23-1.69 6.4c-.49 2.36-.85 4.62-1.06 6.78s-.21 4.37 0 6.62l.81.13c.43.09 1.19.03 2.29-.18 1.1-.2 2.37-.49 3.82-.87 1.45-.38 3-.79 4.66-1.24s3.25-.87 4.8-1.26 2.96-.7 4.24-.95c1.28-.25 2.26-.35 2.95-.31.77-.26 1.55-.52 2.32-.79.77-.27 1.67-.55 2.69-.85 1.02-.3 2.21-.63 3.58-1 1.36-.37 3.02-.77 4.98-1.22s4.26-.96 6.93-1.53c2.66-.57 5.79-1.2 9.39-1.9 3.6-.7 7.72-1.48 12.38-2.34 4.65-.86 9.95-1.82 15.9-2.87l23.82-2.26-23.82 3.51c-4.64.92-8.9 1.77-12.78 2.53-3.88.76-7.49 1.48-10.84 2.16s-6.52 1.33-9.49 1.97c-2.98.63-5.89 1.28-8.73 1.93-2.85.66-5.69 1.35-8.54 2.09s-5.83 1.55-8.94 2.43c-3.12.88-6.43 1.85-9.94 2.92-3.51 1.06-7.34 2.26-11.49 3.59-.47.15-.76.34-.87.56s-.2.55-.29.98c-.32 1.48-.7 3-1.14 4.54s-.84 3.18-1.21 4.9c-.34 1.68-.95 3.76-1.82 6.25-.87 2.49-1.94 5.25-3.22 8.28s-2.73 6.25-4.37 9.65c-1.63 3.4-3.39 6.85-5.27 10.34-1.88 3.49-3.86 6.96-5.93 10.39-2.07 3.44-4.18 6.7-6.33 9.78-2.15 3.08-4.32 5.92-6.51 8.51-2.19 2.59-4.34 4.79-6.45 6.59s-4.14 3.15-6.11 4.03c-1.97.88-3.81 1.14-5.53.77.09-.41.57-1.26 1.45-2.55s2.05-2.91 3.5-4.87c1.45-1.96 3.13-4.19 5.04-6.7s3.95-5.2 6.11-8.07 4.39-5.87 6.69-9.01c2.3-3.14 4.56-6.3 6.78-9.47 2.22-3.18 4.36-6.34 6.4-9.47 2.04-3.14 3.88-6.14 5.51-9.02s3.01-5.59 4.14-8.12c1.12-2.5 1.89-4.75 2.29-6.73zM833.07 33.6c-.11.3-.26.52-.47.64-.21.13-.42.2-.66.23-.24.02-.48.02-.74-.02s-.49-.06-.71-.08c-.24.02-.53.02-.87-.02s-.62-.05-.84-.05c.11 0 .33-.04.66-.11.33-.08.69-.16 1.08-.26s.75-.19 1.1-.27c.34-.09.57-.13.68-.13h.32l.45.07zm12.86-1.45c-1.48.62-3.06 1.04-4.74 1.24-1.68.2-3.38.29-5.12.24l-1.26-.06c.73-.41 1.52-.71 2.37-.92.85-.2 1.71-.35 2.59-.44.88-.09 1.77-.13 2.67-.13s1.78.01 2.64.03l.85.04z"/><path d="M784.27 56.68c-.73 0-1.61.19-2.64.56-1.03.38-2.15.88-3.35 1.5-1.2.62-2.47 1.33-3.8 2.11-1.33.78-2.66 1.58-4 2.38s-2.64 1.6-3.92 2.37c-1.28.77-2.47 1.46-3.58 2.05-1.11.59-2.11 1.05-3 1.39-.89.33-1.62.47-2.17.4-.49-.13-.88-.31-1.14-.55-.27-.24-.46-.5-.58-.81-.12-.3-.19-.63-.21-1-.02-.37-.03-.73-.03-1.1 0-.49.01-.85.03-1.08s.09-.4.21-.52.31-.23.58-.32.65-.27 1.14-.53c.56-.19 1.34-.47 2.35-.84s2.17-.79 3.48-1.27 2.72-1.01 4.24-1.58c1.51-.57 3.06-1.14 4.62-1.72s3.11-1.15 4.62-1.72c1.52-.57 2.93-1.1 4.24-1.58s2.47-.91 3.48-1.27c1.01-.37 1.79-.64 2.35-.84.45-.26.83-.83 1.14-1.72.31-.89.6-1.92.85-3.09.26-1.17.52-2.4.79-3.69s.6-2.48.98-3.56c.39-1.08.85-1.98 1.4-2.69.55-.71 1.23-1.06 2.05-1.06.41 0 .63.35.68 1.06s-.03 1.6-.21 2.66-.43 2.22-.74 3.46c-.31 1.25-.62 2.42-.93 3.51-.31 1.1-.59 2.02-.82 2.77-.24.75-.35 1.17-.35 1.26 7.43-1.8 15-3.56 22.7-5.27 7.7-1.71 15.47-3.36 23.3-4.95 7.83-1.59 15.69-3.1 23.57-4.53 7.88-1.43 15.74-2.77 23.56-4.01 7.82-1.25 15.57-2.39 23.24-3.43 7.67-1.04 15.2-1.96 22.59-2.76h.48c-2.73.45-5.89.93-9.49 1.43-3.6.5-7.54 1.05-11.83 1.64-4.29.59-8.87 1.24-13.76 1.95-4.89.71-9.99 1.5-15.31 2.37-5.32.87-10.81 1.84-16.48 2.9s-11.44 2.23-17.31 3.5c-3.76.73-7.23 1.42-10.41 2.08-3.18.66-6.19 1.29-9.02 1.92-2.84.62-5.56 1.26-8.17 1.9-2.61.64-5.23 1.33-7.85 2.06-2.62.73-5.31 1.53-8.06 2.4s-5.67 1.83-8.77 2.88c-.24.13-.59.29-1.05.48-.46.19-.94.41-1.42.64-.48.24-.92.48-1.32.74s-.66.52-.79.77c-.06.24-.19.78-.37 1.63-.18.85-.42 1.89-.71 3.13s-.63 2.63-1.03 4.19c-.4 1.56-.84 3.17-1.32 4.83-.48 1.67-1.01 3.34-1.58 5.03-.57 1.69-1.18 3.29-1.82 4.82-.64 1.53-1.32 2.91-2.03 4.16-.71 1.25-1.44 2.27-2.19 3.06 0-1.4.17-2.94.5-4.62.33-1.69.76-3.44 1.27-5.25.52-1.82 1.06-3.67 1.64-5.56.58-1.89 1.13-3.73 1.64-5.53.52-1.79.94-3.51 1.27-5.14.33-1.63.5-3.1.5-4.42h-1.93zM762.35 67.7c.13 0 .22.05.27.15.05.1.08.21.08.34s-.03.24-.08.34c-.05.1-.14.15-.27.15s-.23-.05-.29-.15-.1-.21-.1-.34.03-.24.1-.34.16-.15.29-.15z"/><path d="M829.52 64.49c-.18-.46-.42-.83-.71-1.1-.29-.27-.65-.45-1.08-.53-.43-.09-.96-.08-1.58.03-2.23.39-4.2 1.02-5.9 1.89s-3.18 1.91-4.45 3.13c-1.27 1.21-2.34 2.56-3.22 4.04s-1.61 3.03-2.19 4.64-1.03 3.25-1.35 4.93-.56 3.32-.71 4.93c-.04.54-.16.91-.35 1.11s-.42.28-.68.24c-.26-.04-.53-.19-.82-.45-.29-.26-.58-.58-.85-.98-.28-.4-.53-.85-.74-1.35-.21-.5-.37-1.03-.45-1.56-.04-.17-.08-.33-.1-.48-.02-.15-.04-.28-.06-.39l-.06-.39 29.04-74.22c0 1.83-.2 3.82-.6 5.99-.4 2.17-.92 4.43-1.58 6.78s-1.41 4.75-2.27 7.2-1.75 4.87-2.67 7.27c-.92 2.4-1.85 4.72-2.79 6.96-.93 2.25-1.8 4.34-2.61 6.28-.81 1.94-1.52 3.7-2.13 5.27-.61 1.57-1.07 2.86-1.37 3.87l-1.42 4.9c.92-1.14 1.75-2.13 2.47-2.96.72-.84 1.47-1.55 2.24-2.14.77-.59 1.64-1.07 2.61-1.45.97-.38 2.16-.69 3.58-.95 1.05-.19 2.01-.15 2.87.13.86.28 1.63.71 2.3 1.31.68.59 1.28 1.3 1.8 2.13.53.83.98 1.69 1.35 2.59.38.9.69 1.8.93 2.71s.44 1.72.56 2.45c.19 1.05.28 2.29.27 3.71s-.12 2.9-.34 4.43c-.21 1.54-.54 3.07-.98 4.59-.44 1.53-1.01 2.92-1.71 4.17s-1.53 2.32-2.5 3.19-2.08 1.41-3.35 1.63c.58-1.83 1.07-3.44 1.47-4.85.4-1.41.71-2.7.95-3.87s.39-2.26.47-3.25.09-2 .05-3.01c-.04-1.01-.14-2.07-.31-3.17-.16-1.11-.36-2.35-.6-3.72-.1-.66-.25-1.22-.43-1.68zM873.2-.01v1.13c.11-.06.18-.14.23-.23.04-.09.09-.18.15-.27.05-.1.11-.2.18-.31.06-.11.17-.21.32-.32h-.88zm10.74 2.09c.09 0 .13-.03.13-.1s-.04-.1-.13-.1-.13.03-.13.1.04.1.13.1zm.87 2.05c0 .1.03.15.1.15.09 0 .13-.05.13-.15s-.04-.15-.13-.15c-.07.01-.1.05-.1.15zM841.52 84.13c.34-.49.9-.96 1.66-1.4.76-.44 1.63-.85 2.59-1.22.97-.38 2-.74 3.09-1.08 1.1-.34 2.15-.67 3.16-.98 1.01-.31 1.94-.6 2.8-.87.86-.27 1.54-.53 2.03-.79 1.01-2.73 2.07-5.12 3.19-7.17s2.29-3.84 3.51-5.37c1.22-1.53 2.51-2.81 3.87-3.87 1.35-1.05 2.77-1.95 4.25-2.71 1.48-.75 3.03-1.39 4.66-1.92 1.62-.53 3.33-1 5.11-1.43.17-.04.5-.06 1-.06s1.03.01 1.6.02c.57.01 1.1.02 1.59.03.49.01.84.02 1.03.02.56.19.95.54 1.18 1.03.22.49.36 1.02.4 1.58.04.56.04 1.09-.02 1.6-.05.5-.08.85-.08 1.05 0 1.5-.39 2.81-1.16 3.93-.77 1.12-1.78 2.12-3.01 3.01-1.24.89-2.61 1.71-4.12 2.45-1.52.74-3.01 1.47-4.5 2.19-1.48.72-2.86 1.48-4.12 2.27-1.27.79-2.28 1.69-3.03 2.67l3 1.51c.13 0 .53.04 1.19.13.67.09 1.48.19 2.43.32.96.13 2.01.27 3.16.42 1.15.15 2.27.29 3.35.42s2.08.24 3 .32c.91.09 1.62.13 2.11.13 3.16 0 6.26-.13 9.3-.4 3.04-.27 6.06-.56 9.07-.89s6.03-.62 9.07-.89 6.14-.4 9.3-.4h6.74c-4.17 1.25-8.41 2.34-12.73 3.27s-8.56 1.72-12.73 2.35c-4.17.63-8.21 1.11-12.12 1.42-3.91.31-7.54.47-10.89.47-1.87 0-3.69-.08-5.46-.24-1.77-.16-3.52-.33-5.25-.52-1.73-.18-3.44-.35-5.12-.52-1.69-.16-3.37-.24-5.04-.24-.99 0-1.96.1-2.9.31-.95.2-1.88.46-2.79.76-.91.3-1.82.63-2.71.98s-1.79.68-2.71.98c-.91.3-1.84.55-2.79.76s-1.91.31-2.9.31c-.56 0-1.03-.14-1.42-.42s-.68-.61-.87-1-.29-.79-.29-1.22c-.01-.43.1-.8.32-1.1zm39.93-23.55c-.99 0-2.1.12-3.33.37-1.24.25-2.49.6-3.75 1.05s-2.5 1.01-3.71 1.68c-1.2.67-2.28 1.41-3.22 2.24-.95.83-1.7 1.74-2.27 2.72-.57.99-.85 2.04-.85 3.16v1.51c.62-.26 1.48-.61 2.56-1.06s2.27-.98 3.54-1.58c1.28-.6 2.57-1.28 3.87-2.03 1.3-.75 2.48-1.56 3.54-2.42 1.06-.86 1.93-1.76 2.61-2.71.67-.95 1.01-1.92 1.01-2.93zm54.43 17.2h-3l1.48-1.48 1.52 1.48zm8.92-2.22c.17 0 .3.08.39.23.09.15.13.32.13.52s-.04.37-.13.52c-.09.15-.21.23-.39.23-.19 0-.33-.08-.42-.23-.09-.15-.13-.32-.13-.52s.04-.37.13-.52c.09-.15.23-.23.42-.23z"/><path d="M976.02 104.6c-.32-1.98-.32-3.87 0-5.69.32-1.82.81-3.59 1.45-5.32.64-1.73 1.37-3.44 2.18-5.12.8-1.69 1.54-3.38 2.19-5.09.65-1.71 1.14-3.45 1.47-5.22s.32-3.61 0-5.53l-.03-.39c-1.7.84-3.39 1.79-5.08 2.87-1.69 1.07-3.39 2.16-5.11 3.27-1.72 1.11-3.46 2.19-5.22 3.25s-3.57 2.01-5.41 2.84c-1.85.83-3.75 1.49-5.72 2-1.96.5-4 .76-6.11.76-.88 0-1.7-.08-2.45-.24s-1.42-.44-2-.84c-.58-.4-1.07-.92-1.47-1.58-.4-.66-.67-1.48-.82-2.47-.21-1.33-.02-2.77.6-4.32.61-1.55 1.52-3.14 2.71-4.79 1.19-1.64 2.59-3.3 4.21-4.96 1.61-1.66 3.3-3.29 5.08-4.87 1.77-1.58 3.56-3.08 5.35-4.51 1.79-1.43 3.46-2.72 5.01-3.87 1.55-1.15 2.89-2.13 4.03-2.93s1.96-1.38 2.45-1.72c.15-.3.29-.52.43-.64s.29-.23.45-.31c.16-.08.34-.15.53-.23.19-.08.42-.21.68-.4.3-.19.74-.49 1.3-.9.57-.41 1.21-.87 1.92-1.39s1.44-1.06 2.21-1.64 1.49-1.13 2.18-1.64 1.29-.98 1.8-1.4.89-.73 1.13-.92h1.58c1.22-.99 2.52-1.95 3.88-2.88s2.8-1.77 4.3-2.51 3.07-1.34 4.71-1.79 3.34-.68 5.12-.68c.6 0 1.18.08 1.72.23.55.15 1.04.39 1.48.71s.82.73 1.13 1.22c.31.49.53 1.08.66 1.77.06.39.09.91.08 1.58-.01.67-.06 1.38-.16 2.14-.1.76-.23 1.53-.4 2.3s-.38 1.48-.63 2.11c-.25.63-.54 1.15-.87 1.55-.33.4-.69.6-1.08.6h-.74c.11-.49.27-1.15.48-1.98.21-.83.42-1.69.61-2.58.19-.89.35-1.75.48-2.56.13-.82.15-1.47.06-1.97-.09-.49-.25-.92-.5-1.27s-.54-.64-.87-.87-.69-.39-1.08-.48-.77-.15-1.16-.15c-1.53 0-3.17.27-4.95.82-1.77.55-3.6 1.28-5.48 2.21-1.88.92-3.78 1.98-5.71 3.17s-3.81 2.44-5.66 3.75-3.62 2.63-5.32 3.95-3.25 2.57-4.67 3.74c-1.42 1.17-2.67 2.21-3.75 3.13-1.08.91-1.93 1.62-2.53 2.11-.9 1.38-1.98 2.68-3.24 3.92-1.26 1.24-2.58 2.45-3.96 3.66-1.38 1.2-2.79 2.41-4.21 3.63-1.42 1.21-2.73 2.5-3.95 3.85-1.21 1.35-2.27 2.8-3.16 4.33-.89 1.54-1.51 3.23-1.85 5.08.04.3.21.51.52.63.3.12.64.19 1.02.21.38.02.72.02 1.05-.02s.54-.05.64-.05c2.11 0 4.32-.23 6.65-.68s4.66-1.08 6.99-1.9c2.33-.82 4.62-1.79 6.86-2.92s4.34-2.36 6.29-3.71c1.94-1.34 3.68-2.77 5.22-4.29 1.54-1.51 2.77-3.07 3.69-4.66h.39c.49 0 .93.09 1.3.27.38.18.7.42.97.71s.48.63.64 1.02c.16.39.27.77.34 1.16.28 1.63.29 3.5.05 5.59s-.66 4.3-1.24 6.62-1.27 4.69-2.08 7.12c-.81 2.43-1.63 4.8-2.48 7.11-.85 2.31-1.67 4.51-2.45 6.61s-1.43 3.96-1.95 5.59c.02.11-.03.21-.14.31-.12.1-.26.18-.42.26-.16.08-.33.13-.5.18-.17.04-.3.06-.39.06-.19 0-.39-.08-.58-.24s-.31-.35-.35-.56c-.04-.34-.1-.68-.16-1-.04-.26-.09-.52-.13-.77s-.05-.45-.05-.55zm27.26-51.89c.17 0 .26.12.26.35 0 .24-.09.35-.26.35s-.26-.12-.26-.35c0-.23.09-.35.26-.35zM1013.13 76.14c.11-.43.21-.85.32-1.27s.24-.82.4-1.19c.16-.38.36-.73.6-1.05s.54-.6.9-.84l3.74-18.21c.04-.3.18-.52.4-.64.22-.13.47-.2.72-.21.26-.01.51.01.76.05s.42.1.53.16c.77.15 1.35.46 1.72.92.38.46.61 1.01.69 1.64.09.63.07 1.33-.05 2.09-.12.76-.27 1.52-.47 2.27-.19.75-.4 1.47-.61 2.14s-.37 1.25-.45 1.72c.77.15 1.69.09 2.76-.18 1.06-.27 2.2-.67 3.4-1.21 1.2-.54 2.43-1.17 3.67-1.9 1.25-.73 2.43-1.47 3.54-2.22 1.12-.75 2.13-1.48 3.03-2.19s1.61-1.31 2.13-1.8c3.09-1.22 5.99-2.33 8.7-3.32 2.71-.99 5.28-1.89 7.72-2.69s4.8-1.53 7.07-2.16c2.28-.63 4.54-1.21 6.78-1.74 2.25-.53 4.52-1 6.82-1.43 2.3-.43 4.7-.84 7.2-1.22 2.5-.39 5.14-.76 7.93-1.11 2.78-.35 5.78-.71 9.01-1.08l1.19.55c-1.33.06-3.13.26-5.4.58s-4.86.77-7.78 1.34c-2.92.57-6.09 1.26-9.51 2.08-3.42.82-6.94 1.75-10.57 2.8-3.63 1.05-7.29 2.22-10.97 3.5-3.69 1.28-7.25 2.66-10.7 4.16-3.45 1.49-6.71 3.09-9.78 4.79-3.07 1.7-5.81 3.5-8.22 5.41-2.41 1.91-4.4 3.92-5.98 6.01s-2.61 4.28-3.08 6.56c-.06.43-.03.91.11 1.43.14.53.3 1.05.48 1.56s.34 1 .48 1.45c.14.45.17.81.08 1.06-.24 1.46-.75 2.53-1.53 3.21s-1.9.87-3.33.56c-1.29-.26-2.29-.75-3-1.48s-1.21-1.59-1.5-2.58c-.29-.99-.41-2.04-.36-3.14.09-1.11.22-2.17.41-3.18zm117.5-38.77c.32.04.73.1 1.22.16s.88.12 1.16.16c-2.64 0-5.34.04-8.11.13-2.76.09-5.43.22-8.01.4-2.58.18-4.98.42-7.22.73-2.23.3-4.15.66-5.74 1.06l-.23-1.06c2.6-.49 5.09-.9 7.46-1.22s4.66-.55 6.87-.69c2.2-.14 4.34-.18 6.43-.13 2.1.05 4.15.2 6.17.46zm8.35-.03c-.32.19-.72.27-1.19.23-.47-.04-.85-.24-1.13-.58-.13-.17-.05-.27.24-.31.29-.03.62-.03.98.02.37.04.68.12.95.24.27.12.32.25.15.4zm3.61-.74c-.02.13-.09.22-.21.27-.12.05-.25.08-.4.06-.15-.01-.28-.06-.39-.15-.11-.09-.15-.19-.13-.32.02-.15.1-.25.23-.31.13-.05.26-.08.4-.06.14.01.26.06.37.16s.15.22.13.35zm5.83.74c-.17.13-.42.21-.76.24s-.68.02-1.03-.03-.69-.13-1-.24c-.31-.11-.53-.24-.66-.39-.11-.11-.08-.18.1-.23.17-.04.4-.06.69-.06.29 0 .61.02.97.06.35.04.68.1.97.18.29.08.51.15.66.23.15.07.17.15.06.24zm4.9-1.1c.13 0 .23.07.29.21.06.14.09.28.06.42-.02.14-.07.27-.15.39-.08.12-.19.16-.34.11-.15-.02-.26-.1-.32-.24-.06-.14-.09-.28-.08-.42.01-.14.06-.26.14-.37.1-.11.23-.14.4-.1z"/><path d="M1044 78.85c.06-.77.29-1.82.68-3.13.39-1.31.89-2.74 1.5-4.3.61-1.56 1.32-3.16 2.13-4.82.81-1.65 1.64-3.22 2.51-4.71.87-1.48 1.76-2.8 2.66-3.96s1.77-2.02 2.61-2.58c.13.02.31.12.53.31.23.18.44.4.64.64.2.25.38.49.53.74.15.25.23.44.23.56l-.03.55-.03.52-6.35 16.47c-.02.32-.04.61-.06.87-.02.24-.04.47-.05.69-.01.23-.02.4-.02.53-.06.77-.11 1.5-.13 2.18s.05 1.27.21 1.77c.16.5.47.91.93 1.22.46.31 1.14.5 2.05.56 3.07.21 5.7.38 7.9.5 2.19.12 4.1.2 5.74.24 1.63.04 3.07.04 4.32-.02 1.25-.05 2.47-.16 3.66-.31s2.44-.34 3.75-.58c1.31-.24 2.85-.53 4.61-.87 1.76-.34 3.83-.74 6.2-1.19s5.23-.96 8.56-1.51c-2.54.28-4.89.7-7.07 1.26-2.18.56-4.34 1.17-6.48 1.82-2.14.66-4.32 1.31-6.56 1.95s-4.67 1.2-7.3 1.66c-2.63.46-5.53.79-8.7.98-3.17.19-6.75.16-10.75-.1-1.03-.06-2.07-.29-3.11-.69-1.04-.4-1.97-.93-2.77-1.61-.81-.68-1.45-1.49-1.93-2.45-.5-.94-.7-2.01-.61-3.19zm13.92-40.38c.06-.79.28-1.55.64-2.27.37-.72.96-1.32 1.77-1.79.26.11.64.2 1.16.27.52.08 1.08.16 1.69.26.61.1 1.24.22 1.89.37.64.15 1.22.34 1.74.58.52.24.93.54 1.24.9.31.37.46.81.43 1.32-.13.3-.26.58-.39.84-.11.24-.23.46-.37.68-.14.21-.27.39-.4.52-.11.17-.3.4-.56.68-.27.28-.58.59-.94.93-.35.34-.74.69-1.14 1.05s-.8.69-1.18 1c-.38.31-.72.58-1.05.81s-.57.37-.74.44c-.37-.41-.77-.86-1.22-1.35-.45-.49-.88-1.02-1.27-1.56-.4-.55-.73-1.13-.98-1.74s-.36-1.28-.32-1.94z"/><path d="M1079.37 81.49c0-.64 0-1.22.02-1.72.01-.5.06-1 .15-1.48s.21-.98.39-1.5.42-1.1.74-1.74c.15-.15.5-.55 1.06-1.19s1.26-1.43 2.11-2.37 1.81-1.98 2.88-3.13c1.07-1.15 2.21-2.31 3.42-3.48 1.2-1.17 2.45-2.31 3.74-3.42 1.29-1.11 2.55-2.09 3.79-2.95 1.23-.86 2.43-1.55 3.59-2.06s2.22-.77 3.19-.77c.32 0 .64.07.97.21.32.14.64.29.97.45s.64.31.97.44.64.19.97.19c1.03 0 2.06-.37 3.09-1.11s2.05-1.74 3.06-3 2-2.7 2.96-4.33c.97-1.63 1.9-3.34 2.8-5.12.9-1.78 1.76-3.58 2.56-5.4.8-1.82 1.55-3.53 2.24-5.14s1.31-3.05 1.85-4.32c.55-1.27 1.02-2.26 1.4-2.98.39-.72.68-1.09.89-1.11.2-.02.29.42.27 1.32-.02 1.25-.22 2.69-.6 4.33-.38 1.64-.87 3.41-1.48 5.29s-1.32 3.82-2.11 5.83c-.79 2.01-1.61 3.99-2.45 5.93-.84 1.94-1.67 3.81-2.48 5.61-.82 1.79-1.57 3.42-2.26 4.87s-1.27 2.68-1.76 3.69c-.48 1.01-.8 1.71-.95 2.09-.49.82-.9 1.53-1.22 2.14s-.58 1.2-.77 1.76c-.19.56-.33 1.14-.42 1.76s-.13 1.32-.13 2.11c0 1.46.23 2.62.68 3.46.45.85 1.08 1.49 1.89 1.93.8.44 1.76.73 2.85.85s2.29.19 3.58.19c5.03 0 10-.41 14.91-1.24 4.91-.83 9.79-1.86 14.65-3.11 4.86-1.25 9.71-2.59 14.55-4.04 4.84-1.45 9.72-2.8 14.61-4.04 4.9-1.25 9.83-2.28 14.81-3.11 4.97-.83 10.04-1.24 15.2-1.24-1.68.09-3.48.26-5.4.53-1.92.27-3.9.6-5.93 1s-4.08.83-6.15 1.31c-2.07.47-4.08.96-6.03 1.47-1.94.5-3.8 1-5.57 1.5-1.77.49-3.38.96-4.82 1.39-1.44.43-2.67.81-3.71 1.14-1.03.33-1.78.58-2.26.73-.97.32-2.21.77-3.72 1.35-1.52.58-3.22 1.23-5.11 1.95-1.89.72-3.94 1.49-6.16 2.32-2.21.83-4.51 1.64-6.88 2.45s-4.79 1.57-7.23 2.3c-2.45.73-4.86 1.38-7.23 1.95-2.38.57-4.68 1.02-6.91 1.35-2.23.33-4.32.5-6.25.5-1.83 0-3.41-.15-4.75-.44-1.34-.29-2.51-.72-3.5-1.29-.99-.57-1.83-1.28-2.51-2.14-.69-.86-1.27-1.86-1.76-3.01-.48-1.15-.9-2.44-1.26-3.87-.36-1.43-.69-3-1.02-4.72-.75.75-1.67 1.72-2.74 2.9-1.07 1.18-2.26 2.44-3.54 3.79-1.29 1.34-2.65 2.69-4.09 4.03-1.44 1.34-2.89 2.56-4.35 3.64-1.46 1.08-2.91 1.97-4.33 2.66-1.43.69-2.79 1.03-4.08 1.03-.82 0-1.47-.15-1.97-.44s-.89-.68-1.18-1.16c-.29-.48-.48-1.04-.58-1.66-.11-.63-.16-1.26-.16-1.91zm24.43-20.66c-.75 0-1.73.3-2.93.89-1.2.59-2.5 1.36-3.88 2.32-1.38.96-2.78 2.04-4.17 3.25-1.4 1.21-2.66 2.44-3.8 3.69-1.14 1.25-2.06 2.45-2.77 3.61s-1.06 2.17-1.06 3.03c1.07 0 2.28-.31 3.61-.93 1.33-.62 2.7-1.43 4.11-2.42 1.41-.99 2.79-2.1 4.16-3.34 1.36-1.24 2.61-2.48 3.72-3.72 1.12-1.25 2.06-2.43 2.84-3.56s1.27-2.07 1.48-2.82h-1.31zm110.57 0h-1.29v-1.29l1.29 1.29zm6.45-1.29c-.32.64-.67 1.02-1.05 1.13-.38.11-.89.16-1.53.16h-1.29c.32-.64.67-1.02 1.05-1.13.38-.11.89-.16 1.53-.16h1.29z"/></g></svg>
</div>
</div>
</div>
<h2>Overview</h2>
<ol>
<li><a href="/blog/the-dark-side-of-the-grid/#whats-css-grid-layout">What’s CSS Grid Layout?</a> (in part 1)</li>
<li><a href="/blog/the-dark-side-of-the-grid/#name-and-theme-of-this-article">Name and theme of this article</a> (in part 1)</li>
<li><a href="/blog/the-dark-side-of-the-grid/#floyd-fact-1">Pink Floyd Fun Fact 1</a> (in part 1)</li>
<li><a href="/blog/the-dark-side-of-the-grid/#compromising-on-semantics">Compromising on Semantics</a> (in part 1)</li>
<li><a href="#floyd-fact-1">Pink Floyd Fun Fact 2</a> (in this article)</li>
<li><a href="#visual-order">Changing Visual Order</a> (in this article)</li>
<li>Cross Browser Support</li>
<li>Pink Floyd Fun Fact 3</li>
<li>Whose responsibility is it?</li>
<li>Pink Floyd Fun Fact 4</li>
</ol>
<hr>
<h2>Changing Visual Order</h2>
<p>In <a href="/blog/the-dark-side-of-the-grid/">part 1</a>, I addressed the issue with flattening document structures. Before we talk about changing visual order, let me enlighten you with more Pink Floyd wisdom.</p>
<div class="fact lazy u-full-width">
<div class="fact__inner">
<h2 id="floyd-fact-1" class="fact__heading">Pink Floyd Fun Fact #2</h2>
<p>In 1975, Pink Floyd helped to finance the movie Monty Python and the Holy Grail by the comedy group Monty Python. Led Zeppelin and Genesis were also amongst the investors.</p>
</div>
</div>
<h3>Visual order</h3>
<p>The CSS Grid Layout specification provides us with many ways of changing visual order. This flexibility is a nice thing to have but bad for accessibility if we don’t use it consciously.<br />
Before we look at what’s possible with Grid, let’s briefly talk about visual order.</p>
<ol>
<li>Both tab order and the order in which screen readers read content follow DOM order.</li>
<li>Changing visual order with CSS has no effect on DOM order.</li>
</ol>
<p>No matter where we place items with CSS, keyboard users will still encounter elements in the order in which they appear in the HTML document.</p>
<div class="js-a-focus-demo" data-button="Show tab order">
<h4>Visual order matches DOM order</h4>
<ul class="a-grid-order">
<li class="a-grid-order__item" data-number="1">
<a class="a-grid-order__link no-line" href="#">
<span class="a-grid-order__text">{ 1 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="2">
<a class="a-grid-order__link no-line" href="#">
<span class="a-grid-order__text">{ 2 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="3">
<a class="a-grid-order__link no-line" href="#">
<span class="a-grid-order__text">{ 3 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="4">
<a class="a-grid-order__link no-line" href="#">
<span class="a-grid-order__text">{ 4 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="5">
<a class="a-grid-order__link no-line" href="#">
<span class="a-grid-order__text">{ 5 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="6">
<a class="a-grid-order__link no-line" href="#">
<span class="a-grid-order__text">{ 6 }</span>
</a>
</li>
</ul>
</div>
<div class="js-a-focus-demo" data-button="Show tab order">
<h4>Visual order doesn’t match DOM order</h4>
<ul class="a-grid-order">
<li class="a-grid-order__item" data-number="1" style="order: 1">
<a class="a-grid-order__link no-line" href="#">
<span class="a-grid-order__text">{ 1 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="4" style="order: 4">
<a class="a-grid-order__link a-grid-order__link--mismatch no-line" href="#">
<span class="a-grid-order__text a-grid-order__text--mismatch">{ 2 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="2" style="order: 2">
<a class="a-grid-order__link a-grid-order__link--mismatch no-line" href="#">
<span class="a-grid-order__text a-grid-order__text--mismatch">{ 3 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="6" style="order: 6">
<a class="a-grid-order__link a-grid-order__link--mismatch no-line" href="#">
<span class="a-grid-order__text a-grid-order__text--mismatch">{ 4 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="5" style="order: 5">
<a class="a-grid-order__link no-line" href="#">
<span class="a-grid-order__text">{ 5 }</span>
</a>
</li>
<li class="a-grid-order__item" data-number="3" style="order: 3">
<a class="a-grid-order__link a-grid-order__link--mismatch no-line" href="#">
<span class="a-grid-order__text a-grid-order__text--mismatch">{ 6 }</span>
</a>
</li>
</ul>
</div>
<h3>Disconnect between content and presentation</h3>
<p>If the visual order and the DOM order don’t match, it can irritate and confuse users up to a point where the experience is so bad that the site is unusable.</p>
<ol>
<li>Visual order concerns keyboard users because they may have trouble predicting where focus will go next.</li>
<li>It may irritate <a href="https://axesslab.com/make-site-accessible-screen-magnifiers/">users of screen magnifiers</a> if the enlarged portion of the screen skips around a lot.</li>
<li>If a blind user is working with a sighted user, who reads the page in visual order, it may confuse them when they encounter information in different order.</li>
</ol>
<h3>Manipulating visual order</h3>
<p>When you’re applying any property that changes order, you should especially pay attention to testing with a keyboard. Use the <kbd>Tab</kbd> and <kbd>Shift</kbd> + <kbd>Tab</kbd> keys to test if your website is usable without a mouse.</p>
<p>This applies to all properties, not just to those associated with CSS Grid. <br />
However, I'll focus on manipulating order in grids.</p>
<h3>Explicit placement</h3>
<p>One of the most exciting features in Grid is the ability to place grid items anywhere inside or outside of the grid.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1557599128/articles/dark-grid/grid-explicit.png" alt="Visualization of the lines in a 3 by 2 grid" /></p>
<p>You can place items explicitly by defining on which line they start or end. For vertical lines, there’s <code>grid-column-start</code>, <code>grid-column-end</code>, or the shorthand <code>grid-column</code>. For horizontal lines, <code>grid-row-start</code>, <code>grid-row-end</code>, and <code>grid-row</code>. These rules allow a line number (either positive or negative) or the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column#Values">span keyword</a>.</p>
<pre><code class="language-html"><div class="grid">
<div class="grid__item">Item 1</div>
<div class="grid__item">Item 2</div>
<div class="grid__item">Item 3</div>
<div class="grid__item">Item 4</div>
</div></code></pre>
<p>The order in the DOM in this grid is <em>Item 1 - 2 - 3 - 4</em>.</p>
<pre><code class="language-css">.grid {
/* Draw a grid...*/
display: grid;
/* with 2 200px columns. */
grid-template-columns: repeat(2, 200px);
/* Make all rows 100px high... */
grid-auto-rows: 100px;
/* and add 15px space between cells. */
grid-gap: 15px;
}
/* Place item on the second horizontal and vertical line */
.grid__item:nth-child(2) {
grid-column: 2;
grid-row: 2;
}
/* Place item on the first horizontal and vertical line */
.grid__item:nth-child(4) {
grid-column: 1;
grid-row: 1;
}</code></pre>
<p>The order in the DOM is still <em>Item 1 - 2 - 3 - 4</em> but the visual order is now <em>Item 4 - 1 - 3 - 2</em>.</p>
<p>Explicit placement might create a mismatch between DOM order and visual order.</p>
<div class="a-grid-explicit js-a-focus-demo" data-button="Show tab order">
<ul class="a-grid-explicit__items">
<li class="a-grid-explicit__item">
<button type="button" class="a-grid-explicit__button">Item 1</button>
</li>
<li class="a-grid-explicit__item">
<button type="button" class="a-grid-explicit__button">Item 2</button>
</li>
<li class="a-grid-explicit__item">
<button type="button" class="a-grid-explicit__button">Item 3</button>
</li>
<li class="a-grid-explicit__item">
<button type="button" class="a-grid-explicit__button">Item 4</button>
</li>
</ul>
</div>
<h3>Auto flow</h3>
<p>Explicitly placing items that have the same size is one thing, but placing differently sized items may have unexpected side effects with Grids default auto-placement algorithm. The combination of explicit and implicit placement sometimes results in unwanted gaps between grid items. This is because of the default placement algorithm only ever moving forward when placing items and never backtracking to fill holes.</p>
<pre><code class="language-css">.grid {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-auto-rows: 40px;
grid-gap: 20px;
}
/* You can use the span keyword to make items span multiple columns or rows. */
.item:nth-child(1) {
grid-row-end: span 3;
}
.item:nth-child(3) {
grid-row-end: span 3;
}
.item:nth-child(4) {
grid-column: span 2;
}
.item:nth-child(5) {
grid-row: span 2;
}
.item:nth-child(7) {
grid-column: 2 / span 2;
}
.item:nth-child(8) {
grid-column: 2 / span 3;
}
.item:nth-child(9) {
grid-row: span 2;
}</code></pre>
<div class="js-a-focus-demo" data-button="Show tab order">
<div class="a-grid-flow">
<button type="button">1</button>
<button type="button">2</button>
<button type="button">3</button>
<button type="button">4</button>
<button type="button">5</button>
<button type="button">6</button>
<button type="button">7</button>
<button type="button">8</button>
<button type="button">9</button>
</div>
</div>
<p>This can give your designs a nice touch, but it can also annoy. Grid's default auto-placement algorithm can be changed by switching from a “sparse” to a “dense” packing mode using the <code>grid-auto-flow</code> property.</p>
<pre><code class="language-css">.grid {
display: grid;
grid-template-columns: repeat(3, 200px);
grid-auto-rows: 70px;
grid-gap: 20px;
grid-auto-flow: dense;
}</code></pre>
<div class="js-a-focus-demo" data-button="Show tab order">
<div class="a-grid-flow dense">
<button type="button">1</button>
<button type="button">2</button>
<button type="button">3</button>
<button type="button">4</button>
<button type="button">5</button>
<button type="button">6</button>
<button type="button">7</button>
<button type="button">8</button>
<button type="button">9</button>
</div>
</div>
<p>This is a dream come true but the advantage of the default packing mode is that order stays intact which isn’t guaranteed with <code>grid-auto-flow: dense;</code>.</p>
<p>Play with the <a href="https://codepen.io/matuzo/pen/pONEzJ?editors=1100">auto-flow demo on CodePen</a> or read more about <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-flow">grid-auto-flow on MDN</a>.</p>
<div class="info">
<h4><span class="info__label">Aaahhmmm</span><span class="info__heading">Implicit, explicit, whatisit?</span></h4>
<p>If you’re not sure what I mean by explicit and implicit, head over to CSS-Tricks and read about the <a href="https://css-tricks.com/difference-explicit-implicit-grids/" rel="noopener">difference between explicit and implicit grids</a>.</p></div>
<h3>Order</h3>
<p>You might already be familiar with the <code>order</code> property because it has been around since Flexbox. <code>order</code> can be set to any integer value to change the ordering of flex items. This also works with grid items.</p>
<pre><code class="language-css">.grid {
display: grid;
grid-template-columns: repeat(2, 200px);
grid-auto-rows: 100px;
grid-gap: 1.4rem;
}
.grid__item:nth-child(2) {
order: 4;
}
.grid__item:nth-child(4) {
order: 1;
}</code></pre>
<p><a href="https://codepen.io/matuzo/pen/XooEXd">See the order property demo on CodePen</a> or read more about the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/order">order property on MDN</a>.</p>
<h3>Absolute positioning</h3>
<p>It’s possible to combine absolute positioning and explicit placement. I haven’t found a use case yet in any of my projects but it’s interesting how a positioned item behaves inside a grid.</p>
<p>In the following 3 by 2 grid with 6 items I’m moving the second item from line 2 to line 3 by applying <code>grid-column: 3</code>. Since I haven’t set the row property explicitly and I haven’t placed other items, all subsequent items move one cell with the explicitly placed item. DOM order still matches visual order.</p>
<pre><code class="language-css">ul {
display: grid;
grid-template-columns: repeat(3, 200px);
grid-auto-rows: 100px;
grid-gap: 1.4rem;
}
li:nth-child(2) {
/* Move the second item to the third vertical line */
grid-column: 3;
}</code></pre>
<div class="js-a-focus-demo" data-button="Show tab order">
<ul class="a-grid-absolute">
<li><button type="button">Item 1</button></li>
<li><button type="button">Item 2</button></li>
<li><button type="button">Item 3</button></li>
<li><button type="button">Item 4</button></li>
<li><button type="button">Item 5</button></li>
<li><button type="button">Item 6</button></li>
</ul>
</div>
<p>Now let’s see what happens if we add absolute positioning to the mix and position the item in the top left corner relative to its parent.</p>
<pre><code class="language-css">ul {
position: relative;
}
li:nth-child(2) {
grid-column: 3;
position: absolute;
left: 0;
top: 0;
}</code></pre>
<p>Positioned grid items lie on top of other items just like any absolute positioned element. They don’t affect the position of other items, and they’re completely ignored during auto-placement.\<br />
This is where it gets interesting: setting <code>left</code> and <code>top</code> to <code>0</code> doesn’t place them in the top left corner of their parent item (the <code>ul</code>) but in the cell they’re placed in.</p>
<div class="js-a-focus-demo" data-button="Show tab order">
<ul class="a-grid-absolute">
<li><button type="button">Item 1</button></li>
<li class="positioned"><button type="button">Item 2</button></li>
<li><button type="button">Item 3</button></li>
<li><button type="button">Item 4</button></li>
<li><button type="button">Item 5</button></li>
<li><button type="button">Item 6</button></li>
</ul>
</div>
<p>Again, this may cause a disconnect between content and presentation.</p>
<h3>Areas</h3>
<p>Time to make a confession: I’m deeply in love with Grid Areas ❤️. I’ll share my intense feelings about this property in another article soon. Areas are awesome, but nobody is perfect and since this article deals with the dark side of things, let’s see what may go wrong.</p>
<p>The <code>grid-template-areas</code> property does 2 things. <br />
First, it describes layouts in CSS visually. If you have a 2-column layout with a header, content, sidebar and a footer, this is what it may look like in CSS.</p>
<pre><code class="language-css">.layout {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-areas:
'header header'
'content sidebar'
'footer footer';
}</code></pre>
<p>Second, it defines named grid areas where any grid item may be placed inside.</p>
<p>Let’s take this example where the source order is wrong.</p>
<pre><code class="language-html"><body>
<footer>Footer</footer>
<header>Header</header>
<main>Main</main>
</body></code></pre>
<p>The source order of these elements is <em>footer - header - main</em>, where it should be <em>header - main - footer</em>. The correct approach to fixing this is to change the order in HTML but with Areas we don’t have to.<br />
All it takes is to define the correct layout in CSS and then place the elements in the respective area using the <code>grid-area</code> property.</p>
<pre><code class="language-css">body {
display: grid;
/* Three single column, single row areas */
grid-template-areas:
'header'
'content'
'footer';
}
footer {
/* place the footer in the third row/area */
grid-area: footer;
}
header {
/* place the header in the first row/area */
grid-area: header;
}
main {
/* place the main element in the second row/area */
grid-area: content;
}</code></pre>
<p>This is so simple, so beautiful, yet so dangerous. It looks right, but the order is only correct on the surface because the source order is still the same.</p>
<div class="js-a-focus-demo" data-button="Show tab order">
<div class="a-grid-areas">
<div class="footer">
<button type="button">Footer</button>
</div>
<div class="header">
<button type="button">Header</button>
</div>
<div class="main">
<button type="button">Main</button>
</div>
</div>
</div>
<h2>Recap</h2>
<p>None of these features are bad, but just they may affect an important part of the user experience negatively. If you’re changing visual order, be aware of the effects it may have.</p>
<ul>
<li>Test your components with the keyboard by pressing <kbd>Tab</kbd> or <kbd>Shift + Tab</kbd> for the opposite direction.</li>
<li>Make sure that visual order is comprehensible and that it matches DOM order as good as in any way possible.</li>
<li>Test on different devices and screen sizes. <a href="https://webaim.org/projects/screenreadersurvey7/#mobilekeyboard">People also use keyboards on mobile devices</a>.</li>
<li>You can use a tool Like <a href="https://chrome.google.com/webstore/detail/accessibility-insights-fo/pbjjkligggfmakdaogkfomddhfmpjeni?hl=en">Accessibility Insights</a> to visualize and better understand the tab order of your web pages.</li>
</ul>
<figure class="figure">
<div class="content__video-wrapper">
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/lTTd1uWVcEs?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="iframe title attribute demo"></iframe>
</div>
</div>
<figcaption>Accessibility Insights tracks all the tab stops you make when you press the <kbd>tab</kbd> key.</figcaption>
</figure>
</div>
<p>Thanks for reading. ❤️</p>
<p>The last part of this series, "Cross Browser Support", will be published soon.</p>
<p>PS: Thanks to <a href="https://twitter.com/bebraw">Juho</a> and <a href="https://twitter.com/codeability">E.J.</a> for proofreading.</p>
<script>
const demos = document.querySelectorAll('.js-demo');
let demoObserver;
function demoAddButton(demo) {
const controls = document.createElement('div');
controls.classList.add('demo__controls');
const button = document.createElement('button');
const button_inner = document.createElement('span');
button_inner.classList.add('btn__inner');
button_inner.textContent = 'Replay animation';
button.appendChild(button_inner);
button.classList.add('btn');
button.classList.add('demo__btn');
button.addEventListener('click', function() {
demo.classList.remove('demo--playing');
setTimeout(() => {
demo.classList.add('demo--playing');
}, 600);
});
controls.appendChild(button);
demo.appendChild(controls);
}
function demoVisible(entries) {
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
if (entry.intersectionRatio === 1) {
setTimeout(() => {
entry.target.classList.add('demo--playing');
demoObserver.unobserve(entry.target);
}, 400);
}
}
}
function enhanceDemos() {
if (demos.length) {
for (let i = 0; i < demos.length; i++) {
const demo = demos[i];
demoAddButton(demo);
demoObserver = new IntersectionObserver(demoVisible, {
threshold: [1]
});
if (demo.classList.contains('demo--playing')) {
continue;
}
demoObserver.observe(demo);
}
}
}
enhanceDemos();
/**
* Order demo
*/
var demos2 = document.querySelectorAll('.js-a-focus-demo');
var activeLink = [];
for (let i = 0; i < demos2.length; i++) {
activeLink.push(0);
(function () {
const demo = demos2[i];
var links = demo.querySelectorAll('a, button');
var animating = false;
var focusInterval;
var playpause = document.createElement('button');
var playpause_inner = document.createElement('span');
playpause.classList.add('btn');
playpause_inner.classList.add('btn__inner');
playpause.appendChild(playpause_inner);
var buttonTextActive = 'Stop animation';
var buttonTextInactive = 'Play animation';
if (demo.hasAttribute('data-button')) {
buttonTextInactive = demo.getAttribute('data-button');
}
setButtonText(buttonTextInactive);
demo.insertBefore(playpause, demo.firstChild);
playpause.addEventListener('click', function () {
if (animating) {
clearInterval(focusInterval);
playpause_inner.textContent = buttonTextInactive;
} else {
links[activeLink[i]].classList.add('focus');
focusInterval = window.setInterval(focusNext, 700);
setButtonText(buttonTextActive);
}
animating = !animating;
});
function setButtonText(text) {
playpause_inner.textContent = text;
}
function focusNext() {
if (activeLink[i] < links.length - 1) {
activeLink[i]++;
} else {
activeLink[i] = 0;
}
demo.querySelector('.focus').classList.remove('focus');
links[activeLink[i]].classList.add('focus');
}
})(i);
}
</script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CThe+Dark+Side+of+the+Grid+%28Part+2%29%E2%80%9D">blog@matuzo.at</a>.</p> Building the most inaccessible site possible with a perfect Lighthouse score2019-05-31T00:00:00+00:00https://www.matuzo.at/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score
<p>It’s always nice to see when people post their Lighthouse scores on social media to highlight how well they’ve optimised their own or their client's website. It shows that they care about the quality of what they build.</p><style>
.lighthouse-test {
position: relative;
}
.lighthouse-test button {
position: absolute;
z-index: 111;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
max-height: 5.3rem;
}
.lighthouse-test img {
opacity: 0;
transition: opacity 0.3s;
}
.lighthouse-test .content__image-wrapper::before,
.lighthouse-test .content__image-wrapper::after {
display: none;
}
.lighthouse-test--finished button {
display: none;
}
.lighthouse-test--finished img {
opacity: 1;
}
</style>
<img alt="A perfect lighthouse score for performance, accessibility, best practices and SEO" height="131" loading="auto" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/0f545c38b8-1698841510/lighthousescore.png" width="729">
<p>Lighthouse awards us with the number 100 in a green circle if we did an exceptional job. It’s something you can proudly share with your client or on Twitter.</p>
<p>It’s important to measure the quality of our code, but it’s even more important that we interpret the scores automatic testing tools give us correctly. If Lighthouse tells us that our site is 100% accessible, it doesn’t mean it is. It just means we’ve laid the groundwork for manual testing. Lighthouse uses the <a href="https://github.com/dequelabs/axe-core">axe-core</a> accessibility testing library with a <a href="https://github.com/GoogleChrome/lighthouse/blob/ad0a747a712b815677b6ea3bcc59ee7a0883426d/lighthouse-core/gather/gatherers/accessibility.js#L26-L50">custom set of rules</a> for its tests. It identifies some bad practices, but not all of them. Other practices aren’t bad per se, but can be harmful, if we misuse them.</p>
<p>With automatic testing alone you can’t ensure good quality. To prove that, I built the most inaccessible site possible with a perfect Lighthouse score.</p>
<h2>Background</h2>
<p>Zach Leatherman recently posted this on <a href="https://twitter.com/zachleat/status/1122184546609446919">twitter</a>:</p>
<blockquote>
<p>Free blog post idea:</p>
<p>How to Build the Slowest Website with a Perfect Lighthouse Score</p>
</blockquote>
<p>And here’s <a href="https://twitter.com/pepelsbey_/status/1122203926584074240">Vadim Makeev’s response</a> to his tweet, which inspired me to write this post.</p>
<blockquote>That would be a wonderful read! Here’s one for a11y audit:<br> `<img src=picture.png alt=picture.png>`</blockquote>
<p>I thought it would be a fantastic idea to not just try to mess with as many people as possible, but get rewarded with a perfect Lighthouse score on top.</p>
<h2>Let’s exclude as many people as possible</h2>
<p>We’ll take this simple, accessible page as a basis.</p>
<img alt="A page with a heading, a link, two paragraphs, a list and a simple form" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/6326808c13-1698841509/lighthouse_step1.png">
<p><a href="https://codepen.io/matuzo/pen/vwVRJx">CodePen: “100%” accessible - step 0</a></p>
<h3>🖕 CSS only 🖕</h3>
<p>Let’s start nice and easy. I want to make sure that CSS is a dependency on my perfect website. To achieve that I'm adding the <code>hidden</code> attribute to the <code>body</code> element. <code>hidden</code> is the HTML equivalent to <code>display: none;</code> in CSS. (Checkout <a href="https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html">Inclusively Hidden</a> by <a href="https://twitter.com/scottohara">Scott O’Hara</a>, if you want to learn more about accessible hiding).</p>
<p class="code-label"><strong>HTML</strong></p>
<pre><code class="language-html"><body hidden>
...
</body></code></pre>
<img alt="A blank page" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/4b7154486f-1698841509/lighthouse_step2.png">
<p><code>hidden</code> hides content visually, and it removes it from the accessibility tree. That alone would be enough to exclude everyone and pass the Lighthouse tests, but I don’t want to make it too easy on myself. I want to create a site that’s perfectly inaccessible and technically still displays content.<br />
So let’s add some CSS and bring our content back.</p>
<p class="code-label"><strong>HTML</strong></p>
<pre><code class="language-html"><head>
<link rel="stylesheet" href="style.css" />
</head>
<body class="loaded" hidden>
...
</body></code></pre>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">.loaded {
display: block;
}</code></pre>
<p>We’re back to where we were before but now CSS must load if users want to get access to content on our site.</p>
<p><a href="https://s.codepen.io/matuzo/pen/QRZmrJ">CodePen: “100%” accessible - step 1</a></p>
<h3>🖕 JS only 🖕</h3>
<p>Let’s add one more dependency. I’m not applying the class that displays our content in HTML anymore, but I add it via JS.</p>
<p class="code-label"><strong>HTML</strong></p>
<pre><code class="language-html"><head>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body hidden>
...
</body></code></pre>
<p class="code-label"><strong>JS</strong></p>
<pre><code class="language-js">document.querySelector('body').classList.add('loaded');</code></pre>
<p>Great! The site still looks the same but in order for it to display anything at all the CSS and JS file must load and work properly.</p>
<p><a href="https://s.codepen.io/matuzo/pen/GaYxLx">CodePen: “100%” accessible - step 2</a></p>
<p>I'd say it's time for our first Lighthouse test. Fingers crossed! 🤞🏼</p>
<div class="lighthouse-test js-lighthouse-test">
<button class="btn js-run-lighthouse-test" type="button"><span class="btn__inner">Run Lighthouse test</span></button>
<span class="u-vh js-lighthouse-status" role="status"></span>
<img alt="Score: 100" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/4056090ba4-1698841511/lighthouse_test.png">
</div>
<p>Perfect score on a CSS and JS only site. That's great, but we can do better.</p>
<h3>🖕 Screen reader users 🖕</h3>
<p>There are many ways to exclude screen reader users. The easiest and most efficient way is to use the <code>aria-hidden="true"</code> attribute and value. This attribute is powerful and we must apply it with caution, because it removes elements from the accessibility tree. Normally, we may only use it to improve the experience for users of assistive technologies by <a href="https://www.w3.org/WAI/PF/aria/states_and_properties#aria-hidden">removing redundant or extraneous content</a>.<br />
In our website we’re putting it on the <code>body</code> element.</p>
<p class="code-label"><strong>HTML</strong></p>
<pre><code class="language-html"><body hidden aria-hidden="true">
...
</body></code></pre>
<p>Screen reader users will now experience one of those <a href="https://webaim.org/projects/million/"><em>“rare”</em> moments</a> when they have to deal with an inaccessible site. <span aria-label="(sarcasm)" title="(sarcasm)">*</span></p>
<p><strong>Update 06.10.21</strong>: Originally, Lighthouse didn’t flag this as an issue, but they seem to have fixed it. That’s great! Unfortunately (fortunately for this article), we can work around that by setting <code>tabindex</code> to <code>-1</code> on all focusable elements.</p>
<pre><code class="language-html"><a href="https://www.matuzo.at/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/" rel="noopener" tabindex="-1">
Read the article
</a>
<form action="#form">
<p>
<label for="text">Dummy form</label><br>
<input type="text" id="text" tabindex="-1">
</p>
<button tabindex="-1">Send</button>
</form></code></pre>
<p><a href="https://s.codepen.io/matuzo/pen/OYBZbd">CodePen: “100%” accessible - step 3</a></p>
<h3>🖕 Keyboard users 🖕</h3>
<p>Keyboard users can navigate through a page by pressing the <kbd>Tab</kbd> key to jump from one interactive element to another. Browsers show an outline around these items if they’re in focus.</p>
<img alt="Focus outline on a text link" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/eba6a56b30-1698841510/lighthouse_step4.png">
<p>Let’s get rid of that.</p>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">*:focus {
outline: none !important;
}</code></pre>
<p>All it takes are 3 lines of CSS to exclude a whole group of people from being able to access the site. Although, technically, they can still interact with it. They won’t see the focus indicator anymore but interactive elements are still tababble. Since this experiment is all about exclusion, let’s make sure that the keyboard can’t be used at all.</p>
<p class="code-label"><strong>JS</strong></p>
<pre><code class="language-js">document.addEventListener('keydown', function (e) {
e.preventDefault();
});</code></pre>
<p>Our exclusion-first app now removes the default functionality of all keys.</p>
<p><a href="https://s.codepen.io/matuzo/pen/vwVrxo">CodePen: “100%” accessible - step 4</a></p>
<p>Time for another test.</p>
<div class="lighthouse-test js-lighthouse-test">
<button class="btn js-run-lighthouse-test" type="button"><span class="btn__inner">Run Lighthouse test</span></button>
<span class="u-vh js-lighthouse-status" role="status"></span>
<img alt="Score: 100" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/4056090ba4-1698841511/lighthouse_test.png">
</div>
<p>Still perfect.<br><br />
Okay, now it's time to get dirty.</p>
<h3>🖕 High contrast mode 🖕</h3>
<p>People with low vision can improve contrasts on Windows by enabling the so called <a href="https://developer.paciellogroup.com/blog/2016/12/windows-high-contrast-mode-the-limited-utility-of-ms-high-contrast/">High Contrast Mode</a>.</p>
<img alt="Windows with high contrasting colors. Black background and yellow text." loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/78b3396ed8-1698841509/lighthouse_step5.png">
<p>The whole operating system uses high contrasting colors for all applications including browsers and websites.</p>
<p>We can target high contrast mode users specifically by using a dedicated media feature.</p>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">@media screen and (-ms-high-contrast: active) {
/* High contrast styling rules */
* {
color: #000000;
}
}</code></pre>
<p>Rules in this media query only apply if High Contrast is enabled. Unfortunately, we don’t know which colors the theme uses, nor if it’s a light or dark theme. Setting the color to <code>#000000</code> on all elements might or might not work, depending on user preference.<br />
This fifty-fifty chance is not exclusive enough for me, but we’re lucky: Windows High Contrast colors are mapped to <a href="https://www.w3.org/wiki/CSS/Properties/color/keywords#System_Colors">CSS system color keywords</a>. We can use these system color keywords to make sure our text always matches our High Contrast Mode background color, regardless of what it is set to. The background color is mapped to <code>window</code>. So, let’s use the value of the background color for the text color of all elements.</p>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">@media screen and (-ms-high-contrast: active) {
* {
color: window !important;
}
}</code></pre>
<img alt="Windows in high contrast but the text color has the same color as the background" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/5b2e6d3f54-1698841511/lighthouse_step5_2.png">
<p>Oh, man. This is so evil. My LinkedIn inbox will be filled with job offerings by companies like Facebook and Uber.</p>
<p><a href="https://s.codepen.io/matuzo/pen/Ezdpoa">CodePen: “100%” accessible - step 5</a></p>
<h3>🖕 Mouse users 🖕</h3>
<p>Excluding mouse users is easy, we just remove the cursor.</p>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">*,
*:hover {
cursor: none;
}</code></pre>
<p><code>cursor: none;</code> is to mouse users what <code>outline: none;</code> is to keyboard users. Getting your bearings is initially difficult, but interactive elements are still clickable. Let’s improve the quality of our app by decreasing the user experience once more.</p>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">body {
pointer-events: none;
}</code></pre>
<p><code>pointer-events: none;</code> frees our users from the ability to click anything on our site. This property is well-supported, but if we want to make sure that this feature works on as many browsers as possible, we can apply a principle called progressive degradation™.</p>
<p class="code-label"><strong>JS</strong></p>
<pre><code class="language-js">function removeA11y() {
if ('pointerEvents' in document.body.style) {
console.log('pointer-events supported');
return;
}
document.addEventListener('click', function (e) {
e.preventDefault();
});
}
removeA11y();</code></pre>
<p>This JavaScript fallback will kick in and remove click events from all elements, if the browser doesn't support the <code>pointer-events</code> property.</p>
<p><a href="https://s.codepen.io/matuzo/pen/zQmJYB">CodePen: “100%” accessible - step 6</a></p>
<div class="lighthouse-test js-lighthouse-test">
<button class="btn js-run-lighthouse-test" type="button"><span class="btn__inner">Run Lighthouse test</span></button>
<span class="u-vh js-lighthouse-status" role="status"></span>
<img alt="Score: 100" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/4056090ba4-1698841511/lighthouse_test.png">
</div>
<p>Great! Still perfectly accessible!</p>
<h3>🖕 Readability 🖕</h3>
<p>We can’t use the mouse or keyboard anymore but we can still read the content above the fold. Let’s do something about that.</p>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">body {
opacity: 0.03;
}</code></pre>
<p>Our page content is still present but almost invisible. Fabulous!</p>
<p><strong>Update 06.10.21</strong>: Originally, Lighthouse didn’t flag this as an issue, but they seem to have fixed it. That’s great! Unfortunately (fortunately for this article), we can work around that by using the filter property instead.</p>
<pre><code class="language-css">body {
filter: opacity(0.03);
}</code></pre>
<p><a href="https://s.codepen.io/matuzo/pen/eaPLeB">CodePen: “100%” accessible - step 7</a></p>
<h3>🖕 Reader mode 🖕</h3>
<p>Testing the site in different browsers, I noticed that it’s still accessible in Safari in Reader Mode.</p>
<img alt="Safari Reader Mode" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/2aec49f866-1698841509/lighthouse_step6.png">
<p>As it turns out, it’s possible to disable Reader Mode by defining a small font size in the <code>body</code>.</p>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">body {
opacity: 0.03;
font-size: 1px;
}</code></pre>
<p><a href="https://s.codepen.io/matuzo/pen/QRZVJj">CodePen: “100%” accessible - step 8</a></p>
<h3>🖕 View Page Source 🖕</h3>
<p>The site is inaccessible to people with low and good vision, mouse, keyboard and screen reader users.\<br />
If browser power users encounter a site like this, it might awaken their inner <a href="<https://en.wikipedia.org/wiki/Hackers_(film)#Plot>">Zero Cool</a> and they try to hack the site. What I mean by <em>hack</em> is <em>view the page source</em>.</p>
<p>To put the cherry on top of my exclusion-first site, I’m <a href="https://v2.cryptii.com/text/htmlentities">converting the text to html entities</a>. <a href="https://developer.mozilla.org/en-US/docs/Glossary/Entity">Entities</a> are usually used to display reserved characters, invisible characters, and characters that are difficult to type with a standard keyboard. I’m using them to obfuscate text on our site.</p>
<img alt="Text is not readable when viewing the source because it's written in HTML entities" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/5163481693-1698841509/lighthouse_step7.png">
<p><a href="https://s.codepen.io/matuzo/pen/joeeqy">CodePen: “100%” accessible - step 9</a></p>
<p>To wrap it up, a final test.</p>
<div class="lighthouse-test js-lighthouse-test">
<button class="btn js-run-lighthouse-test" type="button"><span class="btn__inner">Run Lighthouse test</span></button>
<span class="u-vh js-lighthouse-status" role="status"></span>
<img alt="Score: 100" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/4056090ba4-1698841511/lighthouse_test.png">
</div>
<h2>Conclusion</h2>
<p>My intention with this post was not to diss Lighthouse or axe-core, the engine behind Lighthouse. I use both tools regularly and I’m glad I have them.\<br />
This post is about you and me. Scores indicate the quality of our apps and sites, but we must not trust these numbers thoughtlessly. We have to understand that automatic testing is just a first step.<br />
Next time you see a high Lighthouse score and you want to call it a day, read the text next to the score.</p>
<img alt="" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/4056090ba4-1698841511/lighthouse_test.png">
<blockquote>
<p>These checks highlight opportunities to <a href="https://developers.google.com/web/fundamentals/accessibility/">improve the accessibility of your web app</a>. Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged.</p>
</blockquote>
<p>Below that paragraph, you'll find a list of additional items you should test manually and a link to a page that explains <a href="https://developers.google.com/web/fundamentals/accessibility/how-to-review">How To Do an Accessibility Review</a>.</p>
<img alt="A list of recommendations like "Headings don't skip levels" or "The page has a logical tab order"" loading="lazy" src="https://www.matuzo.at/media/pages/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/5203b32988-1698841510/lighthouse_manual.png">
<p>We don’t test and optimize our sites for the good feeling a high score gives us. We’re doing it because we want to, and we have to, make sure that what we build is accessible to as many people as possible.<br />
We don’t fully rely on automation when we’re designing and developing, and we shouldn't do it either when we’re testing.</p>
<p>Thanks to <a href="https://ericwbailey.design/">Eric</a> for proofreading and feedback.</p>
<h2>Links and resources</h2>
<ul>
<li><a href="https://developers.google.com/web/tools/lighthouse/">Google Lighthouse</a></li>
<li><a href="https://github.com/dequelabs/axe-core">axe-core</a></li>
<li><a href="https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html">Inclusively Hidden</a></li>
<li><a href="https://webaim.org/projects/million/">The WebAIM Million</a></li>
<li><a href="https://developers.google.com/web/fundamentals/accessibility/?utm_source=lighthouse&utm_medium=devtools">Web Fundamentals – Accessibility </a></li>
<li><a href="https://ericwbailey.design/">Eric W. Bailey</a></li>
<li><a href="https://a11yproject.com">The A11y Project</a></li>
</ul>
<p>∗ (sarcasm)</p>
<h2>Update June 12th</h2>
<p>Added a paragraph about manual testing recommendations provided by lighthouse.</p>
<script>
if (location.search.includes('sfw')) {
console.log(location)
var headings = document.querySelectorAll('h3');
for (var i = 0; i < headings.length; i++) {
console.log(headings[i].textContent)
headings[i].textContent = headings[i].textContent.replaceAll('🖕', '')
}
document.querySelector('.sfw').style.display = 'none';
}
// Lighthouse demos
function addDemoLighthouseTest() {
var lighthouseTests = document.querySelectorAll('.js-lighthouse-test');
if (lighthouseTests.length === 0) {
return;
}
for (let i = 0; i < lighthouseTests.length; i++) {
const lighthouseTest = lighthouseTests[i];
const button = lighthouseTest.querySelector('.js-run-lighthouse-test');
button.addEventListener('click', function (e) {
e.preventDefault();
button.querySelector('span').textContent = 'Running tests…';
lighthouseTest.querySelector('.js-lighthouse-status').textContent =
'Running tests…';
setTimeout(function () {
lighthouseTest.classList.add('lighthouse-test--finished');
lighthouseTest.querySelector('.js-lighthouse-status').textContent =
'Tests finished. Accessibility score: 100.';
}, 1000);
});
}
}
addDemoLighthouseTest();
</script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CBuilding+the+most+inaccessible+site+possible+with+a+perfect+Lighthouse+score%E2%80%9D">blog@matuzo.at</a>.</p> Don't be afraid to share2019-08-20T00:00:00+00:00https://www.matuzo.at/blog/dont-be-afraid-to-share
<p>I don’t consider myself a web accessibility expert but I’ve learned enough in a relatively short time to feel comfortable enough to share my knowledge in blog posts, workshops and talks. </p>
<p>Here’s some advice, if you want to share stuff but are wary about doing it.</p><h2>1. Find something that interests you and stick to it.</h2>
<p>Fullstack is bullshit and so is trying to be an expert in everything. Pick that one thing that interests you the most (design, js, css, a11y, ux,…) and try to be really good in that specific area.</p>
<p>(PS: it can change over time)</p>
<h2>2. Read, read a lot.</h2>
<p>Find experts in the area that interests you and subscribe to their blogs and follow them on social media. Read as many articles as you can. If you can afford it, buy books or videos. If not, ask the author if they’ve got promo codes for you (some do that).</p>
<h2>3. Share!</h2>
<p>I learn the most by explaining stuff to others. If you want to learn something, write an article about it or give a talk at a meetup. You can do that even if you’re not an “expert”. I did that with <a href="https://medium.com/@matuzo/writing-css-with-accessibility-in-mind-8514a0007939">this article</a> and it helped me and many others.</p>
<h2>4. Share even if it has been shared before.</h2>
<p>Don’t limit yourself to topics and content that hasn’t been shared before. New perspectives, ideas and approaches to old topics are often interesting and valuable.</p>
<h2>5. Don’t be afraid to write/talk about basics.</h2>
<p>It’s easy to feel intimidated by the seemingly endless knowledge of people you follow on twitter. For every person who knows more about something than you, there are many more that know less and would enjoy you sharing “basics”.</p>
<h2>6. Don’t be afraid to ask for feedback.</h2>
<p>Before I published my first few articles about accessibility, I asked people I look up to (<a href="https://twitter.com/AArdrian">@aardrian</a>, <a href="https://twitter.com/heydonworks">@heydonworks</a> and others) to give me feedback. I figured if they don’t think it’s shit, it probably isn’t. I still do this with every article I write (by the way, <a href="https://twitter.com/cariefisher">@cariefisher</a> expect a DM next week :)).</p>
<h2>7. Publish on your own website.</h2>
<p>It’s not just that the quality of third party platforms like Medium might decrease at some point, but you also feel much more comfortable expressing yourself and sharing stuff if it happens on your personal website. (+ you own the content.)</p>
<h2>8. Find a mentor.</h2>
<p>Okay, I don’t know where you would find one but if someone you respect offers you their support, take it. <a href="https://twitter.com/AaronGustafson">@AaronGustafson</a> was my mentor for a year and he helped me in so many aspects.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDon%27t+be+afraid+to+share%E2%80%9D">blog@matuzo.at</a>.</p> Please write and talk more about CSS2019-09-25T00:00:00+00:00https://www.matuzo.at/blog/please-write-and-talk-more-about-css
<p>I saw a lot of JavaScript today considering that I was at a CSS conference.</p><p>It absolutely makes sense because for many people writing CSS means writing JS but I've seen enough pro/con talks about CSS-in-JS.</p>
<p>I do enjoy talks that are more advanced and show how to optimize the CSS people write in JS but I'd really love to see a CSS only talk or read an article again that challenges the way I write CSS or approach layout and architecture.</p>
<p>I have a feeling that we should talk and write more about CSS, advanced CSS. Tutorials have their place but I'd love to read and hear more about concepts and approaches, layout and architecture. I know that @rachelandrew, @heydonworks and @hankchizl are doing a lot but I believe there’s more that can be discussed and shared. :)</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CPlease+write+and+talk+more+about+CSS%E2%80%9D">blog@matuzo.at</a>.</p> Beyond automatic accessibility testing: 6 things I check on every website I build2019-10-15T00:00:00+00:00https://www.matuzo.at/blog/beyond-automatic-accessibility-testing-6-things-i-check-on-every-website-i-build
<p>I just finished an accessibility audit for a client and I decided to share some quick checks I perform in every site I audit and build. It’s something that you can apply to your project right away, you don’t have to learn a tool or a software.</p><h2>Step 0: Automatic tests</h2>
<p>The first thing I do is run accessibility checks in <a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a> to figure out if anything obvious is wrong. Automatic accessibility tests are great but they only check a subset of what needs to be tested. <a href="https://www.matuzo.at/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/">If you have a score of 100 or 0 errors, you’re not done</a>. It just means you’ve laid the groundwork for manual testing.</p>
<h2>Step 1: Check image descriptions</h2>
<p>The first semi-manual test I perform is check if images have descriptions and if they’re described correctly. I’m using a browser extension called <a href="https://addons.mozilla.org/de/firefox/addon/web-developer/">Web Developer</a> for that. You can highlight images that have no <code>alt</code> attribute or display the value of <code>alt</code> attributes next to images.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1571155703/articles/a11y-test/a11y_tips_step1.jpg" alt="The images settings section in Web Developer toolbar." /></p>
<h2>Step 2: Disable all styles</h2>
<p>Another feature of the Web Developer extension is the ability to disable CSS on a page. If you disable CSS, you’re able to check various things.</p>
<ul>
<li>Does the website work without CSS (in case it doesn’t load)?</li>
<li>Does the order of elements of your page make sense?</li>
<li>Are images and icons sized correctly?</li>
<li>Is your document well structured?</li>
</ul>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1571155700/articles/a11y-test/a11y_tips_step2.jpg" alt="The schedule page on webclerks.at displayed without CSS." /></p>
<h2>Step 3: Validate HTML</h2>
<p>You can use the <a href="http://validator.w3.org/">W3C Markup Validation Service</a> to check your markup. The Validation Service doesn’t catch everything, but it’s great for finding obvious bugs in your HTML, like duplicate <code>id</code>s or broken aria references.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1571155701/articles/a11y-test/a11y_tips_step3.jpg" alt="Random errors on the w3c validator results page." /></p>
<h2>Step 4: Check the document outline</h2>
<p><a href="https://webaim.org/projects/screenreadersurvey8/#finding">A sound document outline is important</a>. It should start with a <code>h1</code> followed by <code>h2</code>, <code>h3</code>, etc. in hierarchical order. This is great for search engines and screen reader users because they might navigate your site by jumping from one heading to another.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1571155700/articles/a11y-test/a11y_tips_step4.jpg" alt="List of the headings on webclerks.at. tota11y screenshot." /></p>
<p>You can use the <a href="https://validator.w3.org/">W3C Markup Validation Service</a> to check your outline or a tool called <a href="https://khan.github.io/tota11y/">tota11y</a> (as seen in the screenshot above).</p>
<h2>Step 5: Grayscale mode</h2>
<p>I’m using a browser extension called <a href="https://chrome.google.com/webstore/detail/high-contrast/djcfdncoelnlbldjfhinnjlhdjlikmph">High Contrast</a> to display sites in grayscale mode.\<br />
This is an important check because it will tell you if parts of your design work with color only. You should make sure you don’t convey information with color alone. A perfect example are links, they should have underlines to easily differentiate them from normal text. <a href="https://adrianroselli.com/2019/01/underlines-are-beautiful.html">Underlines are beautiful</a>!</p>
<blockquote>
<p>Underline your fucking links you sociopaths.</p>
</blockquote>
<p>- Heydon Pickering</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1571204684/articles/a11y-test/a11y_tips_step5.png" alt="The Smashing Magazine website works in black and white." /></p>
<p>You don’t have to install a browser extension, you can also use <a href="https://github.com/matuzo/a11y-tests.css">CSS to achieve a similar effect</a>.</p>
<pre><code class="language-css">body {
filter: grayscale(100%);
}</code></pre>
<h2>Step 6: Use the keyboard</h2>
<p>Put your mouse away and tab through the page and see if you can use every single part of the site without a mouse/touch pad. The <kbd>tab</kbd> key is a powerful testing tool, it will tell you a lot about your site:</p>
<ul>
<li>Are focus styles clearly visible?</li>
<li>Is everything focusable that should be?</li>
<li>Are your buttons real <code><button></code>s?</li>
<li>Is the overall UX using a keyboard great?</li>
<li>Are you managing focus correctly?</li>
<li>Do you hide and show elements correctly?</li>
<li>Does visual order match DOM order?</li>
<li>Can you use custom JS components without a mouse?</li>
</ul>
<p>There’s even a <a href="https://www.npmjs.com/package/no-mouse-days">npm package for disabling the mouse cursor</a>, built by the wonderful <a href="https://twitter.com/marcysutton">Marcy Sutton</a>.</p>
<p>That’s not the end of the story. There are more things that you should check, but that’s it for now. I’ll share more in another post. :)</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CBeyond+automatic+accessibility+testing%3A+6+things+I+check+on+every+website+I+build%E2%80%9D">blog@matuzo.at</a>.</p> matuzo.at from scratch #0 - introduction2020-01-09T00:00:00+00:00https://www.matuzo.at/blog/matuzoat-from-scratch-video-0
<p>I'm redesigning and building my website from scratch. In this first video I introduce myself and I describe what my plans are for the following weeks and months. Watch it to see if this series of videos is for you or not.</p><nav>
<h2>In this page</h2>
<ol>
<li><a href="#video">Video</a></li>
<li><a href="#transcript">Transcript</a></li>
</ol>
</nav>
<h2 id="video">Video</h2>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/cijF86B5UYI" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="matuzo.at from scratch - #0 introduction"></iframe>
</div></div>
<h2 id="transcript">Transcript</h2>
<h3>[Introduction]</h3>
<p>Hello and welcome to this first video in a series of videos where I'm going to redesign and build my website from scratch.</p>
<p>My name is Manuel Matuzović, I'm a front-end developer from Vienna and I like sharing stuff. I usually do it on my website <a href="https://matuzo.at">matuzo.at</a>. <em>Matuzo</em> is my nickname and <em>at</em> is the top-level domain for Austria. I write articles about CSS and accessibility and sometimes I also publish on other websites where write about the same topics.</p>
<p><em>[Showing articles I've published on <a href="https://www.smashingmagazine.com/2017/07/enhancing-css-layout-floats-flexbox-grid/">Smashing Magazine</a>, <a href="https://css-tricks.com/flex-grow-is-weird/">CSS-Tricks</a> and <a href="https://alistapart.com/article/my-accessibility-journey-what-ive-learned-so-far/">A List Apart</a></em></p>
<p>I also like to give talks at meetups and conferences, and now I decided to share on another medium, which is video.</p>
<p>I already have a website and I want to redesign it for different reasons.</p>
<p>The first reason is that I made some bad decisions when it comes to using custom properties and there are also some other CSS things that I want to improve and I also want to extend what's on my website.<br />
Right now you can see on my home page the latest blog posts and you can click a blog post. It's a pretty basic layout. It looks nice, at least if you ask me.<br />
Most articles look the same but some are different, some of them are branded, for example here you can see a a post for a <a href="https://www.matuzo.at/blog/12-tips-for-more-accessible-react-apps-slides-react-finland-2019/">talk that I gave in Finland</a>, at <a href="https://react-finland.fi">React Finland</a>. I branded it in the colours of Finland and the event, blue and white.<br />
Here you can see a post called <a href="https://www.matuzo.at/blog/the-dark-side-of-the-grid-part-2/">"The Dark Side of the Grid"</a>. I stole the design of the "Dark Side of the Moon" which is an LP by Pink Floyd and I branded the whole article in this design. I like that, it's a bit more work but it looks nice and I also like that not all articles look the same.</p>
<p>Besides my blog there is a page <a href="https://www.matuzo.at/about-me/">about me</a>, it's pretty boring, there isn’t too much on it, I want to extend that a bit I want to go into detail about my workshops and my speaking and so on.</p>
<p>There’s a <a href="https://www.matuzo.at/til/">today I learned page</a>, it's called today I learned because I share a thing that I've learned that day in a<br />
sentence or two. I stole that idea from reddit, there is a subreddit called <a href="https://www.reddit.com/r/todayilearned/">"today I learned"</a> where people do exactly the same thing but I put it on my<br />
website and I share web stuff, for example the last thing is "you can list all pseudo elements of an element in the CSS pane in Firefox developer tools". I didn’t know that before that.</p>
<p>Okay, so that's what's online right now and I want some more stuff. So, this is one of the reasons why I need a redesign and I also want to make a new design.<br />
I like the current design. What I like most about it is that it's not a typical design, it doesn't look like every other website and I also like that a lot of people don't like it. I even had people tell me in my face that my website is ugly - Hi Jens! - but that's a good thing because I'd rather have a lot of people not like my website and others like it then a lot of people not care about my website because not caring means it's boring.</p>
<p>Anyways, I want to make a new design and, yeah, this is what's going to happen in the course of the next few weeks and months and I'm going to record it.</p>
<h3>[List of topics I'll cover]</h3>
<p>I made a list of the topics that I'm going to cover and some of the technology that I want to use or some of the properties, techniques, whatever and I summarized it for you so that you can decide for yourself if this video, series of videos is for you or not.</p>
<ol>
<li>
<p>I'm going to host the website on <strong><a href="https://netlify.com">netlify</a></strong>, just because it's for free and it's easy and it makes deployment really<br />
easy. I'm a big fan of netlify, they didn’t pay me to say that I'm just very convinced of the product.</p>
</li>
<li>
<p>I'm going to use <strong><a href="https://netlifycms.org">netlifyCMS</a></strong>, which is a plugin for netlify hosted websites that will magically create a content management system for your sites. You don't have to install anything, you don't need a server-side programming language or whatever. All you need is just one HTML file and I think a config JSON or YAML file and netlifyCMS will create a user interface and the content management system based on that, which is really cool.</p>
</li>
<li>
<p>We're also going to need <strong>netlify forms</strong> because I want to<br />
bring guest books back to the web. I made my first website about 19 years ago and every website I made until – I don't know – 2004 or 5 had a guestbook, so I want to bring that back, I don't know if bots and spammers and trolls are going to ruin that for me, but we'll try and netlify has forms, as well.</p>
</li>
<li>
<p>I won’t use any programming language like PHP or Python or Ruby to create my website, I'm going to use good old HTML but I'm not going to write HTML files.<br />
I'll write markdown files and use <strong><a href="https://www.11ty.dev/">eleventy</a></strong>, a static site generator to create HTML files out of markdown and nunchuks files. I'm a big fan of eleventy, pretty much everything I build at the moment is based on eleventy. It's a fantastic tool, you're going to love it.<br />
Shout out to <a href="https://twitter.com/zachleat/">Zach Leatherman</a> who built it, it's just perfect.</p>
</li>
<li>
<p>I'll talk about <strong>designing in the browser</strong> because I made a design for my web site, for my current web site and for the new one and I didn’t use Photoshop or Sketch or anything I designed it completely in the browser and I'll talk a little bit about that in the next video and how to find inspiration or at least how I find inspiration for designs as someone who's not a designer per definition.</p>
</li>
<li>
<p>I'm going to use <strong>Sass</strong> for stuff like includes and mixins and variables.</p>
</li>
<li>
<p>We'll also cover <strong>gulp</strong> because I need gulp for minifying and uglyfiying CSS and JavaScript files, maybe some image optimization, we'll see. I don't know yet what we were going to use it for but I know that I'm going to need it.</p>
</li>
<li>
<p>I'm also going to write some <strong>JavaScript</strong>. I'm a HTML and CSS person. I understand JavaScript but I'm not the best JavaScript developer, so don't expect the best JavaScript code but there is going to be some JavaScript because I like to write JavaScript myself, so I don't like to rely too much on frameworks or plugins, for example I'm going to need lazy loading on my website and I'm going to write it on my own, just to practice JavaScript a little bit and to save some file size.</p>
</li>
<li>
<p>Speaking of file size, <strong>performance</strong> is going to be an important topic. I like my websites to be fast so that users have a great experience. It's a usability and user experience concern and an accessibility concern, as well. So performance is important, we're going to talk a lot about performance and I will try to make it as fast as possible.</p>
</li>
<li>
<p>Another important topic is <strong>accessibility</strong>. I'm an accessibility person, I care a lot about accessibility and my users and I want to make sure that as many users as possible can use my website.</p>
</li>
<li>
<p>If I talk about accessibility of course you have to talk about <strong>progressive enhancement</strong>, as well, so I want to make sure that the website is accessible not just to as many users as possible but also to as many devices and operating systems and browsers as possible. For example, I bought this Nokia 3310 about two years ago which is a redesign of the old Nokia phone. It's a great phone, it's pretty cheap and it has a browser, Opera Mini. If you visit <a href="https://caniuse.com">caniuse.com</a> and you search for a property and you see all green in every browser except for one, that's usually the browser that's installed on that phone. So, this is an interesting challenge, we're going to make this website work on my feature phone here.</p>
</li>
<li>
<p>I already mentioned that some of the articles are themed, we have this "Dark Side of the Grid" article and the "React Finland" article. I'm going to talk about <strong>theming</strong> a lot and I'm going to try to find a new strategy on how I'm going to theme my sites.</p>
</li>
<li>
<p>There’s some CSS stuff that I want to implement like <strong>dark mode</strong>. I'm going to use <strong>CSS Grid</strong>, if it makes sense and I want to use <strong>custom properties</strong>, ideally in a smart way, we'll see how we're going to use them, if we're going to use them.</p>
</li>
<li>
<p><strong>Dev tools</strong> is another important topic, I like to use dev tools a lot and use as many features as possible of dev tools and I'm going to talk a lot about that and see if I can find something that you don't know already about dev tools.</p>
</li>
<li>
<p>We might talk about <strong>puppeteer</strong> as well because for the <em>today I learned</em> section I want to have the ability to add code and when I publish it I want to run a script that will take a screenshot of this code sample that I wrote and use it as the preview image on social media and I guess that puppeteer can do that for me.</p>
</li>
<li>
<p>Another thing is that I want to make the website available offline, so it's going to be a <strong>progressive web app</strong>.</p>
</li>
<li>
<p>There’s some other JavaScript stuff and indie web stuff like <strong>web mention</strong> that I want to implement.</p>
</li>
<li>
<p>I want to show you how I test for accessibility and performance and how I do <strong>browser testing</strong> to make sure that my website works the way I expect it to work.</p>
</li>
</ol>
<p>That's a lot, I don't know how many videos I'm going to record, I don't know how often I'm going to publish a video, if it's going to be every two weeks or three weeks. I just don't know how much work it is to create a video, we'll see.</p>
<p>That's it for the first video. What I need now is your feedback, please tell me if this is interesting, if I caught your attention, if you are waiting for the next video to publish and also please tell me if the audio quality is<br />
good enough, if the video quality is good enough. I'm actually just using my smartphone to record this and also please tell me if<br />
you can understand me, I know that my English isn’t perfect but I just hope that my accent is understandable at least understandable enough. Yeah, that's it, thank you so much for watching.<br />
See you next time ❤️</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9Cmatuzo.at+from+scratch+%230+-+introduction%E2%80%9D">blog@matuzo.at</a>.</p> Bad accessibility equals bad quality2020-01-15T00:00:00+00:00https://www.matuzo.at/blog/bad-accessibility-equals-bad-quality
<p>When I talk about web accessibility at meet-ups and conferences, it’s safe to assume that at least one person will ask me something like “Yeah, accessibility sounds nice, but how many people are actually disabled? How many of my users are blind? And why would a blind person visit my website?”</p><h2>1. Who gives a fuck?!</h2>
<p>We’re not building websites for ourselves or our stakeholders, we’re building them for our users, for actual human beings. It’s our job to enable as many people as possible to use what we build. Everything else makes little sense. There’s no point in building something that can’t be used. Stop making assumptions about your audience and start building inclusive products.</p>
<h2>2. It's not just about disabilities</h2>
<p>Web accessibility is not just about keyboard users, color contrast or screen readers. Accessibility is a perfect indicator for the quality of a website. Accessibility is strongly interlocked with other areas of web design and web development. If your website is accessible, it usually means that it’s inclusive, resilient, usable, it offers great <abbr title="user experience">UX</abbr> for everyone, and it’s fast.</p>
<p>Here’s a simplified example and the reason for this rant:</p>
<pre><code class="language-html"><li>
<div tabindex="0" class="link">
<img src="preview.jpg" />
</div>
</li></code></pre>
<p>Yesterday I was looking at a list with 70 preview images that link to detail pages. I wanted to open a few of them in new tabs. Easy, right?<br />
No, because the developers decided to wrap these images in <code>div</code>s and link them with Javascript instead of using an anchor. I could left-click and open a single prototype in the same window, but I couldn’t right-click and open in a new window, because there are different options in the context menu when you right-click a <code>div</code> or an anchor.</p>
<h3>Semantic HTML is beneficial for everyone.</h3>
<p>Using <code>a</code> instead of <code>div</code> gives links a semantic meaning, users more options when using a mouse and it makes links keyboard focusable. Although – and this is the paradox – the list was keyboard accessible, because they made their <code>div</code>-links focusable with JavaScript instead of taking advantage of the browsers native behaviour.</p>
<p>This is just a minor problem and others have to deal with much bigger barriers every day, but this example is characteristic for how carelessly we sometimes treat our users.</p>
<p>Bad accessibility affects all of us at some point, please (re)learn HTML and take advantage of its fantastic features.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CBad+accessibility+equals+bad+quality%E2%80%9D">blog@matuzo.at</a>.</p> matuzo.at from scratch #1 - Designing and finding inspiration2020-01-17T00:00:00+00:00https://www.matuzo.at/blog/matuzoat-from-scratch-video-1
<p>The this video I explain how I approach design and how I find inspiration, <em>both</em> online and offline. <a href="https://www.youtube.com/channel/UCj29HII0sgJvKhqPIrO6Skw">Subscribe to the YouTube channel</a>.</p><nav>
<h2>In this page</h2>
<ol>
<li><a href="#video">Video</a></li>
<li>
<a href="#transcript">Transcript</a>
<ol>
<li><a href="#intro">Intro</a></li>
<li><a href="#skills">Required skills you'll need</a></li>
<li><a href="#design">How I approach design</a></li>
<li><a href="#inspiration">Finding inspiration</a></li>
<li><a href="#outro">Outro</a></li>
</ol>
</li>
</ol>
</nav>
<h2 id="video">Video</h2>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/AvSdsVYn0sk" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="matuzo.at from scratch - #0 introduction"></iframe>
</div></div>
<h2 id="transcript">Transcript</h2>
<h3 id="intro">[Introduction]</h3>
<p>Hi!<br />
My name is Manuel Matuzovic. I'm a front-end developer from Vienna and I'm redesigning and building my website from scratch, and I'm recording it. This is my second video, thank you so much for the great feedback to my first video and also thank you so much for subscribing to my <a href="https://www.youtube.com/channel/UCj29HII0sgJvKhqPIrO6Skw">YouTube channel</a>. I started with 5 subscribers and now I have 102, which is really really amazing.</p>
<p>In my first video I talked about my plans for the next couple of weeks, the technology I'm going to use and what I'm going to cover and what I want to implement. One thing I didn’t tell you about – and thank you Bernd for pointing this out – is the required skill level that you will need in order to be able to follow me.</p>
<h3 id="skills">[Required skills you'll need]</h3>
<p>If you are completely new to <strong>eleventy</strong>, it's absolutely fine. I'm doing a lot of research right now, I'm looking at other websites and I'm trying to steal all the great ideas so they can find their way into my website, and I'm going to start from zero and explain everything that I'm doing.<br />
So, if you're new to eleventy, this is going to be great for you.</p>
<p>If you're completely new to <strong>HTML and CSS</strong>, if you don't know how to write HTML and CSS, I believe that it's going to be hard for you to follow me because I'm not going to talk about the language basics. I'm going to talk about some basics of HTML, of course, and CSS, as well, but also about some advanced stuff and I'm not going to explain the fundamentals.<br />
You might want to learn HTML and CSS first and then come back because there is some stuff like performance and theming in CSS and also advanced layout that I'm not going to explain in detail.</p>
<p>All the <strong>JavaScript</strong> stuff is probably going to be pretty basic because I already told you in the first video I'm not a JavaScript pro. I know how to write JavaScript but it's not going to be the most advanced JavaScript, or you don't need any framework knowledge (React, Vue, whatever) because I'm going to write vanilla JavaScript.</p>
<p>My JavaScript might get better because I just subscribed to <a href="https://justjavascript.com/">“Just JavaScript”</a> by <a href="https://twitter.com/dan_abramov">Dan Abramov</a>, a JavaScript course that helps you write vanilla JavaScript. It seems like he's planning to create the best vanilla JavaScript course there is, together with <a href="https://twitter.com/Mappletons">Maggie Appleton</a>. I'm really looking forward to this, you might want to subscribe.</p>
<p>So, if you know basic HTML and basic CSS, I guess my videos are going to be interesting for you.</p>
<h3 id="design">[How I approach design]</h3>
<p>Let’s get started with the topic for today, I want to talk about design and finding inspiration. I'm a front-end<br />
developer, I get paid to write HTML, CSS and JavaScript. I don't do design but I believe that I have to understand how design works and I should be able to do some basic design because my job is to translate Sketch, Photoshop, sometimes even InDesign to CSS and HTML. In order to be able to<br />
translate I need to understand both languages, so the languages of the web and also the language of design.</p>
<p>I wouldn't trust myself to build a design system or to design a large website with many different components and states, and so on, but I trust myself to build my own website and I also challenge myself to design my own website and this is really important.<br />
Your website is your personal space, this is where you get to express yourself. You can express yourself by the content, the images, the text that you put online and you also get to express yourself and show who you are with your design. That's why I believe that it's important that <em>you</em> design your website, but for some people, including me, it's hard to design and the way I do it, and this kind of works for me, is that I have a starting point and that starting point is usually something that inspires me, and I'm going to talk about that in a second.</p>
<p>So, I have this starting point and then I just design, I just start and what I don't do is that I don't sit down for a day and try to come up with a design.<br />
I only take a few minutes, maybe 30 minutes or up to one hour maybe, because I work full-time I don't have the time and it's also for me easier to design and not get frustrated if I don't spend too much time on it because if I design something for two hours and I'm not able to come up with the result that I expect I get frustrated and if I only have a limited amount of time, this is not very likely to happen.</p>
<h4>[Spend only 30 - 90 minutes every day on design]</h4>
<p>So the way I do it is, in the morning I have about 30 to 90 minutes before I have to get to work, I wake up very early, yes, and I work on a design and I work on it for 30 minutes and then I just let it be and the next morning I look at it and if I still like it, I keep on working. If I don't like it, I throw it away and then in the next morning I repeat this, and so on. The tip I have for you: If design is hard for you, don't try to design everything in one go, like you can, I don't know, write the HTML and CSS and JavaScript for a component in a day and then you're finished and it's fine, but I wouldn't recommend to take a whole day to work on a design.</p>
<p>Take your time, spend multiple days looking at the design, iterating over it and tweaking it and maybe just throwing stuff away, if you don't like it anymore, it's fine just try something else but just take your time and while you're designing also try to work on other stuff on your website because there’s so much you have to do. Don't let the design be a blocker, you don't need the design before you start working.</p>
<h4>[Don't let the design block you]</h4>
<p>You can do all the server setup, you can, in my case, setup eleventy, you can create all the content, write everything, work on the HTML and the<br />
design could be the last thing that you have to do, so don't let a design be a blocker.</p>
<h3 id="inspiration">[Finding inspiration]</h3>
<p>I just mentioned that I usually have this one thing that I start with, like this foundation of my design, something that inspires me. And inspiration is an interesting topic because there are different ways or different sources where you can get inspiration from.</p>
<h4>[Don't look at other websites too early]</h4>
<p>I'm going to start with the most obvious one, which is looking at other websites. This is something that I definitely cannot recommend. Don't look at other websites because if you look at other websites, it might happen that they influence you in your decision-making and design process so much that there isn’t too much of your personality, of “you” in your design and, again, I believe this is important because you express yourself with the design of your website. So, looking at other websites too early in this process might influence you too much and the chances are high that your website will look like every other website.</p>
<p>There is this <a href="https://twitter.com/jongold/status/694591217523363840">famous tweet by John Gold</a> who wrote “Which one<br />
of the two possible websites are you currently designing” and you see this one design with the logo on the left side, a horizontal navigation on the right side followed by a hero area with an image and some tags, a call-to-action button followed by several sections with one to three columns. This is how most websites look like and if you look at too many websites of other people, it might happen that you just create another clone of all the other websites.</p>
<h4>[Fonts]</h4>
<p>So, if I don't recommend looking at other websites too early, what do I recommend?<br />
Well, you could start with pretty much anything, you can start with a font for example. You just visit, I don't know, <a href="https://fonts.google.com/">Google Fonts</a> and just browse through the fonts, maybe don't take the first ones, don't start with Open Sans or Roboto.<br />
Scroll through the page and see if there’s a font that you like and use this as a starting point or, of course, you can also visit – I don't know how to pronounce it – font squirrel, font squirrel, font squirrel, I don't know, <a href="https://www.fontsquirrel.com/">font squirrel</a>. You can browse this website and see if there are free fonts that you can use, that inspire you to create a design. They have a lot of interesting fonts on there.</p>
<h4>[Colors]</h4>
<p>So, a font could be something that inspires you and also a color, for example. There’s <a href="https://color.adobe.com/explore">Adobe color</a>, there are other websites as well, that have color swatches and you just browse through the site and see if there is a combination of colors that you like and use this as a starting point.</p>
<h4>[dribbble.com]</h4>
<p>Another source of inspiration is <a href="https://dribbble.com/">dribbble.com</a>, and I guess they're different ways of<br />
using dribbble.com. On <a href="https://dribbble.com/search/website">this page</a> I searched for “website” and you can see some designs and the problem I have with dribbble is that some of the designs are just too beautiful, just too perfect and if I look at this page, everything looks the same to me because, I don't know if I'm using dribble wrong, but there is usually a trend, a certain type of design or usage of color or typography that everyone is doing at the moment and this is why most designs look the same at least they look the same to me.<br />
I don't know if this is a great source for finding inspiration but what I like about dribbble is, you can search for logos or whatever and just see if you can find colors that interest you. So, it doesn't have to be a user interface design, it could be just a logo that you like and if you click on it, you can see the color swatch and you can copy the colors and you can also click one of the colors and then you will see all kinds of designs and combinations with other colors in that specific color. So, this is really cool.</p>
<h4>[Magazines]</h4>
<p>Another source of inspiration, it is my favorite one, is the offline world.<br />
You can find inspiration anywhere in the offline world, for example, you can look at books or magazines. This is an <a href="https://www.dan-davies.co.uk/print-to-css">article<br />
that Dan Davies wrote</a> and it's called “Print to CSS”. He says that he's a huge fan of magazine layouts and the use of typography and structures and the general look and he wanted to bring that look to the web. And for him it was an interesting exercise because he got to practice Grid and he also made some pretty cool designs.<br />
He looked at several magazines and he recreated the layouts in CSS. You can see for example, this is pretty interesting, he's using the heading of the article as a mask on an image, which looks nice and it seems like you can do this with CSS. So, pretty cool stuff, stuff that might inspire you.</p>
<h4>[Architecture]</h4>
<p><em>[Showing photos of buildings I took in Talinn]</em></p>
<p>Another source of inspiration in the offline world is architecture – I'm a huge fan of architecture – my partner's an architect and whenever we travel she shows me all kinds of interesting buildings. What you see here are just some photos I took in Estonia and they have so many awesome buildings there and, of course I know this is a building, you can’t immediately transform this into a web layout like we just saw with the magazines but – I don't know – texture, color, combination of colors and also sometimes shapes can be very inspiring. For example look at that building, I mean this looks so interesting, all kinds of interesting shapes that you could, for example, recreate with CSS or just use it as a foundation for building something or designing something.</p>
<p><em>[Showing a photo of a poster]</em></p>
<p>On my current website you can see that the colors I'm using are this whitish-beige color in the background and a light blue and a dark blue color and red. And I found those colors in Italy, I was in Italy on vacation and we stayed at an AirBnB and I was brushing my teeth and usually I'm brushing my teeth everywhere but the bathroom. So I was walking around the Airbnb and I was brushing my teeth and then I saw this poster – and you can actually see me brushing my teeth here, if you look closely – and I saw it and I really liked it. I like the shapes, but not too much, but I liked the colors a lot. So, I took a photo and I saved it to my phone. At some point I used this as a basis for my design. If you look at the website again, you can see that the colors are not exactly the same but they're pretty similar.</p>
<p><em>[Showing a punk LP]</em></p>
<p>For my new website, for my redesigned website I already have a very rough idea for a design and the inspiration this time was a LP. I bought this punk LP a few years ago, about eight years ago. It's called “Oi of Japan” which is a compilation of Japanese punk music and a few weeks ago I remembered that I have it and I listened to it and then – I swear it was exactly like that – two days later I woke up in the middle of the night and I thought “My website should look like this cover”. So, I looked at it again and thought “Okay, how can I do it, how can I transform this into a design”.</p>
<p>So, I opened <a href="https://codepen.io/">CodePen</a> and I started designing and this is what I came up with. You can see that the title of the LP is my name and the navigation on my website is where the bands are and I don't have an illustration here but Ibuilt a layout in a square shape or a rectangle shape and I did that and I liked it for some time and – you know – I spent 30 to 60 minutes working on it and then I worked one more day and then another day and then I woke up and I looked at it and I thought “I don't know, it looks nice, but I'm not too convinced”. I mean, I like this three column layout here, it's kind of interesting but I thought I should bring my colors back, the colors that I have on my website because I like them a lot.<br />
So, I changed the colors and I also changed the layout a bit and I did some experimenting and I liked that a first but a few days later I didn’t like it anymore, I didn’t like it at all so I just thought “Okay, I'm going to start from scratch. I'm going to delete everything.”, but in order to honor the original inspiration I had, this LP, I'm going to keep this square shape and I'm going to use it as a background image on my website.</p>
<p>I opened illustrator, I created this background image, I saved it and then I got this and I thought “Man, this looks really cool” because it looks like a college block where you can write on and draw something and scribble and it also kind of… it makes me want to draw more squares on this canvas. So, that's what I did, I drew another square, so I got the original idea back and then I added my logo and the navigation here on the top right. Again, you can see that I'm really trying not to copy all the other websites. That's why I did some experimentation with the positioning and then I also added some content. Now what you can see here is not the final design, this is just a rough draft, a prototype. I'm designing in the browser and I will be working in the browser, of course, so I don't need to design the whole thing. Design here is an ongoing process, so while I'm writing the CSS the design will just happen.<br />
Okay, so this is what I'm going to do, it will probably not look like this but it will be something in that direction, probably. It might also happen that at some point, in video 10, I don't know, I decide that I don't like it anymore and I'm going to design something else, we'll see.</p>
<h4>[Look at other websites for inspiration]</h4>
<p>Okay, in the very beginning I told you that I don't recommend looking at other websites. Actually I do recommend it but it shouldn't be the first thing you do.<br />
At some point it makes sense to look at other websites and that's also what I did.</p>
<p>I visited it this <a href="https://lynnandtonic.com/">website but Lynn Fisher</a>. Lynn Fisher is awesome. If you don't know her, follow her and check out her website. This is her site and if you make the screen smaller, you can see that on a small screen you just see – I guess it's her – her head and her name at the top and the larger the screen gets the more heads you can see. So, her head splits in the middle and then you see a skull and then all of a sudden a pizza and more different faces and in the end you you see her again, I guess, again holding it all together. This is so cool, unfortunately I'm not Lynn Fisher. I'm not as talented and creative as her and I can’t draw, I can barely write, but I still love looking at websites like hers because it inspires me to get started, to do something, to design something, to get creative, even though I can’t do stuff that she can do.</p>
<p>Another great example is – and I guess that most of you have already seen this website by <a href="https://bruno-simon.com/">Bruno Simon</a> and I don't even know how you do it, like there’s a car and you can use the arrow keys to drive around and you can explore his world, his website and this is just so cool. Like again I don't even know how…What is this? JavaScript and three.js? I don't know. It's just so so cool and, again, I'm not as talented as he is to create something like that but it inspires me.</p>
<p>Another example is this website that I stumbled upon recently by – I'm sorry if I didn’t pronounce it correctly – <a href="https://www.ilithya.rocks/">Ilithya</a> and I<br />
spend so much time playing around with that thing, that's really really cool and I also like the colors a lot. Now, I wouldn't try to recreate this and put it on my website because that's not me. I'd like to play around with it but it looks nice and also again it's inspirational and what I like is I looked at the other pages and there are all kinds of interesting things going on. What I like here is that you can see the content is right aligned. On the left side you have this pattern, this dotted pattern and on mobile or small screens they're on the top. At some point they disappear and then reappear again. This is a nice little touch, I like that a lot and it seems like you – oh okay – you have some theming going on, which is also quite nice.</p>
<p>If you like websites like these where a lot of animation is going on, I recommend checking out <a href="https://www.awwwards.com/">awwwards.com</a>. There are a lot of really really cool sites on there.</p>
<p>I looked at some other sites, as well. I was on <a href="https://adactio.com/">Jeremy Keith's website</a> and I also found some things there. I saw that he has thistheme picker where you can change the theme of the site. It reminds me a lot of a <a href="http://csszengarden.com/">CSS zen garden</a>, that's really cool. Since I'm going to have some theming on my site, as well, I might steal that. I like it a lot. So, there’s theming, which I'm going to steal from Jeremy and I also like that on his home page you can see he doesn't have just a list of the latest blog posts but pretty much everything. There’s a photo that he took, there’s an article that he wrote, I guess.<br />
There are some teasers to articles that he read and he liked and there’s all kinds of content that he created or that he put online, that's summarized on his home page. I'm going to do it as well because on my current website you can only see the the latest blog posts.</p>
<p>es, then I visited <a href="http://mina.codes/#%F0%9F%92%8B">Mina Markham's</a> website and I believe<br />
I didn’t even look at the other pages. I got stuck on the home page because I loved the photo that she put on there. I like it because it has a lot of character and it's not a typical photo that you take… a professional photography that you take where you sit like that and you have a photographer perfect camera that takes a photo of you. It's so dynamic and I really like that. I might even put a photo of me on my website I usually don't like taking photos of myself and putting selfies on Instagram or whatever but I believe it makes sense to put a photo of yourself on your personal website if you want people to know who you are.</p>
<p>I also visited <a href="https://daverupert.com/">Dave Rupert's site</a>. I like that there’s this illustration, of him I guess, but I also liked his about page where he made this horizontal scrolling column layout – I guess he got inspired by Trello – with all the work he's done in the last eleven, twelve years. That's really cool, I might steal that idea as well for my today I learned page, I don't know, we'll see.</p>
<p>And the last website that caught my attention recently is the website of my friend <a href="https://mxb.dev/blogroll/">Max Böck</a>. He recently published this blogroll. Usually a blogroll is just a list of names linked to blogs but what he did here is that he added not just a name of the person he links to but also an image and a short paragraph about that person. I like that, I'm going to steal that as well.</p>
<h3 id="outro">[Outro]</h3>
<p>So, it makes sense to look at other websites, not just because of design reasons but also because you might get some inspiration for how to present content and what kind of content and how to build the basic structure of your website not just design stuff.<br />
Alright, that's it for today I hope that this helps you just get started with design or to finish a design actually. I just wanted to share my process how I find inspiration how I design stuff.<br />
In the next video I'm going to talk about designing in the browser and this is going to be the first video where you will finally see some CSS. In the meantime please give me feedback again, share me links to your websites. I want to steal ideas from you and also please <a href="https://www.youtube.com/channel/UCj29HII0sgJvKhqPIrO6Skw">subscribe to the channel</a>, contact me on <a href="twitter.com/mmatuzo">Twitter</a>, if you have questions if you have feedback and thanks so much for watching. See you next time.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9Cmatuzo.at+from+scratch+%231+-+Designing+and+finding+inspiration%E2%80%9D">blog@matuzo.at</a>.</p> CSS pro tip for mac users: always show scroll bars in macOS.2020-01-20T00:00:00+00:00https://www.matuzo.at/blog/css-pro-tip-for-mac-users-always-show-scroll-bars-in-macos
<p>I built a quite complicated component in HTML and CSS last week and I was happy with the result. After testing in different browsers and operating systems, I realised that I had to rewrite the whole thing because I didn’t consider that by default scroll bars don’t take up space on macOS, but on Windows they do.<br />
I <a href="https://twitter.com/mmatuzo/status/1116724406930366466">tweeted about a similar issue</a> about a year ago, but it seems that I didn’t take my own advice, so here’s a reminder for you and me.</p><p>Change your macOS settings to always show scroll bars. This will help you spot overflow bugs, e.g. caused by <code>width: 100vw</code>, before anybody else does. :)</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1571155703/articles/scrolltip.png" alt="General settings screen in macOS with "scow scroll bars" set to "always"" /></p>
<p>Side note: Adding max-width: 100% often helps.</p>
<pre><code class="language-css">.header {
width: 100vw;
max-width: 100%;
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CCSS+pro+tip+for+mac+users%3A++always+show+scroll+bars+in+macOS.%E2%80%9D">blog@matuzo.at</a>.</p> Here’s what I didn’t know about list-style-type2020-01-28T00:00:00+00:00https://www.matuzo.at/blog/heres-what-i-didnt-know-about-list-style-type
<p>At the CSS-in-Vienna meet-up last week <a href="https://twitter.com/udobiasch">Ulrich</a> told me that starting with Chrome 79 it's possible to define a string value for the <code>list-style-type</code> property. I was surprised because I thought <code>::marker</code> was supposed to solve that. That's why I did some research, here’s what I learned.</p><h2>list-style-type accepts a string value</h2>
<p>In Chrome 79+, Firefox 39+, and Opera 66+ it's possible to define a string value as the bullet of an ordered or unordered list, which means that emojis work, as well.</p>
<pre><code class="language-css">ul {
list-style-type: '🐣';
}</code></pre>
<ul class="lst-emoji">
<li>Item 1</li>
<li>Item 2</li>
</ul>
<style>
.lst-emoji {
list-style-type: "🐣";
}
</style>
<p>The list item may also be described as an Unicode value.</p>
<pre><code class="language-css">ul {
list-style-type: '\1F44D';
}</code></pre>
<ul class="lst-unicode">
<li>Item 1</li>
<li>Item 2</li>
</ul>
<style>
.lst-unicode {
list-style-type: "\1F44D";
}
</style>
<h2>@counter-style is a thing</h2>
<p>Browsing the MDN page for <code>list-style-type</code> I discovered that there’s a <code>@counter-style</code> at-rule. It allows you to define custom counter styles. It's <code>list-style-type</code> with super powers.</p>
<p>Currently, only supported in Firefox, there are several interesting options, like a list of one or multiple <code>symbols</code>, <code>suffix</code>, <code>prefix</code> or <code>range</code>. I won’t describe them here, I suggest you read about <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@counter-style">counter styles on MDN</a> or have a look at the demos below (Firefox only).</p>
<p><strong>Drooling emoji and a suffix</strong></p>
<pre><code class="language-css">@counter-style drooling {
system: cyclic;
symbols: '\1F924';
suffix: '. ';
}
.counterstyle {
list-style: drooling;
}</code></pre>
<ul class="lst-drooling">
<li>Item 1</li>
<li>Item 2</li>
</ul>
<style>
@counter-style drooling {
system: cyclic;
symbols: "\1F924";
suffix: ". ";
}
.lst-drooling {
list-style: drooling;
}
</style>
<p><strong>3 different symbols with a prefix only applied to the 2nd, 3rd and 4th list item</strong></p>
<pre><code class="language-css">@counter-style custom {
system: cyclic;
symbols: '\1F924''\1F44D''\1F525';
prefix: '->';
range: 2 4;
}
.counterstyle2 {
list-style: custom;
}</code></pre>
<ul class="lst-custom">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
<style>
@counter-style custom {
system: cyclic;
symbols: "\1F924" "\1F44D" "\1F525";
prefix: "->";
range: 2 4;
}
.lst-custom {
list-style: custom;
}
</style>
<p><a href="https://codepen.io/matuzo/pen/XWJQWPa?editors=1100">Check it out on CodePen</a>.</p>
<h2>What about `::marker?</h2>
<p>On <a href="https://www.htmhell.dev/15-letter-by-letter/">HTMHell</a> I’m using the <code>::marker</code> CSS pseudo-element to select the marker box of list items, which by default contains a bullet or number, and replace is using the <code>content</code> attribute.</p>
<pre><code class="language-css">li::marker {
content: '🔥';
font-size: 2.6rem;
}</code></pre>
<ul class="lst-marker">
<li>Item 1</li>
<li>Item 2</li>
</ul>
<style>
.lst-marker li::marker{
content: "🔥";
font-size: 2.6rem;
}
</style>
<p>What’s great about <code>::marker</code> is that you can finally style bullets.</p>
<pre><code class="language-css">.li::marker {
color: #ff00ff;
font-size: 2em;
}</code></pre>
<ul class="lst-marker2">
<li>Item 1</li>
<li>Item 2</li>
</ul>
<style>
.lst-marker2 li::marker {
color: #FF00FF;
font-size: 2em;
}
</style>
<p>Only all <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts">font properties</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color">color</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-combine-upright">text-combine-upright</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/unicode-bidi">unicode-bidi</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/direction">direction</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/content">content</a> can be used with <code>::marker</code>.</p>
<p>While it’s possible to change the <code>content</code>, I’d say that the main purpose of <code>::marker</code> is styling, and <code>list-style-type</code> and <code>@counter-style</code> are responsible for the value of the bullet.</p>
<p>This post is part of a series called <a href="/blog/heres-what-i-didnt-know">“Here’s what I didn’t know about…”</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CHere%E2%80%99s+what+I+didn%E2%80%99t+know+about+list-style-type%E2%80%9D">blog@matuzo.at</a>.</p> Here’s what I didn’t know about…2020-01-28T00:00:00+00:00https://www.matuzo.at/blog/heres-what-i-didnt-know
<p>Recently, it feels like I see a property, a property value or a selector I haven’t heard about pop up every day. Often these things I learn aren’t even that new, which makes me wonder how much I don’t know about CSS.</p><p>In this series of articles I’m (re)visiting CSS properties and selectors, trying to find out what I didn’t know about them.</p>
<h2>Here’s what I didn’t about…</h2>
<ol>
<li><a href="/blog/heres-what-i-didnt-know-about-content/">content</a></li>
<li><a href="/blog/heres-what-i-didnt-know-about-color/">color</a></li>
<li><a href="/blog/heres-what-i-didnt-know-about-list-style-type/">list-style-type</a></li>
<li><a href="/blog/2022/heres-what-i-didnt-know-about-where/">:where</a></li>
</ol><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CHere%E2%80%99s+what+I+didn%E2%80%99t+know+about%E2%80%A6%E2%80%9D">blog@matuzo.at</a>.</p> Reading recommendations: Animation on the web and vestibular disorders2020-02-11T00:00:00+00:00https://www.matuzo.at/blog/reading-recommendations-animation-on-the-web-and-vestibular-disorders
<p>It’s 7:25 a.m. and I’ve already learned so much. Actually, I just wanted to write a few paragraphs for an article about accessibility in CSS before I go to work, but I got caught up reading about animation on the web and vestibular disorders.</p><p>The articles I read written by <a href="https://ericwbailey.design/">Eric W. Bailey</a> and <a href="https://twitter.com/ShellELittle">Shell Little</a>, <a href="https://valhead.com/">Val Head</a>, and <a href="https://twitter.com/fcorradini">Facundo Corradini</a> are so great that I just had to dedicate this post to them.</p>
<h2>Designing Safer Web Animation For Motion Sensitivity</h2>
<p>Val describes how we can make animated movements easier on folks who find it triggering.</p>
<p>Read <a href="https://alistapart.com/article/designing-safer-web-animation-for-motion-sensitivity/">Designing Safer Web Animation</a> on A List Apart.</p>
<h2>Revisiting prefers-reduced-motion, the reduced motion media query</h2>
<p>Eric highlights the importance of a thoughtful use of animation on the web and he raises awareness for the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion">prefers-reduced-motion media query</a>. Shell explains the relation between animation and neurodivergence.</p>
<p>Read <a href="https://css-tricks.com/revisiting-prefers-reduced-motion-the-reduced-motion-media-query/">Revisiting prefers-reduced-motion</a> on CSS-Tricks.</p>
<h2>Accessibility for Vestibular Disorders: How My Temporary Disability Changed My Perspective</h2>
<p>Facundo shares how his temporary disability changed his perspective on animation and web accessibility.</p>
<p>Read <a href="https://alistapart.com/article/accessibility-for-vestibular/">Accessibility for Vestibular Disorders</a> on A List Apart.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CReading+recommendations%3A+Animation+on+the+web+and+vestibular+disorders%E2%80%9D">blog@matuzo.at</a>.</p> One of my favourite accessibility testing tools: The Tab Key.2020-02-20T00:00:00+00:00https://www.matuzo.at/blog/testing-with-tab
<p>I’ve been employed for about a year now and many things are different compared to being a freelancer. One interesting thing in my specific situation is that I have to evaluate the accessibility of third-party tools regularly. Usually there’s no time for a full audit, I have to gain a good overview of the quality of a product as quickly as possible.</p><p><em>This article is available in German: <a href="/blog/de/testing-with-tab/">Eines meiner Lieblingswerkzeuge für Barrierefreiheit-Checks: Die Tabulatortaste</a>.</em></p>
<p>I’ve already shared <a href="/blog/beyond-automatic-accessibility-testing-6-things-i-check-on-every-website-i-build/">6 things I check on every website I build</a>, but this time I want to focus on one of the most powerful testing tools: The <kbd>Tab</kbd> key.</p>
<p>Let’s say, you’ve managed to score 100 on the Lighthouse accessibility audit. <a href="/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/">This doesn’t necessarily mean that your site is accessible</a>, you’ve just laid the groundwork for the actual testing. A next step could be putting your mouse away and using the keyboard only to navigate your site.</p>
<p>Here’s what pressing the <kbd>Tab</kbd> key will tell you about your website:</p>
<h2>1. Focus styles</h2>
<p>If you press the <kbd>Tab</kbd> key, do you see which item on the page is highlighted?<br />
No? Use <code>:focus{ }</code>, <code>:focus-within{ }</code>, or <code>:focus-visible{ }</code> to style elements in their focus state.</p>
<figure class="figure">
<span class="content__image-wrapper">
<img class="content__image" src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_800/v1582178732/articles/tabkey/tab_a11y_carie.png" alt="A focus linked with a background-color and a dotted outline.">
</span>
<figcaption>Beautiful focus styles on <a href="https://cariefisher.com/">Carie Fishers website</a>.</figcaption>
</figure>
<pre><code class="language-css">a:focus {
background-color: #b426ff;
outline: 5px solid #ea3bcb;
}</code></pre>
<h3>Learn more about focus styles</h3>
<ul>
<li><a href="https://css-tricks.com/focusing-on-focus-styles/">Focusing on Focus Styles</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus">:focus on MDN</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within">:focus-within on MDN</a></li>
<li><a href="https://github.com/WICG/focus-visible">focus visible polyfill</a></li>
</ul>
<h2>2. Interactive elements</h2>
<p>Can you reach interactive elements like links, buttons, form elements, or video controls?<br />
No? Work on your HTML. You’re probably using <code><div></code>, <code><span></code>, <code><svg></code> only, etc. where you should be using <code><input></code>, <code><button></code> or <code><a></code>.</p>
<p>Don’t use <code>div</code>s for buttons. This fake button is not accessible to keyboard and screen reader users.</p>
<pre><code class="language-html"><div class="btn" onclick="send()">Send</div></code></pre>
<p>Do this instead:</p>
<pre><code class="language-html"><button class="btn" onclick="send()">Send</button></code></pre>
<h3>Learn more about links and buttons</h3>
<ul>
<li><a href="https://www.htmhell.dev/3-image-buttons/">#3 image-buttons on HTMHell</a></li>
<li><a href="https://www.youtube.com/watch?v=8XjwDq9zG4I">The Links vs. Buttons Showdown </a></li>
</ul>
<h2>3. Real buttons</h2>
<p>You can reach a button, but nothing happens when you press <kbd>Enter</kbd> or <kbd>Space</kbd>? It’s probably still not a real <code><button></code> or <code><input type="button"></code>.</p>
<p>You can make fake buttons tabbable and you can change their semantics, but you only get key events by default with real buttons.</p>
<pre><code class="language-html"><div class="btn" tabindex="0" role="button" onclick="send()">Send</div></code></pre>
<p>Do this instead:</p>
<pre><code class="language-html"><button class="btn" onclick="send()">Send</button></code></pre>
<h3>Learn more about buttons</h3>
<ul>
<li><a href="https://www.youtube.com/watch?v=CZGqnp06DnI">Just use button -- A11ycasts #05</a></li>
<li><a href="https://www.buttoncheatsheet.com/">The Button Cheat Sheet</a></li>
</ul>
<h2>4. Skip links</h2>
<p>Do you have to tab through a lot of elements before you can reach a certain part of your UI? You probably want to implement skip links.</p>
<figure class="figure">
<span class="content__image-wrapper">
<img class="content__image" src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_800/v1582178732/articles/tabkey/tab_a11y_tatiana.png" alt="A skip link shows up in the top left corner when focused.">
</span>
<figcaption>Skip link on <a href="https://tatianamac.com/">Tatiana Mac’s website</a>.</figcaption>
</figure>
<h3>Learn more about skip links</h3>
<ul>
<li><a href="https://webaim.org/techniques/skipnav/">“Skip Navigation” Links</a></li>
</ul>
<h2>5. Focus management</h2>
<p>When you press a button and a modal/dialog pops up, can you access its contents immediately? No? You probably have to shift focus from the button to the modal.</p>
<pre><code class="language-js">function showModal() {
...
// Store the last focused element
lastFocusedElement = document.activeElement;
var modal = document.getElementById(modalID);
modal.focus();
...
}
function closeModal() {
...
// Return the focus to the last focused element
lastFocusedElement.focus();
...
}</code></pre>
<ul>
<li><a href="https://medium.com/@matuzo/writing-javascript-with-accessibility-in-mind-a1f6a5f467b9#7a0c">Writing JavaScript with accessibility in mind</a></li>
</ul>
<h2>6. Infinite scrolling</h2>
<p>Do you have a footer but you can’t access it by pressing <kbd>TAB</kbd> because you’ve implemented infinite scrolling? Burn it, burn it with fire!</p>
<p>No, seriously. Infinite scrolling is usually a bad practice.</p>
<h3>Learn more about infinite scrolling</h3>
<ul>
<li><a href="http://www.webaxe.org/infinite-scrolling-and-accessibility/">Infinite Scrolling and Accessibility (It’s Usually Bad)</a></li>
</ul>
<h2>7. Off-screen items</h2>
<p>Does the focus indicator suddenly disappear while you keep tabbing? It’s likely that you’re focusing off-screen items. You have to hide them correctly. <code>height: 0</code>, <code>transform: translateX(-100%)</code>, etc. don’t remove items from tab order, <code>display: none;</code> or <code>visibility:hidden</code> do.</p>
<div class="skip-link-container">
<a href="#codepen1-skip" class="skip-link skip-link--inline">Skip CodePen</a>
</div>
<p class="codepen" data-height="300" data-theme-id="6054" data-default-tab="result" data-user="matuzo" data-slug-hash="yxrRGz" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Inaccessible hiding">
<span>See the Pen <a href="https://codepen.io/matuzo/pen/yxrRGz">
Inaccessible hiding</a> by Manuel Matuzovic (<a href="https://codepen.io/matuzo">@matuzo</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<h2 id="codepen1-skip">8. DOM order</h2>
<p>Does the focus indicator skip around a lot? Most of the time it’s because visual order doesn’t match DOM order. Try to avoid reordering logical content and don’t use higher values than <code>0</code> as a value in the<code>tabindex</code> attribute.</p>
<h3>Learn more about source order</h3>
<ul>
<li><a href="https://adrianroselli.com/2015/09/source-order-matters.html">Source Order Matters</a></li>
</ul>
<h2>9. Custom JS components</h2>
<p>Are only parts of your JS components accessible with the keyboard? Read the <a href="https://www.w3.org/TR/wai-aria-practices-1.1/">WAI-ARIA Authoring Practices</a> to learn how to build common patterns correctly and make them accessible to everyone.</p>
<h2>The Tab key is awesome</h2>
<p>You don't need to learn a software to get started with accessibility testing, the <kbd>Tab</kbd> key will tell you a lot about the quality of your website. There’s more you have to check, but testing with the keyboard brings you one step closer to creating an inclusive website.</p>
<p>This post is based on a <a href="https://twitter.com/mmatuzo/status/1090932098456801281">twitter thread from last year</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9COne+of+my+favourite+accessibility+testing+tools%3A+The+Tab+Key.%E2%80%9D">blog@matuzo.at</a>.</p> Eines meiner Lieblingswerkzeuge für Barrierefreiheit-Checks: Die Tabulator-Taste.2020-02-20T00:00:00+00:00https://www.matuzo.at/blog/de/testing-with-tab
<p>Ich bin seit etwa einem Jahr angestellt und viele Dinge sind anders als bei meiner freiberuflichen Tätigkeit zuvor. Eine interessante Neuerung ist, dass ich regelmäßig die Zugänglichkeit von Tools Dritter bewerten muss. Dabei bleibt normalerweise keine Zeit für eine vollständige Prüfung, ich muss mir so schnell wie möglich einen guten Überblick über die Qualität eines Produkts verschaffen können.</p><p><em>Hinweis: Das ist eine Übersetzung des englischsprachigen Originals: “<a href="/blog/testing-with-tab">One of my favourite accessibility testing tools: The Tab Key.</a>”.</em></p>
<hr>
<p>Ich habe bereits <a href="/blog/beyond-automatic-accessibility-testing-6-things-i-check-on-every-website-i-build/">6 Dinge geteilt, die ich bei jeder von mir erstellten Website überprüfe</a>, aber jetzt möchte ich mich auf eines der effektivsten Prüfwerkzeuge konzentrieren: Die Tabulator-Taste.</p>
<p>Nehmen wir an, du hast es geschafft, beim Lighthouse Accessibility Audit 100 Punkte zu erreichen. <a href="/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/">Das bedeutet nicht unbedingt, dass deine Website barrierefrei ist</a>, du hast nur den Grundstein für die eigentlichen Tests gelegt. Ein nächster Schritt könnte darin bestehen, die Maus wegzulegen und nur noch die Tastatur zur Navigation auf deiner Website zu verwenden.</p>
<p>Das Verwenden der Tabulator-Taste verrät dir folgendes über deine Website:</p>
<h2>1. Focus Styles</h2>
<p>Wenn du <kbd>Tab</kbd> drückst, siehst du dann, welches Element auf der Seite hervorgehoben ist? Nein? Verwende <code>:focus{ }</code>, <code>:focus-within{ }</code> oder <code>:focus-visible{ }</code>, um Elemente in ihrem Fokuszustand zu stylen.</p>
<figure class="figure">
<span class="content__image-wrapper">
<img class="content__image" src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_800/v1582178732/articles/tabkey/tab_a11y_carie.png" alt="Ein fokussierter Link mit einer Hintergrundfarbe und einem gepunkteten Outline.">
</span>
<figcaption>Schöne und deutlich sichtbare Fokus-Styles auf <a href="https://cariefisher.com/">Carie Fishers Website</a>.</figcaption>
</figure>
<pre><code class="language-css">a:focus {
background-color: #b426ff;
outline: 5px solid #ea3bcb;
}</code></pre>
<h3>Erfahre mehr über Fokus-Styles</h3>
<ul>
<li><a href="https://css-tricks.com/focusing-on-focus-styles/">Focusing on Focus Styles</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus">:focus on MDN</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within">:focus-within on MDN</a></li>
<li><a href="https://github.com/WICG/focus-visible">focus visible polyfill</a></li>
</ul>
<h2>2. Interaktive Elemente</h2>
<p>Kannst du interaktive Elemente wie Links, Buttons, Formularelemente oder Video-Steuerelemente erreichen?<br />
Nein? Verbessere dein HTML. Du verwendest vermutlich nur <code><div></code>, <code><span></code>, <code><svg></code>, usw., wo du <code><input></code>, <code><button></code> oder <code><a></code> verwenden solltest.</p>
<p>Benutze keine <code>div</code>s für Buttons. Diese Fake-Buttons sind für Tastatur-, Switch Device und Screen Reader-Benutzer:innen nicht zugänglich.</p>
<pre><code class="language-html"><div class="btn" onclick="send()">Senden</div></code></pre>
<p>Viel besser:</p>
<pre><code class="language-html"><button class="btn" onclick="send()">Senden</button></code></pre>
<h3>Erfahre mehr über Links und Buttons</h3>
<ul>
<li><a href="https://www.htmhell.dev/3-image-buttons/">#3 image-buttons on HTMHell</a></li>
<li><a href="https://www.youtube.com/watch?v=8XjwDq9zG4I">The Links vs. Buttons Showdown </a></li>
</ul>
<h2>3. Echte Buttons</h2>
<p>Du kannst einen Button erreichen, aber es passiert nichts, wenn du <kbd>Enter</kbd> oder <kbd>Space</kbd> drückst? Es handelt sich vermutlich immer noch nicht um einen echten <code><button></code> oder <code><input type="button"></code>. Man kann Fake-Buttons mit der Tastatur zugänglich machen und ihre Semantik ändern, aber man bekommt die richtigen Key Events automatisiert nur mit echten <code><button></code>s.</p>
<pre><code class="language-html"><div class="btn" tabindex="0" role="button" onclick="send()">Senden</div></code></pre>
<p>Viel besser:</p>
<pre><code class="language-html"><button class="btn" onclick="send()">Senden</button></code></pre>
<h3>Erfahre mehr über Buttons</h3>
<ul>
<li><a href="https://www.youtube.com/watch?v=CZGqnp06DnI">Just use button -- A11ycasts #05</a></li>
<li><a href="https://www.buttoncheatsheet.com/">The Button Cheat Sheet</a></li>
</ul>
<h2>4. Skip-Links</h2>
<p>Musst du dich durch viele Elemente durchtabben, bevor du einen bestimmten Teil der Benutzeroberfläche, beispielsweise den Hauptinhalt, erreichen kannst? Dann solltst du Skip-Links ergänzen. Diese erlauben es Benutzer:innen direkt zu wichtigen Stellen der Seite zu springen.</p>
<figure class="figure">
<span class="content__image-wrapper">
<img class="content__image" src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_800/v1582178732/articles/tabkey/tab_a11y_tatiana.png" alt="A skip link shows up in the top left corner when focused.">
</span>
<figcaption>Der Skip-Link auf <a href="https://tatianamac.com/">Tatiana Macs website</a>.</figcaption>
</figure>
<h3>Erfahre mehr über Skip-Links</h3>
<ul>
<li><a href="https://webaim.org/techniques/skipnav/">“Skip Navigation” Links</a></li>
</ul>
<h2>5. Focus Management</h2>
<p>Wenn du eine Button drückst und ein Modal/Dialog erscheint, kannst du dann direkt auf dessen Inhalt zugreifen? Nein? Wahrscheinlich muss hier der Fokus via JavaScript vom Button auf das Modal versetzt werden.</p>
<pre><code class="language-js">function showModal() {
...
// Das zuletzt fokussierte Element speichern
lastFocusedElement = document.activeElement;
var modal = document.getElementById(modalID);
modal.focus();
...
}
function closeModal() {
...
// Fokus wieder auf das zuletzt fokussierte Element setzen.
lastFocusedElement.focus();
...
}</code></pre>
<ul>
<li><a href="https://medium.com/@matuzo/writing-javascript-with-accessibility-in-mind-a1f6a5f467b9#7a0c">Writing JavaScript with accessibility in mind</a></li>
</ul>
<h2>6. Infinite Scrolling</h2>
<p>Gibt es einen Footer, aber du kannst ihn nicht erreichen indem Du <kbd>TAB</kbd> drückst, weil auf dieser Seite Inifinite Scrolling implementiert wurde? Dann verbrenne es, verbrenne es mit Feuer!</p>
<p>Nein, im Ernst. Inifinite Scrolling ist normalerweise eine schlechte Praxis. Eine bessere Alternative könnte ein „Weitere Einträge laden“-Button sein.</p>
<h3>Erfahre mehr über Infinite Scrolling</h3>
<ul>
<li><a href="http://www.webaxe.org/infinite-scrolling-and-accessibility/">Infinite Scrolling and Accessibility (It’s Usually Bad)</a></li>
</ul>
<h2>7. Off-Screen Elemente</h2>
<p>Nicht sichtbare Elemente müssen richtig ausgeblendet werden. <code>height: 0</code>, <code>transform: translateX(-100%)</code>, etc. entfernen Elemente nicht aus der Tab-Reihenfolge, <code>display: none;</code> oder <code>visibility:hidden</code> schon.</p>
<div class="skip-link-container">
<a href="#codepen1-skip" class="skip-link skip-link--inline">Demo-CodePen überspringen</a>
</div>
<p class="codepen" data-height="300" data-theme-id="6054" data-default-tab="result" data-user="matuzo" data-slug-hash="yxrRGz" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Inaccessible hiding">
<span>See the Pen <a href="https://codepen.io/matuzo/pen/yxrRGz">
Inaccessible hiding</a> by Manuel Matuzovic (<a href="https://codepen.io/matuzo">@matuzo</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<h2 id="codepen1-skip">8. DOM Reihenfolge</h2>
<p>Springt der Fokusindikator wild auf der Seite umher, wenn du <code>Tab</code> drückst? Meistens liegt das daran, dass die visuelle Reihenfolge nicht mit der DOM-Reihenfolge übereinstimmt. Versuche die visuelle Umordnung von Inhalten zu vermeiden und verwende keine höheren Werte als <code>0</code> im <code>tabindex</code>-Attribut.</p>
<h3>Erfahre mehr über DOM Reihenfolge</h3>
<ul>
<li><a href="https://adrianroselli.com/2015/09/source-order-matters.html">Source Order Matters</a></li>
</ul>
<h2>9. Benutzerdefiniere JS-Komponeten</h2>
<p>Sind nur Teile deiner JS-Komponenten über die Tastatur zugänglich? Werfe einen Blick auf die <a href="https://www.w3.org/TR/wai-aria-practices-1.1/">WAI-ARIA Authoring Practices</a>, um zu erfahren, wie du gängige Pattern korrekt erstellst und für alle zugänglich machst.</p>
<h2>Die Tabulator-Taste ist ein fabelhaftes Werkzeug</h2>
<p>Du musst keine Software erlernen, um mit Zugänglichkeitstests zu beginnen. Die Tabulator-Taste wird dir viel über die Qualität deiner Website verraten. Es gibt noch mehr, was überprüft gehört, aber das Testen mit der Tastatur bringt dich der Erstellung einer inklusiven Website einen Schritt näher.</p>
<p>Dieser Beitrag basiert auf einem <a href="https://twitter.com/mmatuzo/status/1090932098456801281">Twitter-Thread</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CEines+meiner+Lieblingswerkzeuge+f%C3%BCr+Barrierefreiheit-Checks%3A+Die+Tabulator-Taste.%E2%80%9D">blog@matuzo.at</a>.</p> Why 543 KB keep me up at night2020-02-26T00:00:00+00:00https://www.matuzo.at/blog/why-543kb-keep-me-up-at-night
<p>The question how good <em>good enough</em> is and at which point a website is ready to go online is keeping me busy lately. The web is in bad shape and it’s because we’re making it too easy on ourselves. <em>“It’s online and works in most browsers”</em> is not enough - we have to be much more considerate of what we’re putting online.</p><p>Some background: About three years ago I specialized in web accessibility. Now it’s not just my job to make sure that the websites I build are accessible, but the websites of others, too. I’m a front-end developer, but also a consultant and auditor. I’m employed for about a year now as well, and I have to evaluate a lot more third party web products than I used to.</p>
<h2>Something has changed</h2>
<p>A friend recently sent me the link to a website and asked me for feedback, because I had experience with the content management system their client was using.<br />
I <a href="/blog/beyond-automatic-accessibility-testing-6-things-i-check-on-every-website-i-build/">checked a few things</a> and then browsed through the website with <a href="https://developer.mozilla.org/en-US/docs/Tools">Dev Tools</a> and the network panel open. The homepage had a page weight of <strong>4.1 MB (6.7 MB uncompressed)</strong>. I thought, “<em>Aight, that’s not great, but there are a bunch of images, so I guess it’s okay</em>”. Then I visited a page with a header, footer, sidebar navigation and a short paragraph <strong>(543 KB / 1.6 MB uncompressed)</strong> and I thought “<em>Nice, noticeably below 1 MB, that’s pretty good.</em>”.</p>
<p>And then it hit me.<br />
What the hell did just happen? <strong>543 KB</strong> on a simple text-only page is OK? Fuck no, it’s not.</p>
<p>How and when did I get to the point where I would consider a page weight of 4 MB on a large page and 500 KB on a small page normal? This got me thinking (and writing obviously). This is not an exception. The quality of most websites I audit and evaluate is bad. Somehow we've collectively decided that it’s okay to publish garbage.<br />
When I say we, I don’t just mean us developers. The reason could be a developer who doesn’t care enough, but it could also be a team lead, product owner or client who simply doesn’t give devs enough time to care about quality. Causes for that could be a lack of awareness, tough deadlines, short release cycles, or focus on development of new features instead of improvement of existing code.<br />
When I say garbage, I mean slow, inaccessible, annoying, stressful, or intrusive web experiences.</p>
<h3>Slow</h3>
<p>I need 1.6 MB of JavaScript (7.2 MB uncompressed) to display a table with my billing history.</p>
<figure class="figure figure--full">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1582717566/articles/543kb/adobe_j.jpg" rel="noopener">
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1300/v1582717566/articles/543kb/adobe_j.jpg" alt="Logged in on adobe.com. Screenshot.">
</a>
<figcaption>Billing history on adobe.com</figcaption>
</figure>
<h3>Inaccessible</h3>
<p>Many accessibility issues derive from bad markup. I could show a screenshot of any website here, but you’ll find some common mistakes on <a href="https://www.htmhell.dev/">HTMHell</a>.</p>
<figure class="figure figure--full">
<a href="https://www.htmhell.dev/10-section-is-no-replacement-for-div/" rel="noopener">
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1300/v1582719885/articles/543kb/htmhell.jpg" alt="htmhell.com. Screenshot.">
</a>
<figcaption>A submission on HTMHell showing many nested section and article elements.</figcaption>
</figure>
<h3>Annoying</h3>
<p>No, the first thing I want to do when I visit your website is not to sign up for a newsletter.</p>
<figure class="figure figure--full">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1582780558/articles/543kb/sitepoint.jpg" rel="noopener">
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1300/v1582780558/articles/543kb/sitepoint.jpg" alt="Overlay on the sitepoint.com hopmepage. Screenshot.">
</a>
<figcaption>Sitepoint trying to lure users into signing up for their newsletter by giving away a free book.</figcaption>
</figure>
<h3>Stressful</h3>
<p>Only 5 rooms left! Limited-time deal!! 2 other people looked for your dates in the last 10 minutes!!! I will never ever book anything on your website!!!!</p>
<figure class="figure">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1582780558/articles/543kb/booking.jpg" rel="noopener">
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_800/v1582780558/articles/543kb/booking.jpg" alt="Room selection screen on booking.com. Red and orange messages stressing people to book quickly.">
</a>
<figcaption>booking.com trying to stress me in 3 different places into booking a room.</figcaption>
</figure>
<h3>Intrusive</h3>
<p>ZDNet misuses the cookie consent dialog to trigger the push notifications prompt.</p>
<figure class="figure">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1582780558/articles/543kb/zdnet.jpg" rel="noopener">
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1300/v1582780558/articles/543kb/zdnet.jpg" alt="Cookie dialog shows, click Allow, notifications prompt shows. ">
</a>
<figcaption>A tweet by Šime Vidas explaining what he discovered on ZDNet.</figcaption>
</figure>
<p>I don’t know if it has always been like that or if the quality of what we publish online has gotten worse with <a href="https://css-tricks.com/innovation-cant-keep-the-web-fast/">advancing technological possibilities on the web</a> (faster networks, more powerful tools and new APIs). <strong>The web is in bad shape - we need to readjust our understanding of what quality means</strong> and what we can expect our users to put up with.</p>
<p>Now you might think, “<em>Dude, chill. 543 KB? That’s not bad, even on a 3G connection that site should load in a reasonable time.</em>”.</p>
<h2>Why 543 KB might be bad</h2>
<p>Yes, I know, it depends. 543 KB aren't always bad, but on that specific page there’s only a single image (the logo ~20 KB) and a single paragraph. So why then is the page still relatively large, where are the remaining 523 KB coming from?</p>
<p>Let’s break it up and see what we could take into account when we evaluate the total transferred bytes.</p>
<h3>Page weight</h3>
<p>Even in times of 4G or 5G, optimizing for fast download is important. People don’t always surf the web under the best conditions. Build your website for someone who visits it at Starbucks using Starbucks WIFI and not for someone who’s connected to their own fast internet at home.</p>
<p>The website we’re talking about now is an Austrian website, in German language, accessed mostly by Austrians via Austrian networks. We’re a country with affordable and fast data. Don’t get me wrong, the page weight is definitely too big, but I’d argue that it's not the biggest issue, especially if they’re caching efficiently and prioritizing asset loading correctly (spoiler alert: they’re not… ).</p>
<h3>DOM size</h3>
<p>Many and deeply nested elements result in a large DOM tree, which can affect runtime, memory and load performance negatively.</p>
<p>A large DOM tree in combination with complicated styles might have a bad effect on page rendering. In his talk <em>“</em><a href="https://www.technica11y.org/performance-and-the-accessibility-tree"><em>The Intersection of Performance and Accessibility</em></a><em>”</em> <a href="https://ericwbailey.design/">Eric W. Bailey</a> gives an example of a form based on Material Design that caused the screen reader Voice Over to crash because of its massive DOM size.</p>
<blockquote>
<p>To make a material design radio input, you need six HTML elements containing nine attributes with a DOM depth of three. You also need 66 CSS selectors containing 141 properties which weighs in at 10k when minified. You also need 2374 lines of JavaScript which weighs in at 30k when minified. All of this will get you a radio input.</p>
</blockquote>
<p>Another factor is JavaScript. Updating complex DOM structures can get expensive, changes at one level in the DOM tree can cause changes at every level of the tree, which leads to more time being spent <a href="https://gist.github.com/paulirish/5d52fb081b3570c81e3a">performing reflow</a>. DOM operations can become a performance bottleneck, but there’s more to consider, like storing references to a large number of nodes, which might overwhelm the memory capabilities of devices. Creating DOM nodes only when needed and destroying them when no longer needed helps with that.</p>
<p>If you don’t write your markup carefully and enable compression on the server, your HTML document might end up having multiple hundred kilobytes, although it should be somewhere in the lower tens.<br />
By writing carefully I mean only creating DOM nodes you need and adding extra divs and spans only when necessary. In React, <a href="https://reactjs.org/docs/fragments.html">Fragments</a> might help with that.</p>
<pre><code class="language-js">class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}</code></pre>
<p>A fragment just returns the content within the fragment without an extra wrapper <code>div</code>.</p>
<p><a href="https://developers.google.com/web/tools/lighthouse/audits/dom-size">Google recommends</a> less than 1500 nodes total, a maximum depth of 32 nodes, and no parent node with more than 60 child nodes.</p>
<h3>CSS</h3>
<p>There are 11 style sheets with a total weight of 97 KB (566 KB uncompressed). Only by looking at the numbers it’s hard to tell if there are a lot of unused rules in there, but 566 KB of minified CSS is definitely not something that can be ignored. I'm not saying that refactoring their CSS is a must, but I’d at least consider taking another look at what goes into the CSS bundle. Sometimes it’s not even our fault that bundle sizes are too big. Many content management systems or plug-ins come with huge JS and CSS files where only a fraction of the code is needed.</p>
<p>Another thing worth considering is splitting up the main CSS file by media feature, especially if the assets are served via HTTP2.</p>
<pre><code class="language-html"><!-- Downloaded and render blocking -->
<link rel="stylesheet" href="main.css" />
<!-- Downloaded but not render blocking if media query doesn’t match -->
<link rel="stylesheet" href="medium.css" media="(min-width: 768px)" />
<link rel="stylesheet" href="large.css" media="(min-width: 1024px)" />
<link rel="stylesheet" href="print.css" media="print" /></code></pre>
<p>Before HTTP2 this was kinda considered a bad practice, but with HTTP2 multiplexing, the number of requests that can be sent to the server at the same time is no longer limited. The big advantage of splitting CSS into multiple files is that browsers will download all of them, but only those needed to fulfill the current context will block rendering,</p>
<p>Read more about the topic in Harry Roberts fantastic article <a href="https://csswizardry.com/2018/11/css-and-network-performance/">CSS and Network Performance</a>.</p>
<h3>Fonts</h3>
<p>Font files can get pretty big, a <a href="https://www.zachleat.com/web/comprehensive-webfonts/">good font loading strategy</a> is essential. Fonts are sometimes a necessary evil, but one file is a 34 KB Font Awesome file. SVGs might be a better choice, because they're more flexible than icon fonts for animation and styling, and a SVG sprite with just the icons needed and not the whole set of icons might end up being much smaller.</p>
<p>It’s been years since I’ve used services like Font Awesome, SVGs have so much <a href="https://css-tricks.com/icon-fonts-vs-svg/">more advantages over icons fonts</a>.</p>
<h3>JavaScript and bundle sizes</h3>
<p>There are 22 JS files with a total file size of 320 KB (886 KB uncompressed). This is way too much considering that there’s only a navigation and a share button on this page.</p>
<p>I will not bash JavaScript, I love JS and I enjoy writing it. I’m not great at it though, and I’d argue that most of us aren’t. Lack of knowledge paired with a language that can easily fuck up performance is dangerous. That’s why I’m very cautious of what I’m doing and which 3rd party plugins I’m using.</p>
<figure class="figure figure--full">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1582780558/articles/543kb/addy.jpg" rel="noopener">
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1300/v1582780558/articles/543kb/addy.jpg" alt="Addy Osmani on the left on stage at #PerfMatters conf. A slide on the right “Stop taking fast networks, CPU & high RAM for granted”.">
</a>
</figure>
<p>I’ve watched Addy Osmani’s <a href="https://www.youtube.com/watch?v=X9eRLElSW1c&feature=emb_title">The Cost of JavaScript</a> the other day, which is also one reason I’m writing this article. I was blown away by so many things he said. Here’s a summary of my highlights:</p>
<ul>
<li>JavaScript is one of the most expensive parts of your site, reduce how much JS you ship.</li>
<li>Small JavaScript bundles improve download speeds, lower memory usage, and reduce CPU costs.</li>
<li>Avoid large bundles <strong>(50KB+)</strong>.</li>
<li>Post-Download, executing JavaScript is the dominant cost.</li>
<li>The differences between a low-end phone and high-end phone are huge.</li>
<li>Fast JavaScript means fast at download, parse and compile, and execute.</li>
<li>Avoid large inline scripts.</li>
<li>If possible, split your JS code.</li>
<li>Audit JS regularly.</li>
<li>Make performance part of the conversation.</li>
<li>Stop taking fast networks, fast CPU, and high RAM for granted.</li>
<li>Test on real phones and networks.</li>
</ul>
<p>Watch the talk or <a href="https://v8.dev/blog/cost-of-javascript-2019">read the article</a>, I promise you’ll love it.</p>
<p>If you know more about performance than I do (and you probably do), you might be shouting at the screen <em>“You didn’t consider this and that”</em>. Fair enough, but this post is not about how to optimize those 543 KB. It was my attempt to show you that we can’t expect whatever we produce and put online to be fine. We have to be much more considerate, we have to take a second and third look, and constantly challenge our decisions. Sometimes, even just slowing down can make a difference. As <a href="https://hankchizljaw.com/wrote/keeping-it-simple-with-css-that-scales/">Andy Bell recently discovered</a> while reviewing a larger codebase, stepping back and ignoring time pressure for a moment can help with seeing things clearly.</p>
<blockquote>
<p>We had two or three grid systems, some fluid type and some utility driven type that conflicted <strong>and</strong> a card component that was pretty much a website in itself. If I had slowed down and stepped back, I could have seen these problems, but I didn’t. So seriously, slow down and you will save so much time.</p>
</blockquote>
<p><a href="https://hankchizljaw.com/" rel="noopener">Andy Bell</a></p>
<p>By the way, the 543 KB page scored 11 points on the Lighthouse performance test.</p>
<h2>Why bother?</h2>
<p>Why is a website I'll probably never visit again bothering me so much?<br />
Because someone decided that it was good enough to go live. The problem is not this specific website or how fast it loads, but that <strong>shipping seems to be so much more important than performance, usability, accessibility, or user experience</strong>. Again, I know it’s not always up to us, because often we just have to publish something we don’t agree with, but the least we can do is to educate others why some decisions might be harmful, and try to improve things in whatever ways we can.</p>
<blockquote>
<p>It’s becoming increasingly clear that web performance isn’t solely an engineering problem, but a problem of people.</p>
</blockquote>
<p><a href="https://css-tricks.com/innovation-cant-keep-the-web-fast/">Jeremy Wagner</a></p>
<h2>Whats good enough?</h2>
<p>I started this article with the question “how good is <em>good enough</em>”. I can’t answer it for all of us, because it depends on our personal experience and capabilities. What I consider good enough might not live up to the expectations of others, but there are standards and best practices we can follow.</p>
<p>Here are a few things I (as a front-end dev) believe have to happen before a website can go online.</p>
<ul>
<li><a href="https://validator.w3.org/">Validate your HTML.</a></li>
<li>Test in many browsers. There’s a world beyond Firefox and Chrome. Check Opera, Firefox Mobile, Brave, Edge, or Samsung Internet, just to name a few. Safari can be a bitch, too.</li>
<li>Test on low budget phones, if you really want to see how well your website performs.</li>
<li><a href="https://developers.google.com/web/tools/lighthouse">Run Lighthouse</a> and try to score 100 in the accessibility, best practices, and SEO audits. Scoring 100 in performance is hard, but strive for a score above 90.</li>
<li>Check how your site performs on a slow connection by <a href="https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor/Throttling">throttling it in Dev Tools</a>.</li>
<li>Put <a href="https://www.youtube.com/watch?v=H4FzW9oFObs">your mouse aside</a> and use your website with the keyboard only.</li>
<li><a href="https://dequeuniversity.com/screenreaders/">Test your site with screen readers</a>.</li>
<li>Go through <a href="https://thedaviddias.dev/">David Dias</a>' fantastic <a href="https://frontendchecklist.io/">Frontend Checklist</a>.</li>
</ul>
<p>There’s more, but if we all did at least that, our users would be much happier. I know you might be thinking: <em>“We don’t have the budget for that.”</em>. But that’s the point: you should have it. <em>“It’s online and works in most browsers”</em> is not enough. We have to make performance, accessibility, usability, and user experience part of the conversation.</p>
<p>By the time you’ve reached this paragraph, you’ve downloaded 837 KB of data. Can that be improved? Most definitely. Is this website 100% accessible? Probably not. Did you get the best possible user experience? I guess no, I don’t know shit about UX.<br />
If my own website isn’t perfect, who am I to lecture you? Well it’s not about publishing perfect websites. That’s not possible for many different reasons, but it’s important that we’re conscious of what we’re shipping to our users. It’s important that we care about our users and about the quality of our products and that our standards become higher than they are at the moment.</p>
<blockquote>
<p>Good websites need time.<br><br />
Great websites need a lot of time.</p>
<p>For content, for graphics, for interaction, for organization, for findability, usability, accessibility and that dash of delight that makes it special.</p>
<p>Don’t build throw-away websites, build websites that last.</p>
</blockquote>
<p><a href="https://mobile.twitter.com/yatil/status/1230899701802770442">Eric Eggert</a></p>
<h1>Resources</h1>
<ul>
<li><a href="https://www.technica11y.org/performance-and-the-accessibility-tree">Performance and the Accessibility Tree</a></li>
<li><a href="https://developers.google.com/web/tools/lighthouse/audits/dom-size">Uses An Excessive DOM Size</a></li>
<li><a href="https://areknawo.com/dom-performance-case-study/">DOM performance case study</a></li>
<li><a href="https://developers.google.com/web/fundamentals/performance/rendering/">Rendering Performance</a></li>
<li><a href="https://css-tricks.com/innovation-cant-keep-the-web-fast/">Innovation Can’t Keep the Web Fast</a></li>
<li><a href="https://www.filamentgroup.com/lab/5g/">5G Will Definitely Make the Web Slower, Maybe</a></li>
<li><a href="https://hankchizljaw.com/wrote/keeping-it-simple-with-css-that-scales/">Keeping it simple with CSS that scales</a></li>
</ul>
<p>Big thanks to <a href="https://mxb.at/">Max</a> and <a href="http://maddesigns.de/">Sven</a> for helping me with this article. ❤️</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWhy+543+KB+keep+me+up+at+night%E2%80%9D">blog@matuzo.at</a>.</p> How many browsers do you know?2020-03-12T00:00:00+00:00https://www.matuzo.at/blog/how-many-browsers-do-you-know
<p>While testing a new feature recently, I realised that I don’t know too many browsers. I can list some, but I don‘t really know them like I know Firefox or Chrome. I want to change that, and I invite you to do the same.</p><p><a href="https://developer.mozilla.org/en-US/docs/Tools">Firefox Developer Tools</a> have improved so much in the last few years that I naturally switched from Chrome as my main development browser to Firefox. This helped me understand their differences, not just when working (developer experience) but also when surfing (user experience). <br />
Recently we’ve finished building a new feature, and I wanted to test it on my phone. I went through my apps to see which browsers I have installed. I needed quite some time to find them all because <s>I’m not well organised</s> I use them only for testing and then I forget about them.</p>
<h2>How many browsers do you know by heart?</h2>
<p>Okay, try to list as many browsers as possible. I’ll give you a few seconds.</p>
<p>…</p>
<p>Here’s my list: Chrome, Chrome for Android, Firefox, Firefox Mobile, Opera, Opera Mobile, Opera Mini, Vivaldi, IE, Edge, Brave, UC, Firefox Focus, Safari, Samsung Internet, Firefox DE, Safari iOS.</p>
<p>Did you know that there’s a <a href="https://play.google.com/store/apps/details?id=com.duckduckgo.mobile.android">DuckDuckGo Browser</a>, a browser called <a href="https://play.google.com/store/apps/details?id=com.mi.globalbrowser.mini&hl=de_AT">Mint</a> with over 5 Million downloads in the Android Play Store or a browser called <a href="https://play.google.com/store/apps/details?id=mobi.mgeek.TunnyBrowser&hl=de_AT">Dolphin</a> with over 50 Million downloads?</p>
<p>I didn’t. There are even more browser with millions of downloads.</p>
<h2>Let’s switch browsers</h2>
<p>I know how Firefox, Chrome and Safari differ, but I don’t know too much about other browsers and I want to change that. I will switch browsers every two weeks to learn more about them and I invite you to do the same.</p>
<p>So, for the next two weeks I’ll use <a href="https://vivaldi.com/">Vivaldi</a> on my laptop and <a href="https://brave.com/">Brave</a> on my smartphone and I will share my experience before I switch again.<br />
If you join me, I'd love to hear about your experience via <a href="manuel@matuzo.at">email</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CHow+many+browsers+do+you+know%3F%E2%80%9D">blog@matuzo.at</a>.</p> Here’s what I didn’t know about “color”2020-04-19T00:00:00+00:00https://www.matuzo.at/blog/heres-what-i-didnt-know-about-color
<p>This is part 2 of my series <a href="/blog/heres-what-i-didnt-know/">Here’s what I didn’t know about…</a> in which I try to learn new things about CSS. This time I'm trying to find out what I didn’t know about the <code>color</code> property.</p><p>When setting the CSS <code>color</code> property, 2 things happen.</p>
<ol>
<li>The foreground <code>color</code> value of an element's text changes.</li>
<li>The <code>currentcolor</code> value changes.</li>
</ol>
<pre><code class="language-css">a {
color: #237680;
}
circle {
fill: currentColor;
}</code></pre>
<pre><code class="language-html"><a href="#">
Hello World!
<svg height="30" width="30" focusable="false">
<circle cx="15" cy="15" r="10" fill="red">
</svg>
</a></code></pre>
<style>
.rich-text .link-reset:link,
.rich-text .link-reset:visited {
box-shadow: none !important;
text-decoration: underline !important;
transition: none !important;
color: #237680 !important;
}
.rich-text .link-reset circle {
fill: currentColor;
}
</style>
<p>
<a href="#" class="link-reset">
Hello World!
<svg height="30" width="30" focusable="false">
<circle cx="15" cy="15" r="10" fill="red">
</svg>
</a>
</p>
<h2>currentColor is the default color value of some properties</h2>
<p>Usually when I work with the <code>border</code> property, I change the width and color of the border. That’s probably why I’ve never noticed that the default value of <code>border-color</code> is <code>currentColor</code>.</p>
<pre><code class="language-css">.parent {
color: #ca3041;
border-style: solid;
}</code></pre>
<pre><code class="language-html"><div class="parent">yo!</div></code></pre>
<style>
.div-color {
color: #ca3041;
}
</style>
<div class="div-color" style="border-style: solid;">yo!</div>
<p>So, if you change the <code>color</code> value of an element, its border color changes, too.<br><br />
That’s the case for most properties that have a color.</p>
<h3>text-emphasis-color</h3>
<pre><code class="language-css">.parent em {
text-emphasis-style: '*';
}</code></pre>
<div class="div-color"><em style="text-emphasis-style: '*'"><abbr title="What the fuck">WTF</abbr> is text-emphasis?</em></div>
<h3>text-shadow</h3>
<pre><code class="language-css">.parent span {
text-shadow: 5px 10px;
}</code></pre>
<div class="div-color"><span style="text-shadow: 5px 10px;">yo!</span></div>
<h3>text-decoration-color</h3>
<pre><code class="language-css">.parent span {
text-decoration: overline underline;
}</code></pre>
<div class="div-color"><span style="text-decoration: overline underline">yo!</span></div>
<h3>caret-color</h3>
<blockquote>
<p>The user agent selects an appropriate color for the caret. This is generally currentcolor, but the user agent may choose a different color to ensure good visibility and contrast with the surrounding content, taking into account the value of currentcolor, the background, shadows, and other factors.</p>
</blockquote>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/caret-color">https://developer.mozilla.org/en-US/docs/Web/CSS/caret-color</a></p>
<pre><code class="language-css">input {
color: #ca3041;
}</code></pre>
<p>
<label for="input">The text and caret color should be red</label> <br>
<input type="text" class="div-color" id="input">
</p>
<h3>outline-color</h3>
<pre><code class="language-css">.parent span {
outline-style: dotted;
}</code></pre>
<div class="div-color"><span style="outline-style: dotted">yo!</span></div>
<h3>column-rule-color</h3>
<pre><code class="language-css">.parent p {
columns: 3;
column-rule: solid;
}</code></pre>
<div class="div-color"><p style="columns: 3; column-rule: solid;">I didn’t know that column-rule exists. How did I miss that?</p></div>
<p>Now that I've written it down, it absolutely makes sense and I guess I subconsciously knew how most of the properties behave, but I just wasn't aware. Now I am. :)</p>
<h2>HSL colors</h2>
<p>Yes, I know, HSL colors are not specific to the color property and they’ve been around for forever (IE 9+), but I’ve never used them and I don’t know how they work. Now is a good time to find out.</p>
<p>HSL (Hue, Saturation, Lightness) is an alternative representation of the RGB color model.</p>
<h3>Hue</h3>
<p>H is an angle of the color circle. It can be defined using 4 different units.</p>
<p><code>deg</code> (or unitless): a value between 0 and 360. Red: 0deg, Green: 120deg, Blue: 240deg.<br />
<code>rad</code>: a value between 0 and 2π (~6.2832). (Red: 0rad, Green: 2.0944rad, Blue: 4.1888rad)<br />
<code>grad</code>: a value between 0 and 400. (Re: 0grad, Green: 133.33grad, Blue: 266.66grad)<br />
<code>turn</code>: a value between 0 and 1. (Re: 0grad, Green: 0.333turn, Blue: 0.6666turn)</p>
<p>For example, a right angle is 90deg = 100grad = 0.25turn ≈ 1.5708rad</p>
<p>This page about <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/angle">the <angle> CSS data type</a> on MDN helped me better understand how these units relate.</p>
<h3>Saturation</h3>
<p>S is a percentage. 0% is fully unsaturated (gray), 100% is fully saturated.</p>
<h3>Lightness</h3>
<p>L is a percentage. 0% lightness is black, 50% lightness is default, and 100% lightness is white.</p>
<pre><code class="language-css">body {
background: hsl(0, 100%, 100%); /* = #FFFFFF */
}</code></pre>
<p style="background: hsl(0, 100%, 100%); color: hsl(0, 100%, 0%)">hsl(0, 100%, 100%)</p>
<pre><code class="language-css">body {
background: hsl(0, 100%, 0%); /* = #000000 */
}</code></pre>
<p style="background: hsl(0, 100%, 0%); color: hsl(0, 100%, 100%)">hsl(0, 100%, 0%)</p>
<pre><code class="language-css">body {
background: hsl(0, 100%, 50%); /* = #FF0000 */
}</code></pre>
<p style="background: hsl(0, 100%, 50%); color: hsl(0, 100%, 100%)">hsl(0, 100%, 50%)</p>
<pre><code class="language-css">body {
background: hsl(0, 50%, 50%); /* = ##bf4040 */
}</code></pre>
<p style="background: hsl(0, 50%, 50%); color: hsl(0, 100%, 100%)">hsl(0, 50%, 50%)</p>
<p>There’s also hsla, which adds support for a fourth parameter (alpha).</p>
<pre><code class="language-css">body {
background: hsla(0, 100%, 50%, 0.5); /* = rgba(255, 0, 0, 0.5)*/
}</code></pre>
<p style="background: hsla(0, 100%, 50%, 0.5); color: hsl(0, 100%, 0%)">hsla(0, 100%, 50%, 0.5)</p>
<p>CSS Colors Level 4 adds support for space-separated values.</p>
<pre><code class="language-css">body {
background: hsl(0 100% 50% / .15); /* = rgba(255, 0, 0, 0.15*/
background: hsl(0 100% 50% / 15%) /* = rgba(255, 0, 0, 0.15*/
}</code></pre>
<p style="background: hsl(0 100% 50% / 15%); color: hsl(0, 100%, 0%)">hsl(0 100% 50% / 15%)</p>
<blockquote>
<p>Many designers find HSL more intuitive than RGB, since it allows hue, saturation, and lightness to each be adjusted independently. HSL can also make it easier to create a set of matching colors (such as when you want multiple shades of a single hue).</p>
</blockquote>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#HSL_colors">https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#HSL_colors</a></p>
<p>Thanks for reading ❤️. In part 3 I'll probably write about <code>text-emphasis</code> or <code>column-rule</code> 🙃.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CHere%E2%80%99s+what+I+didn%E2%80%99t+know+about+%E2%80%9Ccolor%E2%80%9D%E2%80%9D">blog@matuzo.at</a>.</p> Blogging is one of the best ways of learning2020-04-19T00:00:00+00:00https://www.matuzo.at/blog/blogging
<p>I can’t stress enough how important it is to blog if you want to become better at web development. You learn so much more by explaining something in your own words than by just reading and copying & pasting.</p><p>I overcame my fear of writing and speaking, because I realized that…</p>
<ol>
<li><strong>It doesn’t matter if someone else has written about the same topic</strong>. Different perspectives are important.</li>
<li>You don’t have to create cool demos or present smart hacks. <strong>Most people are looking for actionable advice</strong>, not next level shit or eye candy.</li>
<li>It doesn’t matter how many people read your blog. <strong>Someone somewhere will be glad you wrote that article</strong>.</li>
<li><strong>Talk about what interests you, not about what you believe others will like</strong>. Most of my talks and articles are about “basic” HTML and CSS, because that's what I like.</li>
<li><strong>Don’t be afraid to write/speak in a foreign language.</strong> I speak English with a strong accent and there are probably many mistakes in this tweet alone, but guess what, most people don’t care. My English isn’t perfect, but I gave a talk in Toronto and people understood me…<br><br />
…at least I hope they did. 😁</li>
<li><strong>It’s okay to make mistakes.</strong> If you or someone else finds an error, fix it, explain what and why you’ve changed it and move on.</li>
<li><strong>Don’t be afraid of what others might say or think.</strong> Some people are assholes (ignore), but most people are nice.</li>
</ol>
<p>I’ve learned so much more in the last 4-5 years compared to the years before, just because I wrote and talked about the topics that interested me and not about things I already knew.</p>
<p>This is a copy of a <a href="https://twitter.com/mmatuzo/status/1251857510186856449">thread on Twitter</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CBlogging+is+one+of+the+best+ways+of+learning%E2%80%9D">blog@matuzo.at</a>.</p> The beauty of progressive enhancement2020-04-28T00:00:00+00:00https://www.matuzo.at/blog/beauty-of-progressive-enhancement
<p>Nokia released an updated version of its iconic <a href="https://www.nokia.com/phones/en_int/nokia-3310">Nokia 3310</a> about 3 years ago. It was affordable for me (€60/$65), so I had to get one. It came with a 2 MP camera, a battery that lasts 30 days (up to 22 hours talk time), 2G, 16 MB storage, the original Snake game, and a browser.</p><p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,q_90,w_800/v1588058813/articles/progressive/nokia_phones.jpg" alt="4 Nokia 3310 phones in blue, read, gray, and yellow" /></p>
<p>Screenshot: <a href="https://www.nokia.com/phones/en_int/nokia-3310">Nokia</a>.</p>
<h2>Opera Mini</h2>
<p>You can access the worldwide web on the Nokia 3310 with <a href="https://www.opera.com/de/mobile">Opera Mini</a>. There are different versions of the Opera Mini browser, how it renders pages depends on the operating system, device, and settings you’re using.<br />
If you install it on your smartphone, you probably won’t see any differences, because it uses the underlying browser engine on your Android or iOS phone. An interesting bit about this browser, though, is that you can set different data savings options (off, automatic, high, or extreme). When browsing in extreme mode requests are sent to one of Operas proxy servers which retrieves the web page, processes and compresses it, and sends it back. Opera claims that this reduces the amount of data transmitted up to 90%. This limits interactivity because JavaScript is processed only by the proxy server, and the device just renders it.</p>
<p>Some other notable things about JavaScript in Opera Mini:</p>
<ul>
<li>All scripts are allowed a <strong>maximum of two seconds to execute</strong>.</li>
<li>setInterval and <strong>setTimeout functions are disabled</strong>.</li>
<li>The number of <strong>events allowed to trigger scripts is limited</strong>.</li>
</ul>
<p>Opera Mini has a strong focus on performance and data saving. I guess that’s also one reason why it’s installed on my Nokia phone. Now, the browser installed on my 3310 is different to the Opera Mini version on my smartphone. It’s the one that’s usually presented in a beautiful red color on <a href="https://caniuse.com/">caniuse.com</a>, if you search for almost any feature.</p>
<figure class="figure figure--full">
<a href="https://caniuse.com/#feat=transforms3d" rel="noopener"><img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,q_100,w_1290/v1588059649/articles/progressive/caniuse.jpg" alt="Most browsers support 2D transforms in CSS. Opera Mini is an exception. caniuse.com Screenshot."></a>
<figcaption>
Opera Mini only supports a limited set of CSS and JS features.
</figcaption>
</figure>
<h2>Progressive Enhancement</h2>
<p>Yesterday I was curious and wanted to know if and how <a href="https://www.frontendbookmarks.com/">a website I recently made</a> renders on the Nokia 3310.</p>
<p>Here’s a short demo of how the website looks like in Safari on an iPhone XR compared to Opera Mini.</p>
<div class="content__video-wrapper"><div class="video-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/78gtfL9ZA7U" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="Progressive Enhancement Demo on a Nokia 3310." loading="lazy"></iframe></div></div>
<p>To my surprise, I only had to <a href="https://github.com/matuzo/front-end-bookmarks/commit/6f1c0cdc3bef830b9f87ad1a1d50bc4b4c258166">reduce some paddings and font sizes</a> to make it look nice.<br />
I didn’t have to change much because I follow the Progressive Enhancement principle when I build websites. Progressive Enhancement focuses on content and enhances experiences layer by layer. <a href="https://www.aaron-gustafson.com/">Aaron Gustafson</a> explains how we can apply Progressive Enhancement to web development in his iconic article <a href="https://alistapart.com/article/understandingprogressiveenhancement/">“Understanding Progressive Enhancement“</a>.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,q_100,w_540/v1588060675/articles/progressive/m-m.jpg" alt="A peanut M&M whith each layer labelled. The nut represents content, the chocolate layer presentation and the sugar coating JavaScript." /></p>
<p>Image: <a href="https://alistapart.com/article/understandingprogressiveenhancement/">A List Apart</a></p>
<blockquote>
<p>Start with your content peanut, marked up in rich, semantic (X)HTML. Coat that content with a layer of rich, creamy CSS. Finally, add JavaScript as the hard candy shell to make a wonderfully tasty treat (and keep it from melting in your hands).</p>
</blockquote>
<p><cite>Aaron Gustafson</cite>.</p>
<h2>How I’m applying Progressive Enhancement</h2>
<p>I have some practical examples for you to help you better understand what Progressive Enhancement on the web means.</p>
<h3>Grid</h3>
<p>For the 2-column layout on the homepage of <a href="https://www.frontendbookmarks.com/">Front-end Bookmarks</a>, I’m using CSS Grid. If a browser doesn’t understand <code>display: grid</code> it just falls back to a single column layout. Note that I don’t have to use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Conditional_Rules/Using_Feature_Queries">feature queries</a> because browsers just ignore properties they don’t understand.</p>
<pre><code class="language-css">ul {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(29rem,1fr));
grid-gap: .7rem;
}</code></pre>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1588061714/articles/progressive/ie11ff.jpg" alt="single column vs. 2 column layout comparison" /></p>
<p>The single column layout isn’t ideal, but it's good enough.</p>
<h3>Search</h3>
<p>Modern browsers render a combo-box in the page's header that lets you browse, filter and select pages. In my JavaScript I want to use arrow functions, template literals, etc. without having to polyfill these features for less capable browsers. That’s why I’m sending JavaScript only to browsers that understand ES2015+. I do that by adding <code>type="module"</code> to my script tags.</p>
<pre><code class="language-js"><script src="/assets/js/scripts.min.js" type="module"></script></code></pre>
<p>Browsers will only execute the script if they support JavaScript modules, and browsers that support modules also support ES2015+ features. Philip Walton introduced this technique in 2017 in his article <a href="https://philipwalton.com/articles/deploying-es2015-code-in-production-today/">Deploying ES2015+ Code in Production Today</a>.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1588062035/articles/progressive/ie11ff2.jpg" alt="simple search form on the left in IE, combobox on the right in Firefox" /></p>
<p>This rich JavaScript component falls back to a simple search form. The input field is pre-populated with the value <i>“site:www.frontendbookmarks.com ”</i>. If you add a search term to this string and hit the submit button, the search engine <a href="https://duckduckgo.com">DuckDuckGo</a> will open and <a href="https://duckduckgo.com/?q=site%3A+frontendbookmarks.com+html&t=h_&ia=web">search for the entered keyword on frontendbookmarks.com</a>.<br />
It’s not the best experience, but better than no experience.</p>
<pre><code class="language-html"><form action="https://duckduckgo.com/" method="GET">
<label for="search">Search on DuckDuckGo</label>
<input
id="search"
name="q"
value="site:www.frontendbookmarks.com "
type="text"
/>
<button type="submit">Search</button>
</form></code></pre>
<p>I stole that idea from <a href="https://www.zachleat.com/">Zach</a> who uses a similar solution for search in the <a href="https://www.11ty.dev/docs/search/">eleventy docs</a>.</p>
<p>I’m planning on improving the performance on Front-end Bookmarks by using the webp image format and lazy loading. I haven’t implemented it yet, but for both features I will also make use of Progressive Enhancement.</p>
<h3>Webp Images</h3>
<p>The advantage of webp is the file size, which usually is much smaller compared to other image formats. Unfortunately, I can’t just replace all my jpgs with webps, because <a href="https://caniuse.com/#feat=webp">Safari and IE don't support WebP</a>, but we can give browsers options by using the <code>picture</code> element.</p>
<pre><code class="language-html"><picture>
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="image description" />
</picture></code></pre>
<p>Browsers read the <code>picture</code> element from top to bottom. If they support WebP, they will use the webp image defined in the <code>source</code> element. If not, they just skip it and use the jpg defined in the <code>img</code> tag instead.</p>
<h3>Lazy loading</h3>
<p>To optimize image loading performance, I’ll use lazy-loading which makes sure that images are only downloaded if they’re visible in (or close to) the viewport. I really don’t want to use a large third party script for that, instead I’ll go for native lazy loading with a fallback for browsers that don’t support it.</p>
<pre><code class="language-html"><img data-src="myimage.jpg" loading="lazy" /></code></pre>
<pre><code class="language-js">// Replace the src attribute with the value in the data-src attribute
// for browsers that support native lazy-loading.
if ('loading' in HTMLImageElement.prototype) {
const images = document.querySelectorAll("img[loading='lazy']");
images.forEach((img) => {
img.src = img.dataset.src;
});
} else {
// Fallback for other browsers
}</code></pre>
<p>Rahul Nanwani explains in his article <a href="https://css-tricks.com/the-complete-guide-to-lazy-loading-images/">The Complete Guide to Lazy Loading Images</a> how a fallback for native lazy-loading might look like.</p>
<p>Update June 10, 2020.</p>
<p>This pattern uses Javascript to add the <code>src</code> attribute to the image. If JavaScript is not active in the browser, the image won’t show. We can work around that by progressively enhancing the pattern once more.</p>
<pre><code class="language-html"><img data-src="myimage.jpg" loading="lazy" />
<noscript>
<img src="myimage_lowres.jpg" />
</noscript></code></pre>
<p>Content wrapped in <code>noscript</code> tags will only execute, if scripting is currently turned off in the browser. For that scenario, we can provide a low-resolution version of the image which is much smaller in file size.</p>
<h2>Progressive Enhancement is beautiful</h2>
<p>I’ve titled this post <em>The beauty of progressive enhancement</em> because it’s beautiful to see which shape an experience can take on different devices, operating systems, and browsers. Progressive Enhancement allows us to use the latest and greatest features HTML, CSS and JavaScript offer us, by providing a basic, but robust foundation for all.<br />
Enabling people who use browsers like Internet Explorer 11 or Opera Mini to access content is essential. Don’t rely on browser statistics, think about who you’re building websites for. Our users are diverse, just like their physical abilities, personal preferences, and the devices and browsers they’re using.</p>
<p>Thanks for reading. ❤️<br />
If you have more examples for progressive enhacement, please share them with me via <a href="mailto: manuel@matuzo.at">e-mail</a>.</p>
<h2>Resources</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Opera_Mini">Opera Mini on wikipedia</a></li>
<li><a href="https://css-tricks.com/using-webp-images/">Using WebP Images</a> by Jeremy Wagner</li>
<li><a href="https://addyosmani.com/blog/lazy-loading/">Native image lazy-loading for the web!</a> by Addy Osmani</li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CThe+beauty+of+progressive+enhancement%E2%80%9D">blog@matuzo.at</a>.</p> Here’s what I didn’t know about “content”2020-05-26T00:00:00+00:00https://www.matuzo.at/blog/heres-what-i-didnt-know-about-content
<p>This is part 3 of my series <a href="/blog/heres-what-i-didnt-know/">Here’s what I didn’t know about…</a> in which I try to learn new things about CSS. This time I'm trying to find out what I didn’t know about the <code>content</code> property.</p><p>A few weeks ago Stefan published a post on his website called “<a href="https://www.stefanjudis.com/today-i-learned/css-content-accepts-alternative-text/">The CSS "content" property accepts alternative text</a>”, which blew my mind. He showed that the <code>content</code> property excepts 2 values and not just 1, the actual content and an alternative text.</p>
<pre><code class="language-css">.new-item::before {
/* "Highlighted item" and element content is read out */
content: '★' / 'Highlighted item';
}</code></pre>
<p>I didn’t know that and I was wondering if there were more things I didn’t know about the <code>content</code> property. Since you’re reading this, I found something, so let’s see what I was able to add to my <a href="https://www.matuzo.at/blog/heres-what-i-didnt-know/">“Here’s what I didn’t know about…”</a> series.</p>
<h2>How I’m using the content attribute.</h2>
<p>Before I started my research, I was using this property primarily for 3 things.</p>
<h3>Adding an element to another element using pseudo elements</h3>
<p>If I want to create a simple shape in CSS that is not a rectangle or circle, I use <code>:after</code> and <code>::before</code> to give myself more options for styling.</p>
<style>
.contentdemo1 {
width: 70px;
height: 50px;
margin-top: 15px;
border: 5px solid #123456;
border-radius: 5px;
position: relative;
}
.contentdemo1::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: -16px;
width: 20px;
height: 20px;
margin: auto;
border: solid #123456;
border-width: 5px 5px 0 0;
border-radius: 5px;
transform: rotate(-45deg);
background: #fafafa;
}
</style>
<div class="contentdemo1"></div>
<pre><code class="language-html"><div></div></code></pre>
<pre><code class="language-css">div {
width: 70px;
height: 50px;
margin-top: 15px;
border: 5px solid #123456;
border-radius: 5px;
position: relative;
}
div::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: -16px;
width: 20px;
height: 20px;
margin: auto;
border: solid #123456;
border-width: 5px 5px 0 0;
border-radius: 5px;
transform: rotate(-45deg);
background: #fff;
}</code></pre>
<p>Checkout <a href="https://codepen.io/matuzo/pen/eYpLKdz">example 1 on CodePen</a>.</p>
<p>To render on screen, the pseudo elements needs the <code>content</code> attribute.</p>
<h3>Revealing URLs in print styles sheets</h3>
<p>Printed links are useless if you don’t know where there are leading. I’m using a combination of <code>content</code> and the <code>attr()</code> function in print style sheets to display URLs next to their linked text.</p>
<style>
.contentdemo2[href^="http://"]:after,
.contentdemo2[href^="https://"]:after {
content: " (" attr(href) ")";
}
</style>
<p><a href="https://mxb.dev/" class="contentdemo2">Max Böck</a></p>
<pre><code class="language-css">@media print {
a[href^="http://"]:after,
a[href^="https://"]:after
{
content: ' (' attr(href) ')';
}
}</code></pre>
<h3>Custom counters</h3>
<p>Every now and then I need custom counters in lists. A combination of <code>content</code> and counter properties usually does the job.</p>
<style>
.contentdemo3 {
list-style-type: none;
counter-reset: mylist;
}
.contentdemo3 > li {
counter-increment: mylist;
}
.contentdemo3 > li::before {
content: "🤤 " counter(mylist) ": "
}
</style>
<ol class="contentdemo3">
<li>Element 001</li>
<li>Element 002</li>
<li>Element 003</li>
</ol>
<pre><code class="language-html"><ol>
<li>Element 001</li>
<li>Element 002</li>
<li>Element 003</li>
</ol></code></pre>
<pre><code class="language-css">ol {
list-style-type: none;
counter-reset: mylist;
}
li {
counter-increment: mylist;
}
li::before {
content: '🤤 ' counter(mylist) ': ';
}</code></pre>
<p>Checkout <a href="https://codepen.io/matuzo/pen/eYpLKGg">example 2 on CodePen</a>.</p>
<p>Have a look at <a href="https://www.matuzo.at/blog/heres-what-i-didnt-know-about-list-style-type/">Here’s what I didn’t know about list-style-type</a> for more options to style list items.</p>
<p>Now, let’s see what else <code>content</code> can do for us. Here’s what I’ve learned recently:</p>
<h2>Content accepts images and gradients</h2>
<p>I knew that <code>content</code> accepts the <code>counter</code> and <code>attr</code> functions, but it never came to my mind that it might allow other functions, as well. Whenever I needed an image in a pseudo element, I would use <code>background-image</code>, although <code>content</code> would've worked, too.</p>
<style>
.contentdemo7::before {
content: url('/blog/heres-what-i-didnt-know-about-content/pin.png');
}
</style>
<div class="contentdemo7"></div>
<pre><code class="language-css">div::before {
content: url('pin.png');
}</code></pre>
<p>Checkout <a href="https://codepen.io/matuzo/pen/yLYZgqO?editors=1100">example 3 on CodePen</a>.</p>
<p>If this works with images, it should work with gradients too, right? Yeah, well, no. Chrome seems to be the only browser that renders pseudo elements with gradient content values.</p>
<pre><code class="language-css">div::before {
content: linear-gradient(blue, red);
height: 50px;
width: 50px;
display: block;
border: 1px solid red;
}</code></pre>
<p>Checkout <a href="https://codepen.io/matuzo/pen/WNQPRPv?editors=1100">example 4 on CodePen</a>.</p>
<h2>You can define alt text for content values (…in Chrome)</h2>
<p>What’s the point of using <code>content</code> when <code>background-image</code> has better support? The reason Stefan wrote his post, <code>content</code> supports alt text.</p>
<pre><code class="language-css">div::before {
content: url('pin.png') / 'You are here.';
}</code></pre>
<p>Checkout <a href="https://codepen.io/matuzo/pen/ZEbweNj?editors=1100">example 5 on CodePen</a>.</p>
<p>Unfortunately, this only works in Chrome (tested on macOS 10.15.4, Chrome 81 with VoiceOver). Firefox and Safari don't even render the pseudo element because the value is invalid. Too bad.</p>
<p>Even if this worked in most browsers, I wouldn’t recommend adding text content to a CSS file. Others working on the project probably wouldn’t expect text coming from a CSS file, things might get messy on sites with multiple languages, auto-translation may not work, and the content is only accessible if the CSS loads successfully.<br />
Adrian Roselli shares an example of a poor practice in <a href="https://adrianroselli.com/2020/02/link-targets-and-3-2-5.html#CSS">Link Targets and 3.2.5</a>.</p>
<h2>You can combine text and images</h2>
<p>Nor did I know that you can use the <code>url()</code> function as a value, I also didn’t know that you can combine it with text.</p>
<style>
.contentdemo4::before {
content: url('/blog/heres-what-i-didnt-know-about-content/pin.png') "You are here."
}
</style>
<div class="contentdemo4"></div>
<pre><code class="language-css">div::before {
content: url('pin.png') 'You are here.';
}</code></pre>
<p>Checkout <a href="https://codepen.io/matuzo/pen/xxwMLgJ?editors=1100">example 6 on CodePen</a>.</p>
<h2>You can only replace the content of a regular element with an <image></h2>
<p>The <code>content</code> property is meant to be used with pseudo elements. You can’t use it to replace a string in an element with another string. This won’t work:</p>
<pre><code class="language-css">div {
content: 'You are here';
}</code></pre>
<p>But you can use it to replace a string with an image. The string is still in the <abbr title="Document Object Model">DOM</abbr> but screen readers announce the filename. (You can do it, but you shouldn’t.)</p>
<style>
.contentdemo5 {
content: url('/blog/heres-what-i-didnt-know-about-content/pin.png');
}
</style>
<div class="contentdemo5">You are here!</div>
<pre><code class="language-html"><div>You are here!</div></code></pre>
<pre><code class="language-css">div {
content: url('pin.png');
}</code></pre>
<p>Checkout <a href="https://codepen.io/matuzo/pen/pojGrPw?editors=1100">example 7 on CodePen</a>.</p>
<h2>There are quotes and no-quotes</h2>
<p>Okay, now this one is really cool. Let’s say we have a quote nested in another quote.</p>
<pre><code class="language-html"><blockquote>
My mama always said,
<q>
Life was like a box of chocolates. You never know what you’re gonna get </q
>.
</blockquote></code></pre>
<style>
.contentdemoquote {
margin-bottom: 3rem !important;
font-weight: bold;
}
.contentdemoquote::before {
display: none;
transform: none !important;
border: none !important;
width: auto !important;
height: auto !important;
position: static !important;
}
</style>
<blockquote class="contentdemoquote">
My mama always said,
<q>
Life was like a box of chocolates. You never know what you’re gonna get.
</q>
</blockquote>
<p>What you should see in the above example is that the <code>blockquote</code> has no quotes and <code>q</code> has double quotes.</p>
<p>If we add opening and closing quotes to the <code>blockquote</code> using pseudo elements and the <code>content</code> property, the <code>blockquote</code> now displays double quotes and the <code>q</code> automatically single quotes.</p>
<pre><code class="language-css">blockquote::before {
content: open-quote;
}
blockquote::after {
content: close-quote;
}</code></pre>
<style>
.contentdemoquote2::before {
content: open-quote !important;
display: inline;
}
.contentdemoquote2::after {
content: close-quote;
}
.contentdemoquote2[lang="fr"]::before {
content: open-quote "\00a0" !important;
display: inline;
}
.contentdemoquote2[lang="fr"]::after {
content: "\00a0" close-quote;
}
</style>
<blockquote class="contentdemoquote contentdemoquote2">
My mama always said,
<q>
Life was like a box of chocolates. You never know what you’re gonna get.
</q>
</blockquote>
<p>Nice! To top it all off, we can even have a combination of the two variations, <code>blockquote</code> with no quotes and <code>q</code> with single quotes.</p>
<pre><code class="language-css">blockquote::before {
content: no-open-quote;
}
blockquote::after {
content: no-close-quote;
}</code></pre>
<style>
.contentdemoquote3::before {
content: no-open-quote !important;
display: inline;
}
.contentdemoquote3::after {
content: no-close-quote;
}
</style>
<blockquote class="contentdemoquote contentdemoquote3">
My mama always said,
<q>
Life was like a box of chocolates. You never know what you’re gonna get.
</q>
</blockquote>
<p>The <code>no-open-quote</code> and <code>no-close-quote</code> keywords don’t insert anything, but increment the quotation depth by one.</p>
<h3>Nested quotes in different languages</h3>
<p>Just because I was curious, here are some variations of the second example in other languages.</p>
<h4>French</h4>
<pre><code class="language-html"><blockquote lang="fr">
Maman disait toujours,
<q>
la vie, c'est comme une boîte de chocolats: on ne sait jamais sur quoi on va
tomber </q
>.
</blockquote></code></pre>
<blockquote lang="fr" class="contentdemoquote contentdemoquote2">
Maman disait toujours,
<q>
la vie, c'est comme une boîte de chocolats: on ne sait jamais sur quoi on va tomber
</q>.
</blockquote>
<h4>Russian</h4>
<pre><code class="language-html"><blockquote lang="ru">
Моя мама всегда говорила,
<q>
Жизнь как коробка шоколадных конфет: никогда не знаешь, какая начинка тебе
попадётся </q
>.
</blockquote></code></pre>
<blockquote lang="ru" class="contentdemoquote contentdemoquote2">
Моя мама всегда говорила,
<q>
Жизнь как коробка шоколадных конфет: никогда не знаешь, какая начинка тебе попадётся
</q>.
</blockquote>
<h4>German</h4>
<pre><code class="language-html"><blockquote lang="de">
Mama hat immer gesagt,
<q>
Das Leben ist wie eine Schachtel Pralinen. Man weiß nie, was man kriegt </q
>.
</blockquote></code></pre>
<blockquote lang="de" class="contentdemoquote contentdemoquote2">
Mama hat immer gesagt,
<q>
Das Leben ist wie eine Schachtel Pralinen. Man weiß nie, was man kriegt
</q>.
</blockquote>
<h4>Spanish</h4>
<pre><code class="language-html"><blockquote lang="es">
Mi mamá siempre decía,
<q>
La vida es como una caja de bombones, nunca sabes lo que vas a conseguir </q
>.
</blockquote></code></pre>
<blockquote lang="es" class="contentdemoquote contentdemoquote2">
Mi mamá siempre decía,
<q>
La vida es como una caja de bombones, nunca sabes lo que vas a conseguir
</q>.
</blockquote>
<p>Sorry, if I fucked up any of the translations.</p>
<h2>There’s counter() and counters()</h2>
<p>I'm really not sure if I was aware of the fact there isn’t just a <code>counter()</code> but also a <code>counters()</code> function. However, the difference is that <code>counters()</code> enables nested custom counters.</p>
<style>
.contentdemo6 {
list-style-type: none;
counter-reset: mylist;
}
.contentdemo6 ol {
list-style-type: none;
counter-reset: mylist;
}
.contentdemo6 li {
counter-increment: mylist;
}
.contentdemo6 li::before {
content: "🤤 " counters(mylist, ".") ": "
}
</style>
<ol class="contentdemo6">
<li>
Element 001
<ol>
<li>Element 001</li>
<li>Element 002</li>
<li>Element 003</li>
</ol>
</li>
<li>Element 002</li>
<li>Element 003</li>
</ol>
<pre><code class="language-html"><ol>
<li>
Element 001
<ol>
<li>Element 001</li>
<li>Element 002</li>
<li>Element 003</li>
</ol>
</li>
<li>Element 002</li>
<li>Element 003</li>
</ol></code></pre>
<pre><code class="language-css">ol {
list-style-type: none;
counter-reset: mylist;
}
li {
counter-increment: mylist;
}
li::before {
content: '🤤 ' counters(mylist, '.') ': ';
}</code></pre>
<p>Checkout <a href="https://codepen.io/matuzo/pen/ZEbwMNR">example 8 on CodePen</a>.</p>
<p>You’ll find more ways of using the <code>content</code> property in Adrian’s article <a href="https://adrianroselli.com/2019/12/showing-file-types-in-links.html">https://adrianroselli.com/2019/12/showing-file-types-in-links.html</a>.</p>
<p>Wow, that was a lot. I didn’t expect to write and learn so much. I hope that you’ve learned as much as I did.</p>
<p>Thanks for reading ❤️ and thanks to Stefan for the inspiration for this post.</p>
<h2>Updates</h2>
<p><strong>28.05.2020</strong> Added a disclaimer about putting text in CSS files, and a link to an article by Adrian Roselli.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CHere%E2%80%99s+what+I+didn%E2%80%99t+know+about+%E2%80%9Ccontent%E2%80%9D%E2%80%9D">blog@matuzo.at</a>.</p> Reverse ordered lists2020-06-04T00:00:00+00:00https://www.matuzo.at/blog/reverse-ordered-lists
<p>I’m working on a project where I have a list of items in reverse order. The list starts with the latest item and ends with the oldest. I wanted to express that both semantically and visually. I did some research and found interesting solutions, some of them good, others not so much.</p><p>The result should look a little like this.</p>
<p>3. C<br />
2. B<br />
1. A</p>
<p>Let’s check out our options.</p>
<h2>The reversed attribute in HTML</h2>
<p>The easiest and most straightforward solution is the <code>reversed</code> attribute in HTML.</p>
<pre><code class="language-html"><ol reversed>
<li>C</li>
<li>B</li>
<li>A</li>
</ol></code></pre>
<style>
.article-list {
list-style-type: decimal;
}
.article-list li {
margin-bottom: 0;
}
</style>
<ol reversed class="article-list">
<li>C</li>
<li>B</li>
<li>A</li>
</ol>
<p>It reverses the order of the list marker semantically and visually; the list starts with 3. and ends with 1., and screen readers announce the list in <abbr title="Document Object Model">DOM</abbr> order together with the correct number. “3 C 2 B 1 A”</p>
<p>That’s how you do it, you can stop reading here, if you don’t need custom styling and it doesn’t bother you that the <code>reversed</code> attribute is <a href="https://caniuse.com/#feat=ol-reversed">not supported in IE and old implementations of Edge</a>.<br />
Keep reading, if you need more options, better browser support and more flexibility.</p>
<h2>The value attribute in HTML</h2>
<p>Another approach, which yields the same result, is using the value attribute on the list items.</p>
<pre><code class="language-html"><ol>
<li value="3">C</li>
<li value="2">B</li>
<li value="1">A</li>
</ol></code></pre>
<ol class="article-list">
<li value="3">C</li>
<li value="2">B</li>
<li value="1">A</li>
</ol>
<p>It's more verbose, but we also have more control over the list. You could do something like this:</p>
<pre><code class="language-html"><ol>
<li value="6">C</li>
<li value="4">B</li>
<li value="2">A</li>
</ol></code></pre>
<ol class="article-list">
<li value="6">C</li>
<li value="4">B</li>
<li value="2">A</li>
</ol>
<p>You probably shouldn’t do this because skipping numbers may confuse your users, but it’s possible.</p>
<h2>Custom counter()s in CSS</h2>
<p>There aren’t many options to style standard list items and <a href="https://caniuse.com/#feat=css-marker-pseudo">support for the <code>::marker</code> pseudo-element</a> could be better. So, if we want to apply custom styling to our list markers, custom counters in CSS are an option.<br />
To reverse the order of custom counters we have 2 things to do: Reset the counter to a value other than 0 and increment the counter with a negative number.</p>
<pre><code class="language-html"><ol>
<li>C</li>
<li>B</li>
<li>A</li>
</ol></code></pre>
<pre><code class="language-css">ol {
counter-reset: my-custom-counter 4;
list-style: none;
}
ol li {
counter-increment: my-custom-counter -1;
}
ol li::before {
content: counter(my-custom-counter) '. ';
color: #f23c50;
font-size: 2.5rem;
font-weight: bold;
}</code></pre>
<style>
.article-list-custom {
counter-reset: my-custom-counter 4;
padding-left: 20px;
list-style: none;
}
.article-list-custom li {
counter-increment: my-custom-counter -1;
}
.article-list-custom li::before {
content: counter(my-custom-counter) ". ";
color: #f23c50;
font-size: 2.5rem;
font-weight: bold;
}
</style>
<ol class="article-list article-list-custom">
<li value="6">C</li>
<li value="4">B</li>
<li value="2">A</li>
</ol>
<p>If we don’t know the exact number of items, we can move the <code>counter-reset</code> property into our HTML and let our back-end programming language or our static site generator do the counting.</p>
<pre><code class="language-html"><ol
style="counter-reset: my-custom-counter {% raw %}{{ items.length + 1 }}{% endraw %}"
>
<li>C</li>
<li>B</li>
<li>A</li>
</ol></code></pre>
<pre><code class="language-css">ol {
list-style: none;
}
ol li {
counter-increment: my-custom-counter -1;
}
ol li::before {
content: counter(my-custom-counter) '. ';
}</code></pre>
<h2>How not to do it</h2>
<p>Some articles suggested reversing the order of both the list marker and the item itself in CSS using Flexbox or similar techniques. We shouldn’t do that because it only looks correct, but the <abbr title="Document Object Model">DOM</abbr> order stays the same. Changing order in CSS has no effect on <abbr title="Document Object Model">DOM</abbr> order.</p>
<pre><code class="language-html"><ol>
<li>A</li>
<li>B</li>
<li>C</li>
</ol></code></pre>
<pre><code class="language-css">ol {
display: flex;
flex-direction: column-reverse;
}</code></pre>
<style>
.article-list-flexbox {
display: flex;
flex-direction: column-reverse;
}
</style>
<ol class="article-list article-list-flexbox">
<li>A</li>
<li>B</li>
<li>C</li>
</ol>
<p>Visually we get what we wanted, but screen readers follow <abbr title="Document Object Model">DOM</abbr> order and announce the list in the order “ABC” and not “CBA”. Also, if you copy and paste the list, browsers might copy it in its original order “ABC”.</p>
<p>Before I wrap it up, here’s another incredibly creative solution I found on StackOverflow. The result is similar to the Flexbox solution, but there are even more drawbacks (e.g. it messes with scrolling).</p>
<pre><code class="language-html"><ol>
<li>A</li>
<li>B</li>
<li>C</li>
</ol></code></pre>
<pre><code class="language-css">ol {
transform: rotate(180deg);
}
ol > li {
transform: rotate(-180deg);
}</code></pre>
<style>
.article-list-transform {
transform: rotate(180deg);
padding: 0 40px 0 0;
}
.article-list-transform > li {
transform: rotate(-180deg);
}
</style>
<ol class="article-list article-list-transform">
<li>A</li>
<li>B</li>
<li>C</li>
</ol>
<p>I have a lot of respect for desperate developers, but for the love of everything that’s important to you, please don’t do that.</p>
<h2>Got something to share?</h2>
<p>That’s it, thanks for reading. ❤️ If you know of other approaches or if you have questions, find me on <a href="https://twitter.com/mmatuzo">Twitter</a> or write me an <a href="mailto:manuel@matuzo.at">e-mail</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CReverse+ordered+lists%E2%80%9D">blog@matuzo.at</a>.</p> Accessible to some2020-06-24T00:00:00+00:00https://www.matuzo.at/blog/accessible-to-some
<p>According to <a href="https://webaim.org/projects/million/">WebAims annual accessibility analysis</a>, 98.1% of home pages of the top 1,000,000 websites have detectable WCAG 2.0 failures. Some of these sites may only have minor contrast issues or maybe just a single missing <code>id</code>, while others are highly inaccessible. However, this number is pretty damn high, considering the fact that automatic testing tools <a href="https://accessibility.blog.gov.uk/2017/02/24/what-we-found-when-we-tested-tools-on-the-worlds-least-accessible-webpage/">only report obvious accessibility issues</a>.</p><p>Only 1.9% of the tested home pages pass automatic testing, which is fine, but it doesn’t mean that <a href="https://www.matuzo.at/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/">there aren’t any barriers on these websites</a> either. True accessibility extends beyond automated tests and <a href="https://www.w3.org/WAI/standards-guidelines/wcag/">WCAG</a> regulations.</p>
<p>It's fair to say that most websites are <strong>only accessible to some</strong>.</p>
<p>98.1% sounds bad, but for most of us it’s just a number, isn’t it? For me, as someone whose job it is to help others build accessible websites, it’s easier to imagine what the real-life effects of these failures can be. To help you get a better understanding of what this high percentage means for users and their daily experiences on the web, I created a little experiment.</p>
<p>My partner is an architect, and she sparked my interest in modern architecture. That’s why I built a web page about an architectural movement in Europe, and for my experiment I would like you to open the page and read it. Once you’re finished, try to summarise what you’ve just learned, rate your experience and then please come back to the article.</p>
<p><a href="https://cdpn.io/matuzo/debug/LYGxLLJ">The experiment.</a></p>
<p>Impressive stuff, right? I hope that you could access most of the information without using DevTools. I did everything I can to make it as accessible as possible. If you had trouble understanding what the page was about, please note that:</p>
<ol>
<li>Our developers (me) spent countless hours optimising the accessibility and we’re proud that <a href="https://web.dev/measure/">Lighthouse</a>, <a href="https://wave.webaim.org/">Wave</a>, and <a href="https://www.deque.com/axe/">Axe</a> don’t throw any errors.<br />
It’s <strong>100% accessible</strong>.</li>
<li><strong>It’s your choice how you access the page</strong>, but it’s optimised for desktop screen readers. You will have the best experience if you don’t use a mouse. You can <a href="https://www.nvaccess.org/download/">download NVDA for free</a>.</li>
<li><a href="https://twitter.com/TwitterSupport/status/1273332642113617921">This is an early version of the web page</a> and we’re exploring ways to make these types of pages accessible to everyone.</li>
<li>Unfortunately, <strong>we don’t have the budget</strong> to optimise it for non-screen reader users. The money is not worth spending on this niche market.</li>
<li><strong>Why would people using a mouse want to access a website</strong> about expressionist architecture anyway!?</li>
<li>We focus on our target audience, VoiceOver on macOS, NVDA, and JAWS users, but we’re also evaluating <strong>accessibility widgets</strong> that we might add to the website for others. This improvement will allow you to click around as much as you want and optimise the website to your needs.<br />
This will make the website <strong><a href="https://adrianroselli.com/2015/11/be-wary-of-add-on-accessibility.html">120% accessible</a>.</strong></li>
<li>We’re known for top-of-the-line websites and we’re constantly exploring new technology. We’re considering using <abbr title="artificial intelligence">AI</abbr> to draw images based on the information in the <code>alt</code> attribute.<br />
Based on the <code>alt</code> text <em>“A long rectangular building. Besides looking like a ship, Het Schip resembles a bizarre art form. Its appearance is unconventional from all angles. The exterior consists of bright orange bricks, decked with towers and architectural elements in unconventional shapes.”</em> our <abbr title="artificial intelligence">AI</abbr> super algorithms created the following image:</li>
</ol>
<figure class="figure">
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,q_90,w_800/v1592974740/articles/hetschip_comparison.jpg" alt="In the upper half of the image: A photo of Het Schip. In the lower half: A really bad drawing in photoshop by me of an actual ship with a tower on it, windows, and a door. A five year old would've done a better job.">
<figcaption>
Comparison of the original building (top) and our AI-generated image (bottom). <br>
We are getting there.
</figcaption>
</figure>
<p>I guess that most of you could not access all the information. How does that feel? Especially with my bullshit justifications. Pretty bad, right? Now imagine having a similar experience on most websites you visit. That’s not just annoying and troublesome, but it makes you angry and sad.</p>
<p>Please remember how this experience made you feel every time you work on a new website, page, or component. Make your websites <strong>accessible to all</strong>, not just to some.</p>
<h2>Behind the scenes</h2>
<p>You probably want to know how I built this <em>exclusive</em> website. Please be aware that most of the following code samples (I’ve marked those that aren’t bad) illustrate poor and dangerous practices. I wrote them just to make a point, don’t use this code on real websites.</p>
<h3>Labels</h3>
<p>It’s hard to understand what a page is about, if there’s no <code>h1</code> or if it isn’t labelled properly.</p>
<pre><code class="language-html"><h1 aria-label="Expressionist architecture">Document title</h1></code></pre>
<p>“Document title” is shown, but a screen reader announces “Expressionist architecture”.<br />
Note: <code>aria-label</code> must not be used that way. You don’t need it, if there’s text content already.</p>
<h4>Further reading</h4>
<ul>
<li><a href="https://webaim.org/techniques/semanticstructure/#headings">Semantic Structure: Regions, Headings, and Lists</a></li>
</ul>
<h3>Control hints</h3>
<p>Screen readers provide users with instructions on how to interact with elements like links. Additional hints like “click this link to..” or “press this button” are superfluous.</p>
<pre><code class="language-html"><a href="https://en.wikipedia.org/wiki/Expressionism">
expressionist
<span aria-hidden="true">(Click your mouse to open this link)</span>
</a></code></pre>
<p>Some links in my experiment show an additional <em>“(Click your mouse to open this link)”</em> instruction next to the link text.</p>
<h4>Further reading</h4>
<ul>
<li><a href="https://adrianroselli.com/2019/10/stop-giving-control-hints-to-screen-readers.html">Stop Giving Control Hints to Screen Readers</a></li>
</ul>
<h3>Different language</h3>
<p>The <code>lang</code> attribute is important. Applied to the <code>html</code> element, it tells screen readers the natural language of the page. A screen reader might not detect the language automatically; the <code>lang</code> attribute helps it pick the right voice profile.</p>
<p>If you switch language within a sentence, you can use the <code>lang</code> attribute to mark a word or phrase. (That’s a good practice.)</p>
<pre><code class="language-html"><p>There's a certain <span lang="fr">je ne sais quoi</span> in the air.</p></code></pre>
<p>If you don’t do that, it might cause an English voice profile pronouncing a German sentence with an English accent. This can be hard to understand and confusing, just like switching language mid-paragraph.</p>
<pre><code class="language-html"><p class="u-vh">
Expressionist architecture is one of the three dominant styles of Modern…
</p>
<p aria-hidden="true">
Expressionismus in einer der drei dominanten Stile der modernen Architektur…
</p></code></pre>
<p>One sentence is in German, but it’s hidden from screen readers.</p>
<h4>Further reading</h4>
<ul>
<li><a href="https://adrianroselli.com/2015/01/on-use-of-lang-attribute.html">On Use of the Lang Attribute</a></li>
</ul>
<h3>Image</h3>
<p>A missing <code>alt</code> attribute is bad, but an <code>alt</code> with wrong or useless information isn’t much better. Please don't annoy your screen reader users with long, boring, or irrelevant information.<br />
The <code>alt</code> attribute is often misused as a place to store SEO keywords or copyright information. Here I turned it around and put all the useless information in the image.</p>
<pre><code class="language-html"><figure>
<img
src="hetschip.jpg"
alt="A long rectangular building. Besides looking like a ship, Het Schip resembles a bizarre art form. Its appearance is unconventional from all angles. The exterior consists of bright orange bricks, decked with towers and architectural elements in unconventional shapes."
/>
<figcaption>
Dutch expressionism (Amsterdam School), Het Schip apartment building in
Amsterdam, 1917–20 (Michel de Klerk)
</figcaption>
</figure></code></pre>
<h4>Further reading</h4>
<ul>
<li><a href="https://www.smashingmagazine.com/2020/05/accessible-images/">Accessible Images For When They Matter Most</a></li>
</ul>
<h3>Empty Link</h3>
<p>It’s common for screen reader users to encounter empty links and buttons. Their screen reader might tell them they’re on a link, but if there’s no text, they don’t know where it will take them.</p>
<p>By using <code>aria-label</code> I made the label accessible to screen readers only.</p>
<pre><code class="language-html"><a href="https://" class="btn" aria-label="Het Schip on wikipedia"></a></code></pre>
<p><code>aria-label</code> is often used to provide labels for icons. I’m not a fan of conveying information with icons only because the lack of text my increase cognitive load by requiring mental processing to infer meaning.</p>
<h4>Further reading</h4>
<ul>
<li><a href="https://www.nngroup.com/articles/icon-usability/">Icon Usability</a></li>
<li><a href="https://jonyablonski.com/articles/2015/design-principles-for-reducing-cognitive-load/">Design Principles for Reducing Cognitive Load</a></li>
</ul>
<h3>Wrong order</h3>
<p>Screen readers announce content in the order they appear in the <abbr title="Document Object Model">DOM</abbr>, top to bottom. Changing the order in CSS has no effect on the order in the document. It’s hard to navigate and understand relations between elements if they appear in an incomprehensible order.</p>
<pre><code class="language-html"><p aria-hidden="true">
Brick Expressionism is a this movement special variant of The Netherlands and
northern Germany in western and in.
</p>
<p class="u-vh">
Brick Expressionism is a special variant of this movement in western and
northern Germany and in The Netherlands.
</p></code></pre>
<h4>Further reading</h4>
<ul>
<li><a href="https://tink.uk/flexbox-the-keyboard-navigation-disconnect/">Flexbox & the keyboard navigation disconnect</a></li>
<li><a href="https://adrianroselli.com/2015/10/html-source-order-vs-css-display-order.html">HTML Source Order vs CSS Display Order</a></li>
</ul>
<h3>Hidden content</h3>
<p>Developers came up with a nice workaround for situations when it's just too hard to make parts of a page accessible, they simply add <code>aria-hidden="true"</code> to an element and hide it from screen readers. If it doesn't exist, it doesn't have to be accessible. 🤙 *</p>
<pre><code class="language-html"><p style="filter: invert(1);">The term “Expressionist architecture…</p></code></pre>
<p>Note: I tried using <code>color: #FFFFFF;</code> and <code>opacity: 0;</code> to hide the text visually but axe is too smart. I was able to trick that tool by using <code>filter: invert(1)</code>.</p>
<p>* That was sarcasm. You know, just in case…</p>
<h4>Further reading</h4>
<ul>
<li><a href="https://developer.paciellogroup.com/blog/2012/05/html5-accessibility-chops-hidden-and-aria-hidden/">HTML5 Accessibility Chops: hidden and aria-hidden</a></li>
</ul>
<h3>Click here</h3>
<p>Screen readers can do much more than just announce content in a page, they provide different ways of navigation. You can jump from landmark to landmark or heading to heading, or you can list all form items or links on a page. If these elements don’t have a meaningful label, it’s hard, or sometimes even impossible, to tell what their purpose is, just like a paragraph where every link has the same label.</p>
<pre><code class="language-html"><p>
Important events in Expressionist architecture include the
<a
href="/wiki/Werkbund_Exhibition_(1914)"
aria-label="Werkbund Exhibition (1914)"
>
click here
</a>
in…
</p></code></pre>
<h4>Further reading</h4>
<ul>
<li><a href="https://webaim.org/techniques/hypertext/">Links and Hypertext</a></li>
</ul>
<h3>Excessive nesting</h3>
<p>The structure of a page should be clear and simple. No one benefits from 8 nested <code>article</code>s or <code>ul</code>s.</p>
<pre><code class="language-html"><ul aria-hidden="true">
<li>
The style was
<ul>
<li>
characterised by an
<ul>
<li>
early-modernist adoption
<ul>
<li>
of novel materials,
<ul>
<li>
formal innovation,
<ul>
<li>
and very unusual
<ul>
<li>
massing
<ul>
<li>.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul></code></pre>
<h4>Further reading</h4>
<ul>
<li><a href="https://www.htmhell.dev/10-section-is-no-replacement-for-div/">#10 <section> is no replacement for <div></a></li>
</ul>
<h3>Forms</h3>
<p>Creating accessible and usable forms is hard, but there are a few basics that we just have to get right.</p>
<ul>
<li>Every form item needs a label</li>
<li>Labels should consist of text and not just of images or icons</li>
<li>Labels and form items should be grouped in a way that both machines and users understand their relation.</li>
</ul>
<p>I made the proper label “bad” only accessible to screen reader users by hiding it visually and I disabled clicking by adding <code>pointer-events: none;</code> to the label.</p>
<pre><code class="language-html"><p>
<input type="radio" id="rating2" name="rating" />
<label for="rating2" style="pointer-events: none">
<span class="vh">bad</span>
<span aria-hidden="true">Circled white star Circled white star</span>
</label>
</p></code></pre>
<p>There’s more than that. I can recommend <a href="https://www.smashingmagazine.com/printed-books/form-design-patterns/">Form Design Patterns</a> by Adam Silver, if you want to learn how to make inclusive forms.</p>
<h3>“Buttons”</h3>
<p>A common accessibility issue is the presence of elements on a page that look like buttons, but aren’t actually <code><button></code>s. This usually makes them less accessible or inaccessible to some users. Here I turned it around: It’s a real button, it looks like a button, but you can’t click it.</p>
<pre><code class="language-html"><button
style="pointer-events: none"
type="submit"
class="btn"
onclick='alert("Thank you for your rating");'
>
Send
</button></code></pre>
<h4>Further watching</h4>
<ul>
<li><a href="https://www.youtube.com/watch?v=CZGqnp06DnI">Just use button -- A11ycasts #05</a></li>
</ul>
<h2>Conclusion</h2>
<p>That was a fun experiment and I hope that you had joy reading this article. The underlying issue, though, is no fun at all. That’s just my way of dealing with serious topics.</p>
<p>Your website, app, or new feature is only half as good if only some people can access it. Consider inclusion and diversity from the very beginning and test properly. A score of 100 in Lighthouse or 0 errors in axe doesn’t mean that you’re done, it means that you’re ready to start manual testing and testing with real users, if possible.<br />
Before just you build and launch something, think about your users first and how your decisions might affect them.</p>
<p>Here’s the accessible version of the <a href="https://codepen.io/matuzo/debug/qBbrMgW?editors=1100">page about expressionist architecture</a>.</p>
<p>Thank you, <a href="https://cariefisher.com/">Carie Fisher</a>, for helping me with this article!</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CAccessible+to+some%E2%80%9D">blog@matuzo.at</a>.</p> Writing even more CSS with Accessibility in Mind, Part 1: Progressive Enhancement2020-09-09T00:00:00+00:00https://www.matuzo.at/blog/writing-even-more-css-with-accessibility-in-mind-progressive-enhancement
<p>About 4 years ago, <a href="https://alistapart.com/article/my-accessibility-journey-what-ive-learned-so-far/">I began to focus on web accessibility</a> professionally. I read many articles and books, watched talks, followed experts, and I also shared my knowledge at meet-ups and online. The first 3 articles I wrote were <a href="https://medium.com/alistapart/writing-html-with-accessibility-in-mind-a62026493412">Writing HTML with Accessibility in Mind</a>, <a href="https://medium.com/@matuzo/writing-javascript-with-accessibility-in-mind-a1f6a5f467b9">Writing JavaScript with Accessibility in Mind</a>, and <a href="https://medium.com/@matuzo/writing-css-with-accessibility-in-mind-8514a0007939">Writing CSS with Accessibility in Mind</a>. I've shared the most exciting new things I've learned about creating inclusive experiences in each language.</p><p>I wrote <em>Writing CSS with Accessibility in Mind</em> in 2017 and I’ve covered topics like font size, line height, print style sheets, hiding content, contrast, DOM order vs. visual order and focus styles. 3 years have passed, CSS has evolved, and I’ve learned new things. Therefore, I’ve decided to write about <em>even more CSS with accessibility in mind</em>.</p>
<h2>In this series</h2>
<p>This series of articles covers 4 major topics:</p>
<ol>
<li><strong>Progressive enhancement <em>(this article)</em></strong></li>
<li>User preferences <em>(coming soon)</em></li>
<li>CSS and semantics <em>(coming soon)</em></li>
<li>Improving accessibility with CSS <em>(coming soon)</em></li>
</ol>
<h2>Progressive enhancement basics</h2>
<style>
.css2-pe-l-h {
color: #d25632;
font-weight:bold
}
.css2-pe-l-c {
color: #1b73ba;
font-weight:bold
}
.css2-pe-l-j {
color: #9b8800;
font-weight:bold
}
</style>
<p>I strongly believe in progressive enhancement because it focuses on content and enhances experiences layer by layer. We start with a basic but resilient foundation that works in most browsers: a well structured and semantic <span class="css2-pe-l-h">HTML document</span>. We enhance it with design and visual improvements by adding <span class="css2-pe-l-c">CSS</span>. If the browser doesn’t support CSS at all or just some properties, the site is still accessible, thanks to our strong foundation. Finally, we may add <span class="css2-pe-l-j">JavaScript</span> to enhance the experience even more. We should <a href="https://v8.dev/blog/cost-of-javascript-2019">be careful with JavaScript</a> because it can affect performance negatively, especially on mobile devices.</p>
<style>
.css2-pe {
font-family: sans-serif;
line-height: 1.5;
}
.css2-pe ol {
margin: 0;
padding: 19.2rem 0 0 3.2rem;
position: relative;
transform: rotate(0);
font-weight: bold;
}
@media (min-width: 768px) {
.css2-pe ol {
padding: 2.4rem 0 0 14.4rem;
}
}
.css2-pe li {
opacity: 0.8;
margin-bottom: 0;
position: relative;
}
.css2-pe li > * {
padding-bottom: 1rem !important;
display: inline-block;
font-weight: normal;
}
li::before {
display: block;
content: "";
width: 11rem;
height: 11rem;
position: fixed;
left: 0;
top: 0;
border-radius: 50%;
opacity: 0.1;
transition: opacity 0.3s;
}
li.css2-pe-html,
li.css2-pe-html span {
color: #d25632;
opacity: 1
}
.css2-pe-html::before {
background: #d25632;
width: 4.2rem;
height: 4.2rem;
z-index: 3;
top: 3.4rem;
left: 3.4rem;
opacity: 1;
}
.css2-pe-css::before {
background: #1b73ba;
width: 7.8rem;
height: 7.8rem;
z-index: 2;
top: 1.6rem;
left: 1.6rem;
}
.css2-pe-html {
z-index: 3;
}
.css2-pe-css,
.css2-pe-css span {
color:#1b73ba;
z-index: 2;
}
.css2-pe-js,
.css2-pe-js span {
color: #9b8800;
z-index: 1;
}
.css2-pe-js::before {
background: #f3df4f;
}
.css2-pe-html span,
.css2-pe--active span {
font-weight: bold;
}
.css2-pe--active,
.css2-pe--active::before {
opacity: 1;
cursor: default;
}
s {
text-decoration-color: black;
}
</style>
<div class="css2-pe js-css2-pe">
<ol>
<li class="css2-pe-html"><span>HTML - Semantic markup</span></li>
<li class="css2-pe-css"><span>CSS - Design and visual improvements</span></li>
<li class="css2-pe-js"><span>JS - Enhanceed experience</span></li>
</ol>
</div>
<script>
var css2pe = document.querySelector('.js-css2-pe').querySelectorAll('li');
var peHighlight = function (elem, e) {
for (var j = 0; j < css2pe.length; j++) {
if (j <= Array.prototype.slice.call(css2pe).indexOf(elem)) {
css2pe[j].classList.add('css2-pe--active')
}
}
}
var peUnHighlight = function (e) {
for (var j = 0; j < css2pe.length; j++) {
css2pe[j].classList.remove('css2-pe--active')
}
}
for (var i = 0; i < css2pe.length; i++) {
(function() {
var elem = css2pe[i];
if (i > 0) {
var span = elem.querySelector('span')
span.setAttribute('tabindex',0)
var peH = peHighlight.bind(null, elem)
var peUH = peUnHighlight.bind(null, elem)
span.addEventListener('mouseenter', peH);
span.addEventListener('focus', peH);
span.addEventListener('mouseleave', peUH);
span.addEventListener('blur', peUH);
}
})(i)
}
</script>
<h3>Example</h3>
<p>Let’s take a simple form that allows users to enable tracking.</p>
<style>
.css2-pe-toggle button {
background: #36b1bf;
font-weight: bold;
outline: none;
padding: 1.8rem 3.125rem 1.5rem;
font-family: inherit;
line-height: 1;
border: none;
font-size: 1.8rem;
margin-top: 1rem;
}
.css2-pe-toggle form {
margin-bottom: 3rem;
}
.css2-pe-toggle-css {
padding-left: 7.5rem;
position: relative;
display: block;
margin-bottom: 1rem;
}
.css2-pe-toggle-css::before,
.css2-pe-toggle-css::after {
position: absolute;
left: 0;
top: 0;
content: "";
display: inline-block;
transition: transform 0.3s;
height: 2.7rem;
}
.css2-pe-toggle-css::before {
width: 6rem;
border: 2px solid #aaa;
border-radius: 4px;
}
.css2-pe-toggle-css::after {
width: 3rem;
background: #aaa;
left: 0.1rem;
top: 0.1rem;
}
@media (min-width: 768px) {
.css2-pe-toggle-css::before {
top: 0.4rem;
}
.css2-pe-toggle-css::after {
top: 0.5rem;
height: 2.8rem;
}
}
[type="checkbox"]:focus + .css2-pe-toggle-css::before {
border-color: #1c4e6c;
outline-offset: 2px;
outline: 2px solid #f23c50;
}
[type="checkbox"]:checked + .css2-pe-toggle-css::before {
border-color: #1c4e6c;
}
[type="checkbox"]:checked + .css2-pe-toggle-css::after {
border-color: #1c4e6c;
background-color: #1c4e6c;
transform: translateX(3rem);
}
</style>
<div class="css2-pe-toggle">
<h4 class="css2-pe-l-h">HTML</h4>
We want to make sure that the only real dependency is core HTML.
<pre><code class="language-html"><form>
<input type="checkbox" id="tracking" class="u-vh" />
<label for="tracking">Turn on tracking.</label>
<button type="submit">Save settings</button>
</form></code></pre>
<form>
<input type="checkbox" id="tracking2">
<label for="tracking2">Turn on tracking.</label>
<button type="submit">Save settings</button>
</form>
<p>The button does nothing here. Let’s just assume that there’s a server-side script that saves the settings.</p>
<h4 class="css2-pe-l-c">CSS</h4>
<p>Our checkbox looks a bit boring, let’s add another layer. We can improve the design with CSS. We add a class (<code>.u-vh</code>) to hide the checkbox visually and use pseudo elements to create something that looks like a switch toggle.</p>
<form>
<input type="checkbox" id="tracking" class="u-vh" style="top: auto">
<label for="tracking" class="css2-pe-toggle-css">Turn on tracking.</label>
<button type="submit">Save settings</button>
</form>
<pre><code class="language-css">.label {
padding-left: 7.5rem;
position: relative;
}
.label::before,
.label::after {
position: absolute;
left: 0;
top: 0;
content: '';
display: inline-block;
transition: transform 0.3s;
height: 2.6rem;
}
.label::before {
width: 6rem;
border: 2px solid #aaa;
border-radius: 4px;
}
.label::after {
width: 3rem;
background: #aaa;
left: 0.2rem;
top: 0.2rem;
}
[type='checkbox']:focus + .label::before {
border-color: #1c4e6c;
outline-offset: 2px;
outline: 2px solid #f23c50;
}
[type='checkbox']:checked + .label::before {
border-color: #1c4e6c;
}
[type='checkbox']:checked + label::after {
border-color: #1c4e6c;
background-color: #1c4e6c;
transform: translateX(3rem);
}
.u-vh {
position: absolute;
white-space: nowrap;
width: 1px;
height: 1px;
overflow: hidden;
border: 0;
padding: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
margin: -1px;
}</code></pre>
<h4 class="css2-pe-l-j">JS</h4>
<p>Now we can enhance the form some more and save settings as the user clicks the checkbox so that their settings persist even when the page crashes or they close the window accidentally.</p>
<pre><code class="language-js">document.querySelector('#tracking').addEventListener('change', function (e) {
// Save in database or in a local storage, etc..
alert('Saved: ' + e.target.checked);
});</code></pre>
<form>
<input type="checkbox" id="tracking3" class="u-vh" style="top: auto">
<label for="tracking3" class="css2-pe-toggle-css">Turn on tracking.</label>
<button type="submit" class="css2-pe-toggle-btn">Save settings</button>
</form>
<script>
document.getElementById('tracking3').addEventListener('change', function(e) {
alert('Save: ' + e.target.checked)
})
</script>
<p><strong>Please note that this switch toggle is not accessible</strong> because it doesn’t communicate state well enough. I just built it that way to illustrate progressive enhancement. If you want to learn how to create accessible toggle buttons, read <a href="https://inclusive-components.design/toggle-button/">Toggle Buttons</a> by Heydon Pickering.</p>
</div>
<p>We've started with a basic but resilient foundation that works in most browsers and we've enhanced it feature by feature. Instead of loading multiple megabytes of polyfills, compiled JavaScript and CSS workarounds onto users, we only give browsers code they can handle without additional help. This usually results in less JavaScript and CSS, better performance and happier users. Progressive enhancement is the key to giving more people access to our content by serving code according to the capabilities of the end user’s browser and device.</p>
<p>I’ve been following the basics of this principle for many years, but only recently I discovered how I can bring my practices up to date and use progressive enhancement together with modern CSS and JavaScript.</p>
<h3>Rediscovering Progressive Enhancement</h3>
<p>Nokia released an updated version of its iconic Nokia 3310 a few years ago. I bought it because it was affordable and I wanted to see how the surfing experience was in Opera Mini. You can download Opera Mini to your Android or iOS phone, too, but by default there isn’t much of a difference between Opera Mini and the default browser on these devices. Opera Mini on the Nokia 3310 runs on an operating system called Nokia Series 30+ and its function range is quite limited. If you search for almost any feature on <a href="http://caniuse.com/">caniuse.com</a> and you see a red rectangle, that’s the Opera Mini we’re talking about. I’ve tested a website I’ve recently built using modern CSS and JS on the Nokia 3310 and after some minor tweaks it worked. Just like that. Guess why! Exactly, progressive enhancement.<br />
The fact that JavaScript is just another layer and not a dependency allows users with low-end devices to access the website.</p>
<div class="css2-pe">
<ol>
<li class="css2-pe-html"><span>HTML - Semantic markup</span></li>
<li class="css2-pe-css css2-pe--active"><span>CSS - Design and visual improvements</span></li>
<li class="css2-pe-js"><span><s>JS - Enhanceed experience</s></span></li>
</ol>
</div>
<p>You can read more about the process in <a href="https://www.matuzo.at/blog/beauty-of-progressive-enhancement/">The beauty of progressive enhancement</a>.</p>
<p>HTML, CSS and JS are large layers in our progressively enhanced website, but each layer may comprise even more layers.</p>
<h2>Progressively enhancing CSS</h2>
<p>There are different ways in CSS to define layers and help browsers serve them accordingly.</p>
<h3>Let CSS do its thing</h3>
<p>CSS has progressive enhancement at its core. This is best illustrated by its error handling: <a href="https://www.w3.org/TR/css-syntax-3/#error-handling">When errors occur in CSS</a>, the parser doesn’t stop, but it attempts to only skip content it can’t interpret before returning to parsing as normal.</p>
<p>The following code won’t throw any errors, the CSS parser will just skip the line it doesn’t understand and apply a <code>#153a51</code> color and <code>#36b1bf</code> background color to all <code>div</code> elements.</p>
<pre><code class="language-css">div {
color: #153a51;
css-is: amazing;
background: #36b1bf;
}</code></pre>
<style>
.css2-error {
color: #153a51;
background: #36b1bf;
width: 10rem;
height: 10rem;
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
}
</style>
<div class="css2-error">
CSS <3
</div>
<p>The parser skips the second line in the declaration block because the property <code>css-is</code> doesn’t exist, but errors aren’t always mistakes. A browser like Firefox might interpret a new property without issues, while the same property looks like an error in Internet Explorer. For example: If you add the following rule to a style sheet, modern browsers apply a grid with 2 <code>150px</code> columns to each unordered list, while browsers that don’t support CSS Grid Layout just skip the Grid declarations.</p>
<pre><code class="language-css">ul {
display: flex; /* Fallback for older browsers */
flex-wrap: wrap; /* Allow items to wrap */
display: grid; /* Most modern browsers */
grid-template-columns: repeat(
auto-fill,
150px
); /* Add as many 150px columns per line as possible */
}
ul > * {
border: 1px solid #36b1bf;
margin: 0 0.5rem;
}</code></pre>
<p>A modern browser first sets the <code>display</code> property to <code>flex</code> and allows wrapping of flex items, then it overwrites the first declaration and sets the <code>display</code> to <code>grid</code> and forgets about wrapping again because <code>flex-wrap</code> doesn’t work with grid items. Last, it adds grid columns with its own wrapping mechanism.</p>
<style>
.css2-listgrid {
display: flex;
flex-wrap: wrap;
list-style: none;
padding: 0;
margin: 0;
}
.css2-listgrid--grid {
display: grid;
grid-template-columns: repeat(auto-fill, 150px);
}
.css2-listgrid > * {
border: 1px solid #36b1bf;
margin-bottom: 0 !important;
margin: 0 0.5rem;
}
</style>
<ul class="css2-listgrid css2-listgrid--grid">
<li>Element 1</li>
<li>Element 2</li>
</ul>
<p>An older browser sets <code>display</code> to <code>flex</code> and allows wrapping of flex items and skips the rest.<br />
The list looks a little different, but most users probably won’t even notice.</p>
<ul class="css2-listgrid">
<li>Element 1</li>
<li>Element 2</li>
</ul>
<p><strong>Here’s another example:</strong><br />
We can use <code>shape-outside</code> to make a paragraph that wraps an image look more interesting.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1200/v1599629649/articles/Screen_Shot_2020-08-15_at_13.22.43.png" alt="A photo of a dog from the side looking up wearing a red party hat with white dots and text that wraps around the shape of the dogs head and body." /></p>
<noscript><img class="no-script" src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1200/v1599629649/articles/Screen_Shot_2020-08-15_at_13.22.43.png" alt="A photo of a dog from the side looking up wearing a red party hat with white dots and text that wraps around the shape of the dogs head and body."></noscript>
<pre><code class="language-html"><img
src="dog.jpg"
width="400"
alt="A dog from the side looking up wearing a red party hat with white dots"
/>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Deserunt…</p></code></pre>
<pre><code class="language-css">img {
shape-outside: polygon(
0.23% 2px,
17.11% 0.84%,
61.14% 21.01%,
69.91% 20.17%,
86.88% 27.73%,
90.64% 36.09%,
86.53% 50.56%,
80.07% 79.29%,
86.55% 99.48%,
0px 100%
);
shape-margin: 20px;
float: left;
display: inline-block;
}</code></pre>
<p>Most browsers support the <code>shape-outside</code> and <code>shape-margin</code> properties. Edge < 18, Internet Explorer, and Opera Mini are an exception. This is what users of these browsers will get.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1200/v1599629648/articles/Screen_Shot_2020-08-15_at_13.22.31.png" alt="A photo of a dog from the side looking up wearing a red party hat with white dots and text that wraps around the photo." /></p>
<noscript><img class="no-script" src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1200/v1599629648/articles/Screen_Shot_2020-08-15_at_13.22.31.png" alt="A photo of a dog from the side looking up wearing a red party hat with white dots and text that wraps around the photo."></noscript>
<p>It still looks nice and it’s accessible, just not as fancy. Check out <a href="https://codepen.io/matuzo/pen/yEYyOB?editors=1100">CSS Shapes Demo / shape-outside</a> on CodePen.</p>
<h3>Cutting the mustard</h3>
<p>If people visit our websites in browsers like <abbr title="Internet Explorer">IE</abbr> 11 or Opera Mini, it’s fair to assume that they’re not using a high-end device. I like to make sure that they only get as much code as they need to access the content and still get a decent experience. Anything extra will only be served to more capable browsers and potentially faster devices. This means I have to decide at which point I want to draw the line between full and limited functionality for certain features. There are different ways of doing that.</p>
<p><strong>Let CSS do its thing 2</strong></p>
<p>Again, we can let CSS do its thing. For example, if we don’t want to bother users of legacy browsers with having to download hundreds of kilobytes in font files, we can serve our fonts only in the <code>woff2</code> format (<a href="https://caniuse.com/#search=woff2">see .woff2 support on caniuse.com</a>).</p>
<pre><code class="language-css">@font-face {
font-family: 'Lobster';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Lobster'), url('fonts/Lobster-Regular.woff2') format('woff2');
}
body {
font-family: Lobster, sans-serif;
}</code></pre>
<p>Since a browser like Internet Explorer doesn’t support <code>woff2</code> it won’t try to download the file and it will use the <code>sans-serif</code> fallback font. (Instead of just relying on the generic font family as a fallback, you could also use a system font similar in style and shape).</p>
<p><strong>Feature detection in CSS</strong></p>
<p>Another approach, often referred to as “<a href="https://responsivenews.co.uk/post/18948466399/cutting-the-mustard">cutting the mustard</a>”, is to check whether a browser supports a certain feature and only then, if it cuts the mustard, serve additional code.</p>
<p>We could make a basic vertical layout and enhance it only if the browser supports custom properties. Feature detection is built into CSS in the form of the <code>@supports</code> at-rule <abbr title="also knows as">aka</abbr> <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@supports">feature queries</a>.</p>
<pre><code class="language-css">:root {
--display: flex;
--gap: 1rem;
}
/* Applied in all browsers */
ul {
margin: 0;
padding: 0;
list-style: none;
}
/* Only browsers that support custom properties */
@supports (display: var(--supports)) {
ul {
display: var(--display);
gap: var(--gap);
flex-wrap: wrap;
}
}</code></pre>
<p><a href="https://caniuse.com/#feat=css-featurequeries">Most browsers support feature queries</a>.</p>
<p><strong>Note:</strong> It’s not possible to check support for a property only, you have to provide a value for the property. Something like <code>@supports (display) {}</code> won’t work.</p>
<p><strong>Feature detection in JS</strong></p>
<p>I've built a large website recently with many components, some of them enhanced with JavaScript. Each component works with and without JavaScript. This is important because we only serve a critical amount of JavaScript to users of legacy browsers, which means that they will see most components in their no-JS state.</p>
<p>We’re cutting the mustard by adding this block of Javascript to the <code><head></code> of our site.</p>
<pre><code class="language-html"><head>
<script type="module">
// Add the `.js` class to the <html> element
document.documentElement.classList.add('js');
</script>
</head></code></pre>
<p>The <code>type="module"</code> attribute and value ensures that the scripts block will be only executed, if the <a href="https://caniuse.com/#feat=es6-module">browsers supports JavaScript modules</a>. What’s great about this is that even with the attribute in place we don’t actually have to use JavaScript modules, we can write our JavaScript as usual.</p>
<p>If the <code><html></code> element contains the class <code>js</code>, we know that it’s a modern browser because it supports JavaScript modules. This allows us to style components accordingly.</p>
<p>In an accordion component, for example, the content is visible by default.</p>
<pre><code class="language-html"><body>
<div class="accordion">
<h3 class="accordion__heading">Accordion Heading</h3>
<div class="accordion__panel">
<p>
Accordion panel content visibile by default and only hidden, if the
`.js` class is present.
</p>
</div>
</div>
</body></code></pre>
<style>
.css2-pe-example {
width: 85%;
padding: 2rem;
border: 5px solid #36b1bf;
margin: 0 auto;
}
.css2-pe-example .accordion__heading {
margin-top: 0;
}
.css2-pe-example .accordion__panel--js {
display: none;
}
.css2-pe-example .accordion__panel--visible {
display: block;
}
.accordion__heading button {
appearance: none;
border: none;
padding: none;
font-size: inherit;
font-family: inherit;
padding: 0;
color: inherit;
font-weight: inherit;
}
</style>
<div>
<div class="css2-pe-example">
<div class="accordion">
<h3 class="accordion__heading">Accordion Heading</h3>
<div class="accordion__panel">
<p>Accordion panel content visibile by default and only hidden, if the `.js` class is present.</p>
</div>
</div>
</div>
</div>
<p>We hide the content (<code>.accordion__panel</code>) and add attributes, events, etc. only if the browsers cuts the mustard.</p>
<pre><code class="language-css">.js .accordion__panel {
display: none;
}
.js .accordion__panel--visible {
display: block;
}
.accordion__heading button {
appearance: none;
border: none;
padding: none;
font-size: inherit;
font-family: inherit;
padding: 0;
color: inherit;
font-weight: inherit;
}</code></pre>
<p>We replace the text content within the heading with a button and add a click event to the button that toggles the visibility of the panel.</p>
<pre><code class="language-js">// Select elements
const accordion = document.querySelector('.js-accordion');
const heading = accordion.querySelector('h3');
const panel = accordion.querySelector('div');
const btn = document.createElement('button');
// The panel is hidden by default, so set aria-expanded to false
btn.setAttribute('aria-expanded', false);
// Associate the button with the panel (works only with some screen readers)
btn.setAttribute('aria-controls', 'panel_1');
panel.id = 'panel_1';
// Event that toggles aria-expanded and the panel visibility
btn.addEventListener('click', (e) => {
const state = btn.getAttribute('aria-expanded') === 'true' ? false : true;
btn.setAttribute('aria-expanded', state);
panel.classList.toggle('accordion__panel--visible');
});
// Replace text in heading with the button
btn.textContent = heading.textContent;
heading.textContent = '';
heading.appendChild(btn);</code></pre>
<div>
<div class="css2-pe-example">
<div class="accordion js-accordion">
<h3 class="accordion__heading">Accordion Heading</h3>
<div class="accordion__panel accordion__panel--js">
<p>Accordion panel content visibile by default and only hidden, if the `.js` class is present.</p>
</div>
</div>
</div>
</div>
<script>
var accordion = document.querySelector('.js-accordion')
var heading = accordion.querySelector('h3')
var panel = accordion.querySelector('div')
var btn = document.createElement('button');
btn.setAttribute('aria-expanded', false)
btn.setAttribute('aria-controls', 'panel_1')
btn.id = 'acc_1'
btn.textContent = heading.textContent
btn.addEventListener('click', (e) => {
var state = btn.getAttribute('aria-expanded') === 'true' ? false : true;
btn.setAttribute('aria-expanded', state)
console.log(btn.getAttribute('aria-expanded'))
panel.classList.toggle('accordion__panel--visible')
})
heading.textContent = ''
heading.appendChild(btn)
panel.id = 'panel_1'
</script>
<p>What’s great about this approach is that we can use <abbr title="ECMAScript">ES6</abbr> without having to compile it with Babel because a browser that supports JS Modules also support ES6 syntax. No JavaScript for users of legacy browsers and less JS for everyone else 🎉.</p>
<pre><code class="language-html"><script src="accordion.js" type="module"></script></code></pre>
<p><strong>Note:</strong> This accordion is not complete. Check-out <a href="https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html">Accordion Example | WAI-ARIA Authoring Practices 1.1</a> for a fully accessible and functional example.</p>
<h3>Conclusion</h3>
<p>Progressive enhancement is amazing. Building websites layer by layer allows for a cleaner separation of concerns, which makes the website more accessible. If one layer doesn't work in a specific browser, it doesn't matter because the layers below will make sure that users can still access our content.</p>
<p>Improve the experience of your users by making Progressive Enhancement to one of your core principles. You can learn more about it in the following articles:</p>
<h3>Resources</h3>
<ul>
<li><a href="https://alistapart.com/article/understandingprogressiveenhancement/">Understanding Progressive Enhancement</a> by Aaron Gustafson</li>
<li><a href="https://resilientwebdesign.com/">Resilient web design</a> by Jeremy Keith</li>
<li><a href="https://www.matuzo.at/blog/beauty-of-progressive-enhancement/">The beauty of progressive enhancement</a></li>
<li><a href="https://fettblog.eu/cutting-the-mustard-2018/">Cutting the mustard - 2018 edition</a> by Stefan Baumgartner</li>
</ul>
<h3>Recording</h3>
<p>If you want to learn more about CSS and accessibility and you don’t want to wait for me to publish the other articles in this series, you can watch my talk about writing CSS with accessibility in mind at #ID24:</p>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/o6ssu5oKyaU" title="Writing even more CSS with Accessibility in Mind" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWriting+even+more+CSS+with+Accessibility+in+Mind%2C+Part+1%3A+Progressive+Enhancement%E2%80%9D">blog@matuzo.at</a>.</p> Writing even more CSS with Accessibility in Mind, Part 2: Respecting user preferences2020-10-12T00:00:00+00:00https://www.matuzo.at/blog/writing-even-more-css-with-accessibility-in-mind-user-preferences
<p>In the first article of this series, I explained how important progressive enhancement is for web accessibility. Building websites layer by layer allows for a cleaner separation of concerns and more resilient experiences. This second article is about user preferences and how to respect them when writing CSS.</p><h2>In this series</h2>
<p>This series of articles covers 4 major topics:</p>
<ol>
<li><a href="/blog/writing-even-more-css-with-accessibility-in-mind-progressive-enhancement/">Progressive enhancement</a></li>
<li><strong>User preferences <em>(this article)</em></strong></li>
<li>CSS and semantics <em>(coming soon)</em></li>
<li>Improving accessibility with CSS <em>(coming soon)</em></li>
</ol>
<h2>Respecting user preferences</h2>
<p>Operating systems and browsers provide users with options to customize their browsing experience, and it’s our job to respect these preferences in our style sheets. In the following chapter, I’ll give you examples of how we can build designs according to our ideas while still respecting user preferences.</p>
<h3>Font size</h3>
<p>A fundamental thing we should do is respect our users’ preferred font size for running text.</p>
<h4>Base font size</h4>
<p>The default font size in most browsers is <code>16px</code> but it can be changed to a lower or higher value in the browser preferences.</p>
<figure class="figure">
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1000/v1602476047/articles/ff_settings.jpg" alt="Language and Appearance settings in Firefox. Font site dropdown active. Screenshot.">
<figcaption>
In Firefox: Firefox → Preferences → Language and Appearance
</figcaption>
</figure>
<p>If an user sets the default font size to <code>20px</code>, we should make sure that the size of running text doesn’t get lower than that on our website. We can do that by using relative units instead of absolute units. The problem with absolute units is that if we set the font size to <code>18px</code> on the <code><body></code>, it overwrites the <code>20px</code> our user has chosen as their default, and that wouldn’t be nice of us.</p>
<pre><code class="language-css">body {
/* Don’t use px for font sizes (...in most cases) */
font-size: 18px;
}</code></pre>
<h5>Video demo:</h5>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" height="315" title="Accessible CSS demo: font-size property with an absolute value" src="https://www.youtube.com/embed/_leBu4I1yqc" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div>
<br>
<p>If we use a relative unit like <code>rem</code>, instead, we can still define our preferred size while respecting user preferences. <code>1rem</code> is relative to the root font size, as already mentioned, <code>16px</code> in most browsers. This means that the the size of <code>1rem</code> changes with the default font size in the browser.</p>
<p>If we want to use this unit, we have to convert <code>px</code> to <code>rem</code>. We can do that by taking the target value (<code>18px</code>) and dividing it by the default root value (<code>16px</code>):</p>
<div style="text-align: center">
<img class="234" src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_350/v1602477333/articles/article_css2_calc.png" alt="18px/16px = 1.125rem">
</div>
<p>The result is the <code>rem</code> value. If we use it instead of the absolute pixel value, the font size for users with standard settings will still equal <code>18px</code>, but the font size for users who prefer a larger base font size like <code>20px</code> will be <code>22.5px</code> (1rem = 20px, 1.125rem = 22.5px).</p>
<pre><code class="language-css">body {
font-size: 1.125rem; /* 16 * 1.125 = 18px */
}</code></pre>
<h5>Video demo:</h5>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" height="315" title="Accessible CSS demo: font-size property with a relative value" src="https://www.youtube.com/embed/vE_0JWb9iOA" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div>
<br>
<p>It makes sense to use relative units like <code>rem</code>, <code>em</code>, or <code>%</code> because they’re relative to a parents font size when used with the <code>font-size</code> property. Other relative units like <code>vh</code> or <code>vw</code> are not suitable because they’re relative to the viewport. Even combinations of units like <code>calc(2vw + 0.5rem)</code>should be treated with caution because they might lead to unexpected results. Read <a href="https://adrianroselli.com/2019/12/responsive-type-and-zoom.html">Responsive Type and Zoom</a> by Adrian Roselli for details.</p>
<p><strong>Line lengths</strong></p>
<p>I like to constrain the width of content container elements to improve readability by ensuring that lines don’t exceed a certain length. I use the <code>ch</code> (character) unit for that. <code>1ch</code> is as wide as the glyph 0 (zero) in the respective font and size. If we set the <code>max-width</code> to <code>80ch</code>, only approximately 80 characters will fit each line. A pleasant side effect is that when users change the default font size, the width of the content container grows with the size of the font. The relative unit <code>ch</code> makes sure that there’s always a pleasant font size to line length ratio, no matter how large the text is.</p>
<pre><code class="language-css">.content {
max-width: 80ch;
}</code></pre>
<p><a href="https://codepen.io/matuzo/pen/RwaZNVa?editors=1100">ch unit Demo on CodePen.</a></p>
<h5>Video demo:</h5>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" height="315" title="Accessible CSS demo: difference between px and ch in max-width declarations." src="https://www.youtube.com/embed/7LR9HnV-j7Q" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div>
<h3>Motion and animation</h3>
<p>Animations on the web are sometimes annoying, but they may also cause nausea, dizziness, and headaches in some users. For people with vestibular disorders it may even cause pain and make them feel so bad that they have to stop using the computer, needing time to recover. Earlier this year I read “<a href="https://alistapart.com/article/accessibility-for-vestibular/">Accessibility for Vestibular Disorders: How My Temporary Disability Changed My Perspective</a>” by Facundo Corradini and it blew my mind because it was the first time that I read about the negative side effects of animations on the web by someone who experienced it first-hand. Facundo is a developer, and he describes which patterns and effects were especially bad and made him feel dizzy and sick.</p>
<blockquote>
<p>Really, there are no words to describe just how bad a simple parallax effect, scrolljacking, or even <code>background-attachment: fixed</code> would make me feel. I would rather jump on one of those 20-G centrifuges astronauts use than look at a website with parallax scrolling.</p>
</blockquote>
<p>He also describes how distracting animations can be when you’re having a hard time focussing itself.</p>
<blockquote>
<p>The extreme, conscious, focused effort it took to read would make it such that anything moving on the screen would instantly break my focus, and force me to start the paragraph all over. And I mean anything.</p>
</blockquote>
<p>I can highly recommend this article, it outlines well how important it is to use animation on the web cautiously. Now let’s see how we can do that.</p>
<p><strong>Reduce or remove motion if users prefer reduced motion</strong></p>
<p><a href="https://developer.paciellogroup.com/blog/2019/05/short-note-on-prefers-reduced-motion-and-puzzled-windows-users/">Some operating systems allow users to reduce motion</a>, and we can react to that in CSS by using the <code>prefers-reduced-motion</code> media feature. If our users prefer less motion on the screen, we should respect that and serve CSS without or with fewer animations and transitions.</p>
<p>Here’s an example: If you don’t have any preference for reduced motion, you should see someone moonwalking from one end of the article to the other. If you do, you should see someone moonwalking in place.</p>
<style>
@keyframes walk {
0% {
transform: translateX(800px);
}
}
@media (prefers-reduced-motion: no-preference) {
.moonwalk img {
animation: walk 10s linear infinite;
}
}
.moonwalk {
overflow: hidden;
}
.moonwalk img { border: none}
</style>
<div class="moonwalk">
<picture>
<source srcset="https://assets.codepen.io/144736/moonwalk.gif" media="(prefers-reduced-motion: no-preference)">
<img class="p" src="https://assets.codepen.io/144736/moonwalk.png" alt="Someone doing the moon walk.">
</picture>
</div>
<p>Inspired by <a href="https://codepen.io/lassediercks/pen/MEmEyj">Marquee Jackson</a> by Lasse Diercks.</p>
<pre><code class="language-html"><img
src="https://media.giphy.com/media/EDZP0UCtxiRQQ/giphy.gif"
alt="Person doing the moon walk."
/></code></pre>
<pre><code class="language-css">/* Define keyframe animation */
@keyframes walk {
0% {
transform: translateX(100vw);
}
}
/* Apply animation */
img {
animation: walk 10s linear infinite;
}
/* Remove animation if the user prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
img {
animation: none;
}
}</code></pre>
<p>We could go one step further and replace the gif with a png by using a combination of the <code>prefers-reduced-motion</code> media feature and the picture element.</p>
<pre><code class="language-html"><picture>
<source srcset="moonwalk.png" media="(prefers-reduced-motion: reduce)" />
<img src="moonwalk.gif" alt="Someone doing the moonwalk" />
</picture></code></pre>
<p><a href="https://codepen.io/matuzo/pen/NWrWeNq?editors=1100">picture element and prefers-reduced-motion demo on CodePen</a></p>
<p>In this example I set the <code>animation</code> property to <code>none</code> , but you don’t always have to remove motion entirely. Eric W. Bailey points out that <a href="https://css-tricks.com/revisiting-prefers-reduced-motion-the-reduced-motion-media-query/#article-header-id-5">animation isn’t bad per se</a>, because it may help users, especially people with cognitive disabilities, understand the relationship between seemingly disparate objects. Val Head outlines even more benefits of animations in “<a href="https://alistapart.com/article/designing-safer-web-animation-for-motion-sensitivity/#section7">Designing Safer Web Animation For Motion Sensitivity</a>” and “<a href="https://www.smashingmagazine.com/2020/09/design-reduced-motion-sensitivities/">Designing With Reduced Motion For Motion Sensitivities</a>”.</p>
<p><strong>Progressive enhancement applied to animation</strong></p>
<p>It’s important to respect user preferences, but this approach, applying animation and conditionally removing it, isn’t perfect. The reduced motion media feature was introduced only a few years ago, which means that <a href="https://caniuse.com/#feat=prefers-reduced-motion">users of legacy browsers will see animations no matter what</a>.</p>
<p>Most articles recommend something like this:</p>
<pre><code class="language-css">/* Apply animations and transitions */
img {
animation: walk 10s linear infinite;
}
div {
transition: transform 1s ease-in;
}
/* Select all elements in the page and reduce or remove motion. */
/* Important note: This has no effect in browsers that don’t support prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}</code></pre>
<p>That’s why <a href="https://twitter.com/patrick_h_lauke">Patrick H. Lauke</a> recommends a <a href="https://codepen.io/patrickhlauke/pen/YzPPdeo">defensive prefers-reduced-motion use</a> where animations only run in browsers that support the media feature and only if they have <em>not</em> expressed a preference for reduced motion.</p>
<pre><code class="language-css">/* has no effect in browsers that don’t support prefers-reduced-motion */
@media (prefers-reduced-motion: no-preference) {
img {
animation: walk 10s linear infinite;
}
div {
transition: transform 1s ease-in;
}
}</code></pre>
<p>I like this approach because it’s more sensitive towards users and it takes progressive enhancement one step further. We start with no animation and only add this layer of enhancement if the browser supports the media feature and the user has no preference for reduced motion.</p>
<p>Here’s an example of how I’ve used the media feature in a website I’ve recently built. We only animate <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior">scrolling to anchor links</a>, if users have no preference for reduced motion.</p>
<pre><code class="language-css">@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}</code></pre>
<p><a href="https://codepen.io/matuzo/pen/bGprNYo">Smooth scrolling demo on CodePen</a></p>
<p>Please respect your user’s motion preferences in your style sheets.</p>
<blockquote>
<p>Using this media query could spare someone from having to unnecessarily endure a tremendous amount of pain for simply having the curiosity to click on a link or scroll down a page.</p>
</blockquote>
<p>- <a href="https://twitter.com/ericwbailey">Eric Bailey</a></p>
<h3>Dark Mode</h3>
<p>Another setting some operating systems provide is the ability to switch the default theme from one that uses light colors to one that uses dark colors (<a href="https://www.a11yproject.com/posts/2020-01-23-operating-system-and-browser-accessibility-display-modes/#toc_Dark-Mode">Dark Mode</a>). That’s especially useful for people who want to reduce eye strain or for people who have certain photosensitive conditions.</p>
<p>The media feature <code>prefers-color-scheme</code> allows us to react to these settings and provide users with a dark or light design accordingly.</p>
<pre><code class="language-css">body {
/* Dark text on light background */
color: #222222;
background-color: #ffffff;
}
@media (prefers-color-scheme: dark) {
body {
/* Light text on dark background */
color: #ffffff;
background-color: #222222;
}
}</code></pre>
<p>I did some research and collected useful tips and tricks for working with Dark Mode.</p>
<h4>1. Change the design according to user preference but allow users to change it back.</h4>
<p>Some users may prefer a dark theme for the OS but light themes on the web. They should be able to pick their preferred theme. You can see how Cassie Evans does it on her website <a href="https://www.cassie.codes/">cassie.codes</a>.</p>
<figure class="figure">
<a href="https://cassie.codes" rel="noopener"><img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,q_100,w_1000/v1602564954/articles/article_css2_cassie.jpg" alt="A styled switch that looks like a sun or moon depending on the mode at the top right corner on Cassie Evans website"></a>
<figcaption>
Comparison of light and dark mode.
</figcaption>
</figure>
<h4>2. Try to avoid hard contrasts like <code>#FFFFFF</code> text color on a <code>#000000</code> background.</h4>
<p>Use softer combinations like <code>#EFEFEF</code> on <code>#111111</code>, which are easier on the eyes.</p>
<style>
.a-up-color {
display: flex;
}
.a-up-color div {
max-width: 150px;
height: 150px;
width: 48%;
background: #000;
color: #fff;
margin-right: 2%;
display: flex;
align-items: center;
text-align: center;
}
.a-up-color div + div {
margin-right: 0;
background: #111111;
color: #EFEFEF;
}
</style>
<div class="a-up-color">
<div>
#FFFFFF on #000000
</div>
<div>
#EFEFEF on #111111
</div>
</div>
<h4>3. <a href="https://markdotto.com/2018/11/05/css-dark-mode/">Dim images</a> by decreasing the <code>opacity</code>.</h4>
<p>Dark backgrounds may amplify the brightness of light images, dimming images can help with that. You can still show the original opacity on hover and focus.</p>
<pre><code class="language-css">img {
opacity: 0.8;
transition: opacity 0.5s ease-in-out;
}
a:hover img,
a:focus img {
opacity: 1;
}</code></pre>
<style>
.a-up-image {
display: flex;
background: #111111;
padding: 20px;
}
.a-up-image a {
box-shadow: none !important;
}
.a-up-image a:link,
.a-up-image a:visited {
box-shadow: none !important;
color: #fefefe;
}
.a-up-image img {
display: block;
}
.a-up-image a + a{
margin-left: 1rem;
}
.a-up-image a:last-child img {
opacity: 0.8;
transition: opacity .5s ease-in-out;
}
.a-up-image a:last-child:hover img,
.a-up-image a:last-child:focus img{
opacity: 1;
}
</style>
<div class="a-up-image">
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1602565671/articles/article_css2_schoenbrunn.jpg">
100% opacity
<img class="a" src="https://res.cloudinary.com/dp3mem7or/image/upload/v1602565671/articles/article_css2_schoenbrunn.jpg" alt="">
</a>
<a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1602565671/articles/article_css2_schoenbrunn.jpg">
80% opacity
<img class="a" src="https://res.cloudinary.com/dp3mem7or/image/upload/v1602565671/articles/article_css2_schoenbrunn.jpg" alt="">
</a>
</div>
<h4>4. Simply inverting colors might not be want you want.</h4>
<p>Some articles recommend to simply invert colors and call it a day. While this might work on some sites, it doesn’t mean that it works well.</p>
<pre><code class="language-css">html,
body {
background-color: #fff;
}
body {
color: #222;
}
@media (prefers-color-scheme: dark) {
html {
/* Invert the colors */
filter: invert(100%);
}
img {
filter: invert(100%); /* Invert the colors in images back to normal */
}
}</code></pre>
<p><a href="https://codepen.io/matuzo/pen/ExKKMGw">Dark Mode with filter: invert(100%) on CodePen</a></p>
<p>The <code>filter</code> property and <code>invert</code> function reverts colors in a page.</p>
<p>That’s not a best practice because just like you’ve picked the right combination of colors for your default theme carefully, you want to <a href="https://twitter.com/steveschoger/status/1151160261170126850">hand-pick the colors for your dark theme</a>, too. However, on a component level <code>filter: invert(100%)</code> might work well, so I’d keep it in my tool set.</p>
<h4>5. Use the <code>currentColor</code> keyword in SVGs for <code>fill</code> or <code>stroke</code> properties instead of absolute values.</h4>
<p>In this simplified example you can see that the fill color of the SVG in the button is always the same as the text color of the button.</p>
<pre><code class="language-html"><button>
Sign up
<svg height="50" width="50" focusable="false">
<circle cx="25" cy="25" r="20">
</svg>
</button></code></pre>
<pre><code class="language-css">button {
color: #f00;
}
circle {
fill: #f00;
}
button:hover {
color: #0f0;
}
button:hover circle {
fill: #0f0;
}
@media (prefers-color-scheme: dark) {
button {
color: #00f;
}
circle {
fill: #00f;
}
button:hover {
color: #f0f;
}
button:hover circle {
fill: #f0f;
}
}</code></pre>
<style>
.a-up-button {
background: #153a51;
border-radius: 5px;
border: none;
font-size: 2rem;
display: flex;
align-items: center;
padding: 0.3rem 0.7rem;
}
.a-up-button1 {
color: #F00;
}
.a-up-button1 circle {
fill: #F00;
}
.a-up-button1:hover {
color: #0F0;
}
.a-up-button1:hover circle {
fill: #0F0;
}
@media (prefers-color-scheme: dark) {
.a-up-button1 {
color: #00F
}
.a-up-button1 circle {
fill: #00F;
}
.a-up-button1:hover {
color: #F0F;
}
.a-up-button1:hover circle{
fill: #F0F;
}
}
</style>
<div>
<button class="a-up-button a-up-button1" type="button">
Sign up
<svg height="50" width="50" focusable="false">
<circle cx="25" cy="25" r="20">
</svg>
</button>
</div>
<p>It works but the code is too verbose. We can reduce the number of lines and make it more dynamic by using the <code>currentColor</code> keyword. <code>currentColor</code>is relative to the <code>color</code> property of the element or its parent element, it changes with the color on hover, focus, Dark Mode, etc.</p>
<pre><code class="language-css">button {
color: #f00;
}
button:hover {
color: #0f0;
}
circle {
fill: currentColor;
}
@media (prefers-color-scheme: dark) {
button {
color: #00f;
}
button:hover {
color: #f0f;
}
}</code></pre>
<style>
.a-up-button2 {
color: #F00;
}
.a-up-button2:hover {
color: #0F0;
}
.a-up-button2 circle {
fill: currentColor;
}
@media (prefers-color-scheme: dark) {
.a-up-button2 {
color: #00F
}
.a-up-button2:hover {
color: #F0F;
}
}
</style>
<div>
<button class="a-up-button a-up-button2" type="button">
Sign up
<svg height="50" width="50" focusable="false">
<circle cx="25" cy="25" r="20">
</svg>
</button>
</div>
<p>For the sake of completeness, here’s an example that uses custom properties and <code>currentColor</code>.</p>
<pre><code class="language-css">:root {
--color: #f00;
--color_hover: #0f0;
}
@media (prefers-color-scheme: dark) {
:root {
--color: #00f;
--color_hover: #f0f;
}
}
button {
color: var(--color);
}
button:hover {
color: var(--color_hover);
}
circle {
fill: currentColor;
}</code></pre>
<style>
:root {
--up_color: #F00;
--up_color_hover: #0F0;
}
@media (prefers-color-scheme: dark) {
:root {
--up_color: #00F;
--up_color_hover: #F0F;
}
}
.a-up-button3 {
color: var(--up_color);
}
.a-up-button3:hover {
color: var(--up_color_hover);
}
.a-up-button3 circle {
fill: currentColor;
}
</style>
<div>
<button class="a-up-button a-up-button3" type="button">
Sign up
<svg height="50" width="50" focusable="false">
<circle cx="25" cy="25" r="20">
</svg>
</button>
</div>
<p><a href="https://codepen.io/matuzo/pen/ExKvjWP">currentColor demo on CodePen</a></p>
<h4>6. Swapping images</h4>
<p>CSS has its limitations when it comes to swaping colors or manipulating colors in images. Sometimes its necessary to swap entire images. I found this great solution on <a href="https://nandovieira.com/supporting-dark-mode-in-web-content">Nando Vieras blog</a> where he uses the <code>picture</code>element and media queries to serve the right image.</p>
<pre><code class="language-html"><picture>
<source srcset="dark_logo.jpg" media="(prefers-color-scheme: dark)" />
<img src="light_logo.jpg" alt="Homepage" />
</picture></code></pre>
<p><a href="https://codepen.io/matuzo/pen/BazoQrz?editors=1100">Switch images in Dark Mode demo on CodePen</a></p>
<p>Users change browser or OS settings to improve their experiences for a reason. Please respect these decisions by writing CSS that’s flexible enough to respond to their preferences. Font size, motion, and dark colors settings are only examples, there’s even more to consider like <a href="https://developer.paciellogroup.com/blog/2016/12/windows-high-contrast-mode-the-limited-utility-of-ms-high-contrast/">High Contrast mode</a> on Windows or <a href="https://adrianroselli.com/2017/11/os-high-contrast-versus-inverted-colors.html">inverted colors</a>.</p>
<h3>Resources</h3>
<p>Dark Mode is a fantastic example for a feature that's <a href="https://www.w3.org/WAI/perspective-videos//">essential for some and useful for all</a>. If you want to add it to your website (yeah, I know, there’s no Dark Mode on my site. It’s a long story...), check these links. (Thanks, <a href="https://twitter.com/mxbck/">Max</a>).</p>
<ul>
<li><a href="https://hankchizljaw.com/wrote/create-a-user-controlled-dark-or-light-mode/">Create a user controlled dark or light mode</a> by Andy Bell</li>
<li><a href="https://www.smashingmagazine.com/2017/09/building-inclusive-toggle-buttons/">Building Inclusive Toggle Buttons</a> by Heydon Pickering</li>
<li><a href="https://uxdesign.cc/dark-mode-ui-design-the-definitive-guide-part-1-color-53dcfaea5129">Dark Mode UI: the definitive guide</a> by Atharva Kulkarni</li>
<li><a href="https://www.youtube.com/watch?v=8PPdUS9zMhA">Respecting User Preferences on the Web</a> by Eric Eggert (video)</li>
</ul>
<h3>Recording</h3>
<p>If you want to learn more about CSS and accessibility and you don’t want to wait for me to publish the other articles in this series, you can watch my talk about writing CSS with accessibility in mind at #ID24:</p>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/o6ssu5oKyaU" title="Writing even more CSS with Accessibility in Mind" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div>
<p>Thanks to <a href="https://twitter.com/yatil">Eric</a> for helping me with this article.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWriting+even+more+CSS+with+Accessibility+in+Mind%2C+Part+2%3A+Respecting+user+preferences%E2%80%9D">blog@matuzo.at</a>.</p> The lang attribute: browsers telling lies, telling sweet little lies2020-10-19T00:00:00+00:00https://www.matuzo.at/blog/lang-attribute
<p>The <code>lang</code> attribute is an essential component in the basic structure of an HTML document. It’s important that we define it correctly because it affects many aspects of user experience. Unfortunately, the negative effects a missing or wrong attribute can have aren’t always evident. Austrian news site <a href="http://orf.at">orf.at</a> learned that the hard way recently.</p><p>Applied to the <code><html></code> element, the <code>lang</code> attribute defines the natural language of a page. If your document is written in French, you would set it to <em>fr.</em></p>
<pre><code class="language-html"><html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html></code></pre>
<p>Search engines, screen readers, browser extensions, and other software use this information.</p>
<h2>Why is the lang attribute so important?</h2>
<p><a href="https://twitter.com/aardrian">Adrian Roselli</a> wrote about the <a href="https://adrianroselli.com/2015/01/on-use-of-lang-attribute.html">Use of the Lang Attribute</a>, so I will not repeat everything he says but I’ll give you some examples of how this attribute influences UX, and I will show you what happened recently on the most popular Austrian website in Austria.</p>
<h3>Auto-translation</h3>
<p>Translation tools like Google Translate may offer to translate contents of a page if the language defined in the <code><html></code> element is not the same as the default language in the browser. It seems it doesn’t matter in which language the page is actually written, what counts for Google Translate is the value of the <code>lang</code> attribute. In the following example, Chrome gives me the option to translate the Croatian text on the page, which isn’t Croatian, to English because I set <code>lang</code> to <code>hr</code>.</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1000/v1603086165/articles/croatian.png" alt="Google Translate prompt offering to translate Croatian which is actually English to English" /></p>
<pre><code class="language-html"><!DOCTYPE html>
<html lang="hr">
<head>
…
</head>
<body>
<blockquote>Does Barry Manilow know that you raid his wardrobe?</blockquote>
</body>
</html></code></pre>
<p>That makes sense and in principle it’s a useful feature but it can be problematic, if translations are not desired:</p>
<p>Vienna went to the polls this month to elect a (new) municipal government and <a href="https://orf.at">orf.at</a> published the results on their website. They split the results by district, from the 1st district “Innere Stadt” to the 23rd district “Liesing”. Soon after they’ve published the results, they received bug reports from users stating that some names of districts where wrong. When they tried to reproduce the bug, they noticed that Chrome offered to translate the page. If you just close the prompt, nothing happens, but if in the past you <a href="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1000/v1603086165/articles/croatian2.png">let Google Translate translate pages automatically</a>, it wouldn’t ask anymore but automatically just translate the page.</p>
<p>The page was written in German but the natural language of the page was set to English in HTML (<code>en</code>), and that’s why Chrome users who instructed the browser to translate English to German automatically saw this:</p>
<p><img src="https://res.cloudinary.com/dp3mem7or/image/upload/c_scale,w_1000/v1603086166/articles/luegen.jpg" alt="A list of districts on orf.at. Instead of Liesing some users so Lügen" /></p>
<p>Google Translate changed the name of the 23rd district “Liesing” to “Lügen” (<em>to lie</em> in English) and the name of the 10th district from “Simmering” to “Sieden” (<em>to simmer</em> in English). Google Translate assumed that the authors meant to write "lying" (<span lang="de">lügen</span> in German) and that "Simmering" isn’t just a name but the act of cooking liquids.</p>
<h4>How did it happen?</h4>
<p>First, I have that to say that it’s not a big deal. I’m just writing about it because it’s an interesting side effect of an issue that usually affects accessibility and not user experience in general.</p>
<p>I know some devs at <a href="http://orf.at">orf.at</a> and I also know that they care about quality and accessibility, so why did that still happen? Of course, because we’re all human and shit happens, but the specific problem on <a href="http://orf.at">orf.at</a> was that they’re using <a href="https://github.com/vuejs/vue-cli">vue-cli</a>, which automatically creates a boilerplate structure for documents that sets the <code>lang</code> attribute to <code>en</code> by default. The devs didn’t notice it because validators and automatic accessibility testing tools just check if the attribute is present and the value valid.</p>
<h4>Preventing this bug</h4>
<p>So, it’s a bug that’s hard to spot, but how can we prevent it?</p>
<ol>
<li>Frameworks and other setups that offer a boilerplate HTML structure could make picking the natural language part of the automatic or manual installation process instead of setting a default. The devs at <a href="http://orf.at">orf.at</a> already created an <a href="https://github.com/vuejs/vue-cli/issues/5945">issue for vue-cli</a>.</li>
<li>The documentation should include a section about the HTML document structure and explain why certain things are important and which side effects a wrong implementation can have.</li>
<li>You could use something like <a href="https://ffoodd.github.io/a11y.css/">a11y.css</a> to debug your document or create your own debug.css and add tests you want to run in your development environment. “Tests” sounds fancy, but it’s just a bunch of selectors that show a big red border or other visual indicators, if there’s an error.</li>
</ol>
<h4>Debugging the lang attribute</h4>
<p>There are several things you can check.</p>
<ol>
<li>Is a <code>lang</code> attribute present? If not, show a red dotted border.</li>
</ol>
<pre><code class="language-css">html:not([lang]) {
border: 10px dotted red;
}</code></pre>
<p>You could even display an error message.</p>
<pre><code class="language-css">html:not([lang])::before {
content: 'lang attribute missing';
display: inline-block;
background: red;
position: fixed;
top: 0;
left: 0;
padding: 0.3em;
color: #fff;
font-size: 1.2rem;
}</code></pre>
<p><a href="https://codepen.io/matuzo/project/editor/AqPqmJ#0">Missing lang attribute demo on CodePen</a></p>
<ol start="2">
<li>Is a lang attribute present but the value empty?</li>
</ol>
<pre><code class="language-css">html[lang=''],
html[lang*=' '] {
border: 10px dotted red;
}</code></pre>
<p><a href="https://codepen.io/matuzo/project/editor/AgGLRa">Empty lang attribute demo on CodePen</a></p>
<ol start="3">
<li>Does the lang attribute contain the correct value?</li>
</ol>
<pre><code class="language-css">html:not(:lang(en)) {
border: 10px dotted red;
}</code></pre>
<p><a href="https://codepen.io/matuzo/project/editor/DNabgn">Wrong value in lang attribute demo on CodePen</a></p>
<p>What’s great about using the <code>lang()</code> pseudo class is that this test will work even if the value is <code>en-US</code> or <code>en-GB</code>, <abbr title="et cetera">etc</abbr>.</p>
<p>Note: That won’t work on multilingual websites and you’ll have to adjust it for every project depending on the language.</p>
<ol start="4">
<li>Putting it all together:</li>
</ol>
<pre><code class="language-css">html:not([lang]),
html:not(:lang(en)),
html[lang=''],
html[lang*=' '] {
border: 10px dotted red;
}
html:not([lang])::before,
html[lang='']::before,
html[lang*=' ']::before,
html:not(:lang(en))::before {
display: inline-block;
background: red;
position: fixed;
top: 0;
left: 0;
padding: 0.3em;
color: #fff;
font-size: 1.2rem;
}
html:not(:lang(en))::before {
content: 'wrong value for lang attribute';
}
html[lang='']::before,
html[lang*=' ']::before {
content: 'lang attribute empty';
}
html:not([lang])::before {
content: 'lang attribute missing';
}</code></pre>
<p><a href="https://codepen.io/matuzo/project/editor/ZyrVee">Debugging the lang attribute on CodePen</a></p>
<p>As already mentioned, the <code>lang</code> attribute doesn’t just affect auto-translation but many other aspects of user experience. Here are two more examples:</p>
<h3>Screen readers</h3>
<p>Screen readers, the software people use to read content on a page, may pick a different voice profile according to the value of the <code>lang</code> attribute. If you set it to Russian (<code>ru</code>) but the language of the page is English, it may sound like someone’s talking English with a heavy Russian accent.</p>
<p>Steve Faulkner recorded a demo to show you the effects.</p>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" title="Effect of lang attribute values on JAWS speech" height="315" src="https://www.youtube.com/embed/0uzxu9dQnuU" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div>
<h3>Quotation marks</h3>
<p>Quotation marks may change depending on the the natural language of the page.</p>
<p>Quotation marks on an English page look like this:</p>
<style>
.contentdemoquote {
margin-bottom: 3rem !important;
font-weight: bold;
}
.contentdemoquote::before {
display: none;
transform: none !important;
border: none !important;
width: auto !important;
height: auto !important;
position: static !important;
}
.contentdemoquote::before {
content: open-quote !important;
display: inline;
}
.contentdemoquote::after {
content: close-quote;
}
</style>
<blockquote class="contentdemoquote" lang="en">
Does Barry Manilow know that you raid his wardrobe?
</blockquote>
<pre><code class="language-html"><html lang="en">
<head>
…
</head>
<body>
<blockquote>Does Barry Manilow know that you raid his wardrobe?</blockquote>
</body>
</html></code></pre>
<p>German:</p>
<blockquote class="contentdemoquote" lang="de">
Does Barry Manilow know that you raid his wardrobe?
</blockquote>
<pre><code class="language-html"><html lang="de">
<head>
…
</head>
<body>
<blockquote>Does Barry Manilow know that you raid his wardrobe?</blockquote>
</body>
</html></code></pre>
<p>French:</p>
<blockquote class="contentdemoquote" lang="fr">
Does Barry Manilow know that you raid his wardrobe?
</blockquote>
<pre><code class="language-html"><html lang="fr">
<head>
…
</head>
<body>
<blockquote>Does Barry Manilow know that you raid his wardrobe?</blockquote>
</body>
</html></code></pre>
<p>You can learn more about quotation marks in CSS in <a href="https://www.matuzo.at/blog/heres-what-i-didnt-know-about-content/">Here’s what I didn’t know about “content”</a>.</p>
<h3>Others</h3>
<p>The correct usage of the value also may affect hyphenation, <abbr title="Web Content Accessibility Guidelines">WCAG</abbr> compliance, input fields, spell checking and the default font selection for CJK languages (Chinese, Japanese, and Korean). Read <a href="https://adrianroselli.com/2015/01/on-use-of-lang-attribute.html">Adrian Rosellis article</a> (and the comments section) for details.</p>
<h2>Tell me lies, tell me sweet little lies</h2>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe width="560" title="Fleetwood Mac - Little Lies" height="315" src="https://www.youtube.com/embed/uCGD9dT12C0" style="border:none" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CThe+lang+attribute%3A+browsers+telling+lies%2C+telling+sweet+little+lies%E2%80%9D">blog@matuzo.at</a>.</p> Slow Movement2020-12-16T00:00:00+00:00https://www.matuzo.at/blog/flex-grow-is-weird-or-is-it
<p>My answer to the question “What is one thing you learned about building websites this year?”</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CSlow+Movement%E2%80%9D">blog@matuzo.at</a>.</p> Ordering CSS properties2021-01-04T00:00:00+00:00https://www.matuzo.at/blog/ordering-css-properties
<p>I haven’t thought about ordering CSS properties in a while, but I began to work on the redesign of <a href="https://www.htmhell.dev">HTMHell</a> recently and I decided to challenge my current approach.</p><h2>How I’m ordering CSS properties now</h2>
<p>Honestly, I don’t know. It’s a mix of logical grouping, alphabetical order and just no order at all. It kinda has some structure, but I guess you could describe it as pretty arbitrary.</p>
<p>Here’s a random class from the current HTMHell website.</p>
<pre><code class="language-css">.site-intro {
font-size: 2rem;
text-align: center;
font-weight: 700;
line-height: 1.5;
max-width: 36ch;
margin-left: auto;
margin-right: auto;
}</code></pre>
<p>According to <a href="https://twitter.com/mmatuzo/status/1345815183680270340">a poll I ran on Twitter</a>, most people order by type, closely followed by alphabetical ordering. I also did a quick search and, as expected, many people have thought and written about different approaches and the pros and cons compared to others.</p>
<ul>
<li><a href="https://css-tricks.com/poll-results-how-do-you-order-your-css-properties/">Poll Results: How do you order your CSS properties?</a></li>
<li><a href="https://webdesign.tutsplus.com/articles/outside-in-ordering-css-properties-by-importance--cms-21685">“Outside In” — Ordering CSS Properties by Importance</a></li>
<li><a href="https://9elements.com/css-rule-order/">How to organize CSS @ 9elements</a></li>
<li><a href="https://medium.com/@jerrylowm/alphabetize-your-css-properties-for-crying-out-loud-780eb1852153">Alphabetize your CSS properties, for crying out loud</a></li>
<li><a href="https://dev.to/thekashey/happy-potter-and-the-order-of-css-5ec">Harry Potter and the Order of CSS</a></li>
<li><a href="https://meiert.com/en/blog/on-declaration-sorting/">On Declaration Sorting in CSS</a></li>
</ul>
<h2>My new approach</h2>
<p>Since I don’t have a strong opinion on the topic and I haven’t tried different techniques, I will not repeat what others have done already, I’ll just share how I’ll try to write CSS on the new HTMHell website. Switching to a completely new system feels a bit too radical, so I decided to refine and structure my current approach.</p>
<p>I’m against grouping all properties, because it’s hard to group all of them sensibly and it’s hard to remember later which properties go together. I’m also against ordering alphabetically because I want <code>bottom</code> and <code>right</code> close to <code>position</code> and <code>align-items</code> close to <code>display</code>. I want strict ordering for properties I use a lot and a loose but structured ordering for the rest. I work my way from the outside in, from the back to the front.</p>
<ol>
<li><strong>Box model properties</strong><br />
(<code>width</code>, <code>height</code>, <code>min-/max-width</code>, <code>min-/max-height</code>, <code>padding</code>, <code>margin</code>, <code>border</code>)</li>
<li><strong>Background</strong><br />
(<code>background-image</code>, <code>background-size</code>, etc.)</li>
<li><strong>Layout</strong><br />
(<code>display</code>, <code>position</code>, <code>justify-content</code>, <code>top</code>, <code>bottom</code>, <code>align-items</code>, <code>align-content</code>, <code>grid-gap</code>, <code>grid-template-columns</code>, <code>flex-wrap</code>, etc.)</li>
<li><strong>Typography</strong><br />
(<code>font-size</code>, <code>font-style</code>, <code>font-family</code>, <code>color</code>, <code>line-height</code>, <code>letter-spacing</code>, etc.)</li>
<li><strong>Everything else</strong>, in alphabetical order</li>
</ol>
<p>Here’s an example:</p>
<pre><code class="language-css">body {
/* box model */
margin: 0;
padding: 2rem 1rem;
min-height: 100vh;
/* background */
background-image: url(../img/bg.png);
/* layout */
display: flex;
flex-direction: column;
/* type */
font-family: sans-serif;
font-size: 1.2rem;
line-height: 1.7;
color: var(--color-text);
/* other, in alphabetical order */
box-shadow: 2px 2px 2px 2px #000;
transition: all 0.3s;
}</code></pre>
<p>If I notice that I’m using a property from the 5th group a lot, I might promote it to one of the other groups. Grouping and sorting is probably unnecessary on such a small site like the one I’m working on right now, but it’s an interesting exercise. If It works here, I might try it at work on a larger project with more people involved. If not, I’ll just keep doing whatever feels right to me. 🙃</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9COrdering+CSS+properties%E2%80%9D">blog@matuzo.at</a>.</p> More or less burger-less navigation2021-01-07T00:00:00+00:00https://www.matuzo.at/blog/more-or-less-burger-less-navigation
<p>For your and my inspiration: A collection of websites that don’t hide the navigation on mobile behind a burger/menu button.</p><div class="grid">
<div>
<h2 class="h3">Show some, hide the rest</h2>
<p><a href="https://bbc.com/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610015126/articles/mobile-nav/bbccom.png" alt="bcc.com shows as many links as fit into one row and hides the rest behind a “more” button" loading="lazy" width="375" height="300">
</a>
</div>
<div>
<h2 class="h3">Single row, centered</h2>
<p><a href="https://numl.design/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610016244/articles/mobile-nav/numldesign.png" alt="numl.design has a centered logo at the top and a single row with three links below." loading="lazy" width="375" height="300">
</a>
</div>
<div>
<h2 class="h3">Single row, space-between</h2>
<p><a href="https://codersblock.com/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610017321/articles/mobile-nav/codersblock.png" alt="codersblock.com has a centered logo at the top and a single row with four links below with items split evenly." loading="lazy" width="375" height="300">
</a>
<p><br><br />
<br></p>
<p><a href="https://hiddedevries.nl/en/contact/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610018172/articles/mobile-nav/hidde.png" alt="hiddedevries.nl has a left aligned logo at the top and a single row with five links below with items split evenly." loading="lazy" width="375" height="300">
</a>
</div>
<div>
<h2 class="h3">Single row, scrollable</h2>
<p><a href="https://www.flickr.com/photos/thomashawk/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610017321/articles/mobile-nav/flickr.png" alt="There are 6 links in the navigation on flickr profile pages displayed in a single scrollable row. They use gradients to indicate that the row is scrollable." loading="lazy" width="375" height="300">
</a>
</div>
<div>
<h2 class="h3">Multiple rows</h2>
<p><a href="https://womenforwomeninternational.de/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610016243/articles/mobile-nav/womenforwomeninternational.png" alt="“Women for women” has a top bar with a link and a search, below a centered logo and below the logo two rows with three links and a button." loading="lazy" width="375" height="300">
</a>
<p><br><br />
<br></p>
<p><a href="https://dfkaye.com/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610018172/articles/mobile-nav/dfkaye.png" alt="DF Kayes site has link to the homepage on the left and three rows with 6 links on the right." loading="lazy" width="375" height="300">
</a>
</div>
<div>
<h2 class="h3">Vertical navigation</h2>
<p><a href="https://stylestage.dev/styles/drafter/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610016244/articles/mobile-nav/stylestagedrafter.png" alt="“A “Style Stage” theme. The navgation is rotated 90 degrees and positioned on the right side of the viewport" loading="lazy" width="375" height="600">
</a>
</div>
<div>
<h2 class="h3">Single row, additional icons</h2>
<p><a href="https://draufsicht.com/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610018636/articles/mobile-nav/draufsicht.png" alt="draufsicht.com decreases the font size significantly, but adds icons to each link on mobile." loading="lazy" width="375" height="300">
</a>
</div>
<div>
<h2 class="h3">Multiple rows, aligned to the right</h2>
<p><a href="https://www.ilithya.rocks/installations/" rel="noopener"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1610019266/articles/mobile-nav/ilithya.png" alt="The navigation on ilithya.rocks is aligned to the right and splits items into 1-2 rows depending on the viewport width" loading="lazy" width="375" height="300">
</a>
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CMore+or+less+burger-less+navigation%E2%80%9D">blog@matuzo.at</a>.</p> Dev Tools: Debugging DOM Tree modifications2021-01-29T00:00:00+00:00https://www.matuzo.at/blog/dev-tools-debugging-dom-tree-modifications
<p>“Break on Subtree Modification” allows you to debug dynamically added and removed DOM nodes.</p><p>The other day I was debugging a Drag’n’Drop component, and I noticed that it added a DOM node every time I dragged an element. I wanted to inspect the node and see what’s going on in the CSS panel, but as soon as I dropped the element I was dragging, the new node was removed from the DOM (Document Object Model). I tried to catch it quickly, but I didn’t have a chance.</p>
<h2>Break Stuff</h2>
<p>A quick search pointed me to an option I’ve noticed several times in the context menu of nodes in the elements panel, but I never cared to see what it was doing: “Break on…”.</p>
<p><a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1611901905/articles/domtree1.jpg"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1611901905/articles/domtree1.jpg" alt="Context menu in Firefox Dev Tools: the option “Break on…” selected and three child menu items showing: Subtree Modification, Attribute Modification, and Node Removal" loading="lazy" width="750" height="476">
</a>
<p>Selecting “Break on Subtree Modification” pauses any script that modifies the <abbr title="Document Object Model">DOM</abbr> of the selected element and jumps to the line in the script that modified it.</p>
<p><a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1611901905/articles/domtree2.jpg"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1611901905/articles/domtree2.jpg" alt="The line that caused the DOM modification is highlighted in the sources panel." loading="lazy" width="750" height="476">
</a>
<p>Now that the script is paused, you can unhurriedly inspect the <abbr title="Document Object Model">DOM</abbr>.</p>
<p><a href="https://res.cloudinary.com/dp3mem7or/image/upload/v1611901905/articles/domtree3.jpg"></p>
<img src="https://res.cloudinary.com/dp3mem7or/image/upload/v1611901905/articles/domtree3.jpg" alt="Dev tools shows a message “Paused on DOM mutation” and allows me to select the new node in the elements panel." loading="lazy" width="750" height="476">
</a>
<p>This option helped me not only to debug the component, but I also felt less stupid because I didn’t have to desperately try to move the mouse as quickly as possible.</p>
<p>The other options are useful, too. I can imagine that “Break on attribute modification” can come in handy when we’re debugging complex animations or user interactions.</p>
<p>You can use <a href="https://cdpn.io/matuzo/debug/BaQajJg">this CodePen</a>, if you want to try it yourself.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDev+Tools%3A+Debugging+DOM+Tree+modifications%E2%80%9D">blog@matuzo.at</a>.</p> Highlighting columns in HTML tables2021-03-20T00:00:00+00:00https://www.matuzo.at/blog/highlighting-columns
<p>The <code>col</code> element allows us to style columns in tables.</p><p>In the past, I’ve used the <code>colgroup</code> and <code>col</code> elements to define max-widths for columns in tables when I didn’t want to rely on the default algorithm for distribution of widths, usually when building templates for e-mail newsletters.</p>
<h3>A simple table with dummy content</h3>
<pre><code class="language-html"><table>
<thead>
<tr>
<th>First column</th>
<th>Second column</th>
<th>Third column</th>
</tr>
</thead>
<tbody>
<tr>
<td>First cell</td>
<td>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Id, dolor delectus. Modi accusamus id magni.</td>
<td>Last cell</td>
</tr>
</tbody>
</table></code></pre>
<style>
td, th {
border: 1px solid #aaa;
padding: 0.2rem;
}
</style>
<table>
<thead>
<tr>
<th scope="col">First column</th>
<th scope="col">Second column</th>
<th scope="col">Third column</th>
</tr>
</thead>
<tbody>
<tr>
<td>First cell</td>
<td>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Id, dolor delectus. Modi accusamus id magni.</td>
<td>Last cell</td>
</tr>
</tbody>
</table>
<h3>A simple table with dummy content and a fixed width for the first and last column</h3>
<pre><code class="language-html"><table>
<colgroup>
<col style="width: 150px">
<col>
<col style="width: 150px">
</colgroup>
<thead>
…
</table></code></pre>
<table>
<colgroup>
<col style="width: 150px">
<col>
<col style="width: 150px">
</colgroup>
<thead>
<tr>
<th scope="col">First column</th>
<th scope="col">Second column</th>
<th scope="col">Third column</th>
</tr>
</thead>
<tbody>
<tr>
<td>First cell</td>
<td>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Id, dolor delectus. Modi accusamus id magni.</td>
<td>Last cell</td>
</tr>
</tbody>
</table>
<h3>Styling columns</h3>
<p>The other day I looked up <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/colgroup">colgroup on MDN</a> because something didn’t work the way I expected it to work. I found the solution to my problem, but the demo at the beginning of the page also caught my attention. The author adds a class to the <code>col</code> element, which does nothing with the <code>col</code> element itself, at least not visually, because the element doesn’t get rendered on the page, but it applies the styles from the class to all the cells in the column.<br />
Why is this interesting? Well, there are no columns in HTML tables, only rows and cells and the <code>col</code> element allows us to style columns anyway.</p>
<style>
.heading-table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
max-width: 55ch;
}
<p>.heading-table caption {<br />
font-size: 1.1em;<br />
text-align: left;<br />
font-weight: bold;<br />
margin-bottom: 0.5em;<br />
}</p>
<p>.heading-table a:link,<br />
.heading-table a:visited {<br />
color: #000;<br />
}</p>
<p>.heading-table .highlight,<br />
.heading-table col:target {<br />
background: #dedede;<br />
}</p>
</style>
<pre><code class="language-css">.highlight {
background: #dedede;
}</code></pre>
<pre><code class="language-html"><table>
<caption>Frequency and average use of heading elements.</caption>
<colgroup>
<col>
<col class="highlight">
<col>
</colgroup>
<thead>
<tr>
<th>Heading</th>
<th>Occurrences</th>
<th>Average per page</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>h1</code></td>
<td>10,524,810</td>
<td>1.66</td>
</tr>
…</code></pre>
<table class="heading-table">
<caption>Frequency and average use of heading elements.</caption>
<colgroup>
<col>
<col class="highlight">
<col>
</colgroup>
<thead>
<tr>
<th scope="col">Heading</th>
<th scope="col">Occurrences</th>
<th scope="col">Average per page</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>h1</code></td>
<td>10,524,810</td>
<td>1.66</td>
</tr>
<tr>
<td><code>h2</code></td>
<td>37,312,338</td>
<td>5.88</td>
</tr>
<tr>
<td><code>h3</code></td>
<td>44,135,313</td>
<td>6.96</td>
</tr>
<tr>
<td><code>h4</code></td>
<td>20,473,598</td>
<td>3.23</td>
</tr>
<tr>
<td><code>h5</code></td>
<td>8,594,500</td>
<td>1.36</td>
</tr>
<tr>
<td><code>h6</code></td>
<td>3,527,470</td>
<td>0.56</td>
</tr>
<tr>
<td><code>h7</code></td>
<td>30,073</td>
<td>0.005</td>
</tr>
<tr>
<td><code>h8</code></td>
<td>9,266</td>
<td>0.0015</td>
</tr>
</tbody>
</table>
<p>That’s pretty cool, but what’s even better is that we can let users decide which column they want to highlight and they can even share a link to the table with a specific column highlighted.</p>
<p>We can do that by adding <code>id</code>s to the <code>col</code> elements and styling them differently, if the <code>id</code> matches the URL’s fragment (#myid).</p>
<pre><code class="language-css">col:target {
background: #dedede;
}</code></pre>
<pre><code class="language-html"><table>
<caption>Frequency and average use of heading elements.</caption>
<colgroup>
<col id="table1-heading">
<col id="table1-occurrences">
<col id="table1-average">
</colgroup>
<thead>
<tr>
<th><a href="#table1-heading">Heading</a></th>
<th><a href="#table1-occurrences">Occurrences</a></th>
<th><a href="#table1-average">Average per page</a></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>h1</code></td>
<td>10,524,810</td>
<td>1.66</td>
</tr>
<tr>
<td><code>h2</code></td>
<td>37,312,338</td>
<td>5.88</td>
</tr>
<tr>
<td><code>h3</code></td>
<td>44,135,313</td>
<td>6.96</td>
</tr>
<tr>
<td><code>h4</code></td>
<td>20,473,598</td>
<td>3.23</td>
</tr>
<tr>
<td><code>h5</code></td>
<td>8,594,500</td>
<td>1.36</td>
</tr>
<tr>
<td><code>h6</code></td>
<td>3,527,470</td>
<td>0.56</td>
</tr>
<tr>
<td><code>h7</code></td>
<td>30,073</td>
<td>0.005</td>
</tr>
<tr>
<td><code>h8</code></td>
<td>9,266</td>
<td>0.0015</td>
</tr>
</tbody>
</table></code></pre>
<p>Click on any of the table headings and refresh the page to see it in action.</p>
<table class="heading-table">
<caption>Frequency and average use of heading elements.</caption>
<colgroup>
<col id="table1-heading">
<col id="table1-occurrences">
<col id="table1-average">
</colgroup>
<thead>
<tr>
<th scope="col"><a href="#table1-heading">Heading</a></th>
<th scope="col"><a href="#table1-occurrences">Occurrences</a></th>
<th scope="col"><a href="#table1-average">Average per page</a></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>h1</code></td>
<td>10,524,810</td>
<td>1.66</td>
</tr>
<tr>
<td><code>h2</code></td>
<td>37,312,338</td>
<td>5.88</td>
</tr>
<tr>
<td><code>h3</code></td>
<td>44,135,313</td>
<td>6.96</td>
</tr>
<tr>
<td><code>h4</code></td>
<td>20,473,598</td>
<td>3.23</td>
</tr>
<tr>
<td><code>h5</code></td>
<td>8,594,500</td>
<td>1.36</td>
</tr>
<tr>
<td><code>h6</code></td>
<td>3,527,470</td>
<td>0.56</td>
</tr>
<tr>
<td><code>h7</code></td>
<td>30,073</td>
<td>0.005</td>
</tr>
<tr>
<td><code>h8</code></td>
<td>9,266</td>
<td>0.0015</td>
</tr>
</tbody>
</table>
<p>Notes on accessibility: 1. I’m not sure if using links in table headings is the best idea. At least, they need better labels to provide users with context. 2. This technique only highlights columns visually, not semantically. </p>
<p><small><br />
Data from: <a href="https://almanac.httparchive.org/en/2020/markup#headings">Web Almanac 2020: Markup</a><br />
</small></p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CHighlighting+columns+in+HTML+tables%E2%80%9D">blog@matuzo.at</a>.</p> My current HTML boilerplate2021-04-09T00:00:00+00:00https://www.matuzo.at/blog/html-boilerplate
<p>Every element I use for the basic structure of a HTML document, with explanations why.</p><p><a href="https://www.ibidemgroup.com/edu/mi-plantilla-html/">Traducción a Español</a> by www.ibidemgroup.com.</p>
<p>Usually when I start a new project, I either copy the HTML structure of the last site I built or I head over to <a href="https://html5boilerplate.com/">HTML5 Boilerplate</a> and copy their boilerplate. Recently I didn’t start a new project, but I had to document the structure we use at work for the sites we build. So, simply copying and pasting wasn’t an option, I had to understand the choices that have been made. Since I spent quite some time researching and putting the structure together, I decided to share it with you.</p>
<div class="quote">
<blockquote>Cool, this is like https://github.com/h5bp/html5-boilerplate but a little worse.</blockquote>
<p>-Random reply guy on Hacker News</p>
</div>
<h2>My boilerplate</h2>
<p>This is the final document. Scroll down for details.</p>
<pre><code class="language-html"><!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Unique page title - My Site</title>
<script type="module">
document.documentElement.classList.remove('no-js');
document.documentElement.classList.add('js');
</script>
<link rel="stylesheet" href="/assets/css/styles.css">
<link rel="stylesheet" href="/assets/css/print.css" media="print">
<meta name="description" content="Page description">
<meta property="og:title" content="Unique page title - My Site">
<meta property="og:description" content="Page description">
<meta property="og:image" content="https://www.mywebsite.com/image.jpg">
<meta property="og:image:alt" content="Image description">
<meta property="og:locale" content="en_GB">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta property="og:url" content="https://www.mywebsite.com/page">
<link rel="canonical" href="https://www.mywebsite.com/page">
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/my.webmanifest">
<meta name="theme-color" content="#FF00FF">
</head>
<body>
<!-- Content -->
<script src="/assets/js/xy-polyfill.js" nomodule></script>
<script src="/assets/js/script.js" type="module"></script>
</body>
</html></code></pre>
<h2>Line by line explanation</h2>
<h3>General</h3>
<pre><code class="language-html"><!DOCTYPE html></code></pre>
<p>For the old-schoolers among you, you don’t need any of the other doc types you’ve learned by heart. This is the one and only. Even though today there are no other real options, it has to be present for <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Quirks_Mode_and_Standards_Mode">compatibility reasons</a>.</p>
<pre><code class="language-html"><html lang="en" class="no-js"></code></pre>
<p>The <code>lang</code> attribute is one of the most important attributes in HTML, because it’s powerful and responsible for many things. You can read more about it in <a href="https://adrianroselli.com/2015/01/on-use-of-lang-attribute.html">On Use of the Lang Attribute</a> and <a href="https://www.matuzo.at/blog/lang-attribute/">The lang attribute: browsers telling lies, telling sweet little lies</a>. Applied to the <code>html</code> element, it defines the natural language of the page. It contains a single “language tag” in the format defined in <a href="https://www.ietf.org/rfc/bcp/bcp47.txt">Tags for Identifying Languages (BCP47)</a>, for example <code>en</code> for English, <code>de</code> for German or <code>ru</code> for Russian. </p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang">lang Language tag syntax MDN</a></li>
</ul>
<p>I use the <code>no-js</code> class in case I want to apply styling to specific components in browsers that don’t support JavaScript. This class will be removed in browsers that support and execute modern JavaScript.</p>
<pre><code class="language-html"><meta charset="UTF-8"></code></pre>
<p>This attribute declares the document’s character encoding. Leaving it off might cause specific characters to display incorrectly in some browsers.</p>
<p>Here’s how Safari displays my name with and without the <code>charset</code> meta tag.</p>
<p>Manuel Matuzović - Manuel Matuzović</p>
<p>It must come before the <code><title></code> element to avoid faulty characters in the page title.</p>
<pre><code class="language-html"><meta name="viewport" content="width=device-width, initial-scale=1"></code></pre>
<p>The viewport meta tag allows us to change the width of the viewport, which is necessary for responsive web design. <code>width=device-width</code> sets the viewport width to the width of the screen. <code>initial-scale</code> controls the zoom level when the page is first loaded.</p>
<p>I’m not sure if setting <code>initial-scale=1</code> is still necessary. I believe I read somewhere that it was only needed for Safari on < iOS 9, but I can’t find the article anymore.</p>
<p>The <code>viewport</code> meta tag should come as early as possible in the document <a href="https://github.com/joshbuchea/HEAD#recommended-minimum">to ensure proper document rendering</a>.</p>
<p>We dont need the <code>shrink-to-fit=no</code> option anymore since iOS 9.3.</p>
<ul>
<li><a href="https://www.scottohara.me/blog/2018/12/11/shrink-to-fit.html">Time to remove the shrink-to-fit=no band aid?</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag">Using the viewport meta tag to control layout on mobile browsers</a></li>
</ul>
<h3>Title, description, and social media</h3>
<pre><code class="language-html"><title>Unique page title - My Site</title></code></pre>
<p>The unique title of the page. It’s displayed in many places, for example, on the browser tab, in search engine results, when you save a page as a bookmark, etc.</p>
<ul>
<li><a href="https://www.w3.org/WAI/tips/writing/#provide-informative-unique-page-titles">Provide informative, unique page titles</a></li>
<li><a href="https://hiddedevries.nl/en/blog/2018-07-19-accessible-page-titles-in-a-single-page-app">Accessible page titles in a Single Page App</a></li>
</ul>
<pre><code class="language-html"><script type="module">
document.documentElement.classList.remove('no-js');
document.documentElement.classList.add('js');
</script></code></pre>
<p>I’m <a href="https://fettblog.eu/cutting-the-mustard-2018/">cutting the mustard</a> at JS module support. If a browser supports JavaScript modules, it means that it’s a browser that supports modern JavaScript, such as modules, ES 6 syntax, fetch, etc. I ship most JS only to these browsers and I use the <code>js</code> class in CSS, if the styling of a component is different, when JS is active.</p>
<p><a href="https://caniuse.com/?search=javascript%20modules"></p>
<img alt="caniuse showing that all modern browser support JS modules" height="266" loading="lazy" src="https://www.matuzo.at/media/pages/blog/html-boilerplate/8b0bdd6866-1698841506/caniuse_jsmodules750.png" width="750">
</a>
<pre><code class="language-html"><link rel="stylesheet" href="/assets/css/styles.css"></code></pre>
<p>CSS for the site.</p>
<pre><code class="language-html"><link rel="stylesheet" href="/assets/css/print.css" media="print"></code></pre>
<p>Print CSS for the site.</p>
<ul>
<li><a href="https://www.matuzo.at/blog/i-totally-forgot-about-print-style-sheets/">I totally forgot about print style sheets</a></li>
</ul>
<pre><code class="language-html"><meta name="description" content="Page description"></code></pre>
<p>The unique description of the page, for example, displayed on search result pages. It can be any length, but search engines truncate snippets to ~155–160 characters.</p>
<pre><code class="language-html"><meta property="og:title" content="Unique page title - My Site"></code></pre>
<p>The unique title of the page. Used by URL scrapers on social media platforms like Twitter or Facebook.</p>
<pre><code class="language-html"><meta property="og:description" content="Page description"></code></pre>
<p>The unique description of the page. Used by URL scrapers on social media platforms like Twitter or Facebook.</p>
<pre><code class="language-html"><meta property="og:image" content="image.jpg"></code></pre>
<p>The image displayed when you share the link to a page on social media, chat applications, or other sites that scrape URLs.</p>
<p>Ideally, it should be a square image with the important content placed in the middle of the square in a rectangle with a 2:1 ratio. This will make sure that the image will look good in cards with rectangle and square shaped images.</p>
<p>Here’s is how <a href="https://res.cloudinary.com/dp3mem7or/image/upload/w_1200/articles/sm_tables.png">this image</a> will look on Twitter and on WhatsApp.</p>
<p>
<img alt="Large rectangle image in a Twitter card" height="353" loading="lazy" src="https://www.matuzo.at/media/pages/blog/html-boilerplate/fee0764af5-1698841506/htmldoc_tw500.png" width="500">
</p>
<p>
<img alt="Small square image on Whatsapp" height="115" loading="lazy" src="https://www.matuzo.at/media/pages/blog/html-boilerplate/37404b7793-1698841506/htmldoc_wa500.png" width="500">
</p>
<p>Rules for Twitter: <q>Images for this Card support an aspect ratio of 2:1 with minimum dimensions of 300x157 or maximum of 4096x4096 pixels. Images must be less than 5MB in size. JPG, PNG, WEBP and GIF formats are supported.</q></p>
<ul>
<li><a href="https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/summary-card-with-large-image">Twitter Developer Docs: Cards</a></li>
</ul>
<pre><code class="language-html"><meta property="og:image:alt" content="Image description"></code></pre>
<p>A description of the image. Don’t use this meta tag if the image is purely decorative and doesn’t provide any meaningful information. Screen readers ignore the image, if we don’t provide alt text.</p>
<pre><code class="language-html"><meta property="og:locale" content="en_GB"></code></pre>
<p>An optional Open Graph property, but recommended. It defines the natural language of the page.</p>
<pre><code class="language-html"><meta property="og:type" content="website"></code></pre>
<p>The <a href="https://ogp.me/#types">type of content</a> you’re sharing, e.g. <code>website</code>, <code>article</code>, or <code>video.movie</code>. A required property for valid Open Graph pages.</p>
<pre><code class="language-html"><meta property="og:url" content="https://www.mywebsite.com/page"></code></pre>
<p>The canonical URL of the page. A required property for valid Open Graph pages.</p>
<ul>
<li><a href="https://ogp.me">The Open Graph protocol</a></li>
</ul>
<pre><code class="language-html"><meta name="twitter:card" content="summary_large_image"></code></pre>
<p>This meta tag defines how cards will look when shared on Twitter. There are two options for websites, <code>summary</code> and <code>summary_large_image</code>.</p>
<p>
<strong>summary_large_image</strong><br>
<img alt="A large rectangle image on top, followed by the page title, description and URL below."" height="351" loading="lazy" src="https://www.matuzo.at/media/pages/blog/html-boilerplate/fb1af7b634-1698841506/htmldoc_summary-large500.png" width="500">
</p>
<p>
<strong>summary</strong><br>
<img alt="A small square image on the left, page title, description and URL on the right."" height="148" loading="lazy" src="https://www.matuzo.at/media/pages/blog/html-boilerplate/0423f70570-1698841505/htmldoc_summary500.png" width="500">
</p>
<p>You can see that I’m using a square image to ensure that the card looks good in both variations. I’ve painted the top and bottom part of the card pink so that you can see that these parts will be cut off in a <code>summary_large_image</code>.</p>
<h3>Icons and address bar</h3>
<pre><code class="language-html"><meta name="theme-color" content="#FF00FF"></code></pre>
<p><code>theme-color</code> provides browsers with a CSS color to customize the display of the page or of the surrounding user interface.</p>
<p>Supported browsers: Chrome, Brave and Samsung Internet on Android.</p>
<img alt="Pink UI in Brave browser" height="79" loading="lazy" src="https://www.matuzo.at/media/pages/blog/html-boilerplate/6cce285f66-1698841505/htmldoc_theme-color800.jpg" width="400">
<p>Starting with version 15, Safari will support <code>theme-color</code> on macOS and iOS. On top of that it will also support the <code>prefers-color-scheme</code> media feature. As of <time datetime="2021-06-17">today (17.06.21)</time> Android does not support the <code>media</code> attribute on <code>theme-color</code> meta tags, it will just use the first of the two declarations, regardless of the <code>media</code> attribute.</p>
<div class="post" style="margin-left: 2.25rem">
<pre><code class="language-html"><meta name="theme-color" content="#000" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#FFF" media="(prefers-color-scheme: light)"></code></pre>
</div>
<pre><code class="language-html"><link rel="icon" href="/favicon.ico"></code></pre>
<p>A 32×32px favicon for legacy browsers. It should live in the root of your website. </p>
<pre><code class="language-html"><link rel="icon" href="/favicon.svg" type="image/svg+xml"></code></pre>
<p>Most modern browser support <a href="https://caniuse.com/link-icon-svg">SVG favicons</a>. The benefits of the <code>favicon.svg</code> are that it looks better when it’s scaled, because it’s a vector and not raster image, and we can add HTML and CSS to the SVG, which means that we can support dark mode.</p>
<div class="post" style="margin-left: 2.25rem">
<pre><code class="language-html"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<style>
path {
fill: #153a51;
}
@media (prefers-color-scheme: dark) {
path {
fill: #FFFFFF;
}
}
</style>
<path d="M397.8 117.9v290h-76.4V273.5h-.8l-46.4 97.2h-36.8l-46-96.8h-.8v134h-76.4v-290h80.4l60.8 150.8h.8l61.2-150.8h80.4z"/>
</svg></code></pre>
<p>
Favicon on my website in light mode.<br>
<img alt="A blue M on light background in the browser tab in Chrome" loading="lazy" src="https://www.matuzo.at/media/pages/blog/html-boilerplate/bd5a65c419-1698841506/htmldoc_favicon_light800.png" width="400">
</p>
<p>
Favicon on my website in dark mode.<br>
<img alt="A white M on dark background in the browser tab in Chrome" loading="lazy" src="https://www.matuzo.at/media/pages/blog/html-boilerplate/f481eea8a4-1698841506/htmldoc_favicon_dark800.png" width="400">
</p>
</div>
<ul>
<li><a href="https://blog.tomayac.com/2019/09/21/prefers-color-scheme-in-svg-favicons-for-dark-mode-icons/">prefers-color-scheme in SVG favicons for dark mode icons</a></li>
</ul>
<pre><code class="language-html"><link rel="apple-touch-icon" href="/apple-touch-icon.png"></code></pre>
<p>The 180×180px icon Apple devices will use if you add the page to your home screen.</p>
<pre><code class="language-html"><link rel="manifest" href="/my.webmanifest"></code></pre>
<p>For Android devices we need a <code>.webmanifest</code> file, which provides browsers with the information where the icons needed for the home screen and the splash screen for <abbr title="Progressive Web Apps">PWAs</abbr> are located.</p>
<div class="post" style="margin-left: 2.25rem">
<pre><code class="language-json">{
"name": "matuzo.at",
"icons": [
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
]
}
</code></pre>
</div>
<ul>
<li><a href="https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs">How to Favicon in 2021: Six files that fit most needs</a></li>
</ul>
<pre><code class="language-html"><link rel="canonical" href="https://www.mywebsite.com/page"></code></pre>
<p>Use the <code>canonical</code> link element to prevent SEO issues caused by duplicate content by specifying the original source for pages that are available on multiple URLs.</p>
<pre><code class="language-html"><script src="/assets/js/xy-polyfill.js" nomodule></script></code></pre>
<p>If I want to serve JavaScript targeted specifically at browsers that don’t support modules, I add the <code>nomodule</code> attribute, which causes the script to only run in legacy browsers, namely IE 11.</p>
<pre><code class="language-html"><script src="/assets/js/script.js" type="module"></script></code></pre>
<p>JavaScript with the <code>type="module"</code> will only run in browsers that support modules, it’s the opposite of the <code>nomodule</code> attribute.</p>
<hr>
<p>This isn’t the absolute minimum, but it’s what I need in most sites I build. To round things up, I’ve added a bunch of tags to this post that we probably don’t need anymore and some others that you might need occasionally. If you want to learn more about the <code><head></code> element and its children, check out Josh Buchea’s fantastic repository <a href="https://github.com/joshbuchea/HEAD">HEAD</a>.</p>
<p>Did I get anything wrong or did I miss anything? Please tell me on <a href="https://twitter.com/mmatuzo">Twitter</a> or via <a href="mailto:manuel@matuzo.at">e-mail</a>.</p>
<h2>Stuff we don’t need anymore</h2>
<pre><code class="language-html"><meta name="msapplication-TileColor" content="#efefef"></code></pre>
<p><a href="https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs#windows-tile-icon">According to Andrey Sitnik</a>, this is no longer required for recent versions of Windows.</p>
<pre><code class="language-html"><meta http-equiv="X-UA-Compatible" content="ie=edge"></code></pre>
<blockquote>
<p>Starting with IE11, document modes are deprecated and should no longer be used, except on a temporary basis. </p>
</blockquote>
<blockquote>
<p>Starting with IE11, edge mode is the preferred document mode; it represents the highest support for modern standards available to the browser.</p>
</blockquote>
<p><a href="https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/dev-guides/bg182625(v=vs.85)#document-mode-changes">Compatibility changes in IE11</a></p>
<pre><code class="language-html"><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#36b1bf"></code></pre>
<p>Since Safari 12, we don’t need a separate variation of the icon for pinned tabs anymore.</p>
<h2>Other noteworthy elements</h2>
<pre><code class="language-html"><link rel="preload" href="font.woff2" as="font" type="font/woff2"></code></pre>
<p>Use <code>preload</code> if you want to ensure that specific resources are available earlier in the page lifecycle.</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content">Preloading content with rel="preload"</a></li>
<li><a href="https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf">Preload, Prefetch And Priorities in Chrome</a></li>
<li><a href="https://css-tricks.com/prefetching-preloading-prebrowsing/">Prefetching, preloading, prebrowsing</a></li>
</ul>
<pre><code class="language-html"><link type="application/atom+xml" rel="alternate" href="/feed.xml" title="My Blog - Manuel Matuzovic"></code></pre>
<p>RSS Feed for your site.</p>
<pre><code class="language-html"><meta name="format-detection" content="telephone=no"></code></pre>
<p>Disable automatic detection and formatting of phone numbers.</p>
<pre><code class="language-html"><meta name="twitter:dnt" content="on"></code></pre>
<p>Disallow Twitter from using your site’s info for personalization purposes.</p>
<p>Check out <a href="https://github.com/joshbuchea/HEAD">HEAD</a> for much more.</p>
<h2>Updates</h2>
<h3>Update 17.06.21</h3>
<ol>
<li>
<p>Added Spanish translation.</p>
</li>
<li>
<p>I tried to run some tests and find out whether <code>initial-scale=1</code> is still needed or not, but I realized that testing that properly what be too complicated, so to be safe I’ve included it again. Never change a winning team, right?</p>
</li>
<li>
<p>Added details about <code>theme-color</code> support in Safari 15.</p>
</li>
</ol>
<meta name="viewport" content="width=device-width, initial-scale=1"><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CMy+current+HTML+boilerplate%E2%80%9D">blog@matuzo.at</a>.</p> Browser support: focus pseudo classes2021-07-07T00:00:00+00:00https://www.matuzo.at/blog/focus-support
<p>I don’t know who needs to hear this, but that’s the current browser support for <code>:focus-visible</code> and <code>:focus-within</code>, and I love it!</p><h2>:focus-visible browser support</h2>
<p>
<a href="https://caniuse.com/css-focus-visible" rel="noopener">
<img alt="caniuse screenshot: All major browsers except IE 11 and Safari support the focus-visible pseudo class" height="287" loading="lazy" src="https://www.matuzo.at/media/pages/blog/focus-support/359242767c-1698841514/caniuse_focus-visible.jpg" width="751">
</a>
</p>
<h2>:focus-within browser support</h2>
<p>
<a href="https://caniuse.com/css-focus-within" rel="noopener">
<img alt="caniuse screenshot: All major browsers except IE 11 support the focus-within pseudo class" height="256" loading="lazy" src="https://www.matuzo.at/media/pages/blog/focus-support/3cc793e6bd-1698841513/caniuse_focus-within.jpg" width="751">
</a>
</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CBrowser+support%3A+focus+pseudo+classes%E2%80%9D">blog@matuzo.at</a>.</p> Element diversity2021-09-02T00:00:00+00:00https://www.matuzo.at/blog/element-diversity
<p>Did you know that there are 112 elements in HTML?!</p><pre><code class="language-html"><div id="appRoot">
<div>
<div>
<div>
<div class="heading">Heading</div>
<div class="content">
<div>
<div>
<div class="list">
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div></code></pre>
<!-- teaser -->
<p>
The markup above is something I see a lot on websites I audit professionally or when I just look under the hood of a website because I’m curious to see how it’s structured. The <a href="https://almanac.httparchive.org/en/2020/markup#top-elements">div is by far the most popular element</a>, which is absolutely fine, but it's often being used in favor of other elements that would fit better. This overuse is nothing new, but the rise of JavaScript (JS) frameworks has amplified it.
</p>
<p>It would be a bit too easy to only blame JS frameworks, there are several reasons we use <code>div</code>s so much:</p>
<ol>
<li>
<a href="#elements">Poor knowledge of HTML elements</a>
</li>
<li>
<a href="#why">Lack of understanding why</a>
</li>
<li>
<a href="#css">Insufficient CSS skills</a>
</li>
<li>
<a href="#defaultstyles">Default styles</a>
</li>
<li>
<a href="#frameworks">JS frameworks</a>
</li>
<li>
<a href="#thepage"> We don't care enough about <em>the page</em></a>
</li>
<li>
<a href="#style">Some elements are hard to style</a>
</li>
</ol>
<h2 id="elements">Poor knowledge of HTML elements</h2>
<p>
It's hard to use something you don't know exists. As someone who teaches HTML, I know that universities don't put enough emphasis on teaching semantic HTML. They teach syntax, basic structure and the ~20 most important elements and attributes, and that's pretty much it.
</p>
<p>
Did you know that there are 112 elements in HTML? <a href="twitter.com/plfstr">Paul Foster</a> built this fantastic <a href="https://codepen.io/plfstr/full/zYqQeRw">HTML memory test</a>. Try it, you'll be surprised how much you don't remember and know. (Heads up: His test lists 115 elements because it includes <code>svg</code>, <code>math</code>, and the deprecated <code>rb</code> element.)
</p>
<p class="codepen" data-height="400" data-slug-hash="zYqQeRw" data-user="plfstr" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/plfstr/pen/zYqQeRw">
HTML Tags Memory Test</a> by Paul (<a href="https://codepen.io/plfstr">@plfstr</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<h2 id="why">Lack of understanding why</h2>
<p>
Having spoken with many developers at workshops, meetups, and conferences, I believe that most know that semantic HTML is important, but only a few know why.
</p>
<p>
<q>For accessibility! Duh!</q>
</p>
<p>
Of course, for accessibility, but most people don't know why you need a <a href="https://www.htmhell.dev/tips/the-document-outline/">sound document outline</a> or landmarks, or what the advantages of a <code>button</code> button over a <code>div</code> button are. If I don't know which consequences my choices have on users, why would I bother using <a href="https://htmhell.dev/18-main-divigation/">anything else but the div</a>?
</p>
<h2 id="frameworks">JS frameworks</h2>
<p>
Some JavaScript frameworks require that you wrap components in a single element. If no semantic element fits, devs use the <code>div</code>. I believe they got used to the <code>div</code> so much that they began using it all the time. It's common to see invalid code like this:
</p>
<pre><code class="language-html"><ul>
<div>
<li>Item 1</li>
<li>Item 2</li>
</div>
</ul></code></pre>
<p>
The number of wrapper divs can be reduced, though, because most frameworks don't require you anymore to wrap your components in HTML elements. <a href="https://reactjs.org/docs/fragments.html">React supports fragments</a>, wrapper elements that don't add extra nodes to the DOM, and <a href="https://v3.vuejs.org/guide/migration/fragments.html#overview">Vue supports multi-root components</a> in version 3.
</p>
<h2 id="css">
Insufficient CSS skills
</h2>
<p>
A pattern I've noticed among students who start my beginner HTML and CSS course with previous knowledge of HTML is that they wrap almost everything in a <code>div</code>. When I ask them why, they usually say that they're adding them in case they might need them later. I know CSS pretty well and usually when I look at a design, I can tell if I will need extra elements for styling. Even if I miss something, it's much better to add some <code>div</code>s later than wrapping everything preemptively. Pro tip: Start with pen and paper and sketch out the structure of your site before you write a line of code. I usually use a combination of drawing boxes and writing Emmet pseudo code. If you make mistakes, you're making them on paper, which is easier to fix than changing actual code later.
</p>
<p>
By the way, you can get pretty far without using a single <code>div</code>. Just take a look at the source code of <a href="https://www.erikkroes.nl/">Erik’s</a> website.
</p>
<h2 id="defaultstyles">
Default styles
</h2>
<blockquote>
divs are so much easier to style than buttons.
</blockquote>
<p>
That might have been true in the past, but it's not anymore. Here's what you need to remove the default styles of a button.
</p>
<pre><code class="language-css">button {
all: initial;
}</code></pre>
<button type="button" style="all:initial" onclick="alert('Yes, that’s a button.')">Look mum, no styles.</button>
<p>If you want to reset everything, but inherit font styling, you need 2 more lines.</p>
<pre><code class="language-css">button {
all: initial;
font: inherit;
color: inherit;
}</code></pre>
<button type="button" style="all:initial; font: inherit; color: inherit" onclick="alert('Yes, that’s a button.')">Look mum, no styles.</button>
<h2 id="thepage">
We don't care enough about <em>the page</em>
</h2>
<p>
Many devs don't build pages anymore, they build components. I do that, too, because it makes sense as soon as your site reaches a certain level of complexity. Also, frameworks and pattern libraries think in terms of components which kind of forces you to do that, too.
</p>
<p>
The problem with working on small pieces is that it's easy to ignore the big picture. We can conceive, design, and develop components, but the end results for users is usually a whole page. A component with 2 or 3 <code>div</code>s might not look bad, but if I nest it in 5 other components, I might end up with 15 <code>div</code>s that don't do anything but increase complexity and file size.
</p>
<h2 id="style">Some elements are hard to style</h2>
<p>Yes, some elements are hard to style. To make our designers or customers happy, we don't use the default <code>select</code> element, but we write a complex component that consists of a bunch of divs with aria roles and a lot of JS. Sometimes have to do that, but sometimes it's better to just live with the fact that you can't style <code>option</code> elements and use the native component that comes with all the functionality and accessibility by default.</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<h2>So?! What's the point of this post?</h2>
<p>Honestly, I don’t know. I started writing and here we are. I guess I just needed to get these thoughts out of my head. If there’s one thing you can take away from this post, it’s that while HTML might not be the most complex frontend language, it’s the most important language with the biggest impact on users. Learning how to write HTML is not as hard as learning to write JavaScript, but learning how to write it in a way that it best possibly benefits users also takes time. The least we can do is to familiarize ourselves properly with the <a href="https://html.spec.whatwg.org/#semantics">elements and attributes</a> it provides us with.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CElement+diversity%E2%80%9D">blog@matuzo.at</a>.</p> Workshop: Deep Dive on Accessibility Testing2021-10-02T00:00:00+00:00https://www.matuzo.at/blog/workshop-deep-dive-on-accessibility-testing
<p>I’ve teamed up with my friends at <a href="https://www.smashingmagazine.com/">Smashing Magazine</a> 😻 to share with you everything I know about web accessibility testing! In this <a href="https://smashingconf.com/online-workshops/workshops/manuel-matuzovic/">smashing workshop</a> we’ll talk about automatic and manual testing, screen reader basics, Single Page Applications, Dev Tools, and more.</p><!-- teaser -->
<p>Sounds interesting? Great! Here are some more details about the workshop:</p>
<video src="/images/workshop_promo.mp4" controls>
<track default kind="captions" srclang="en" src="/images/workshop_promo.vtt" label="English">
<track default kind="subtitles" srclang="de" src="/images/workshop_promo_de.vtt" label="Deutsch">
Sorry, your browser doesn't support embedded videos.
</video>
<h2>What you will learn in this workshop</h2>
<ul><li>Which <strong>testing tools</strong> are available and most commonly used.</li><li>How to <strong>assess the accessibility</strong> of a component or page.</li><li>The difference between <strong>automatic and manual testing</strong>.</li><li>How to use <strong>automatic accessibility testing</strong> tools and how to interpret the results.</li><li>How to use the <strong>keyboard to discover accessibility bugs</strong>.</li><li><strong>Screen reader basics</strong> and how to use them for accessibility testing, both on desktop and mobile devices.</li><li>How to test the accessibility of <strong>Single Page Applications</strong>.</li><li>Common <strong>pitfalls of Single Page Applications</strong> and how to avoid them.</li><li>How to <strong>integrate accessibility testing</strong> in your day-to-day development workflow.</li><li>Running <strong>tests on the command line</strong> and creating automated reports.</li><li><strong>Integrating accessibility testing in your build pipeline</strong>.</li><li>Where to find <strong>information and help on how to build complex components</strong>.</li></ul>
<h2>Time and schedule</h2>
<p>This workshop is split over <strong>five days</strong>. The workshop sessions will run on the following days:</p>
<ul><li>Thu, November 4, <span class="small-caps">09:00 – 11:30 AM PT <strong>(17:00 – 19:30 CET)</strong></span></li><li>Fri, November 5, <span class="small-caps">09:00 – 11:30 AM PT <strong>(17:00 – 19:30 CET)</strong></span></li><li>Thu, November 11, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Fri, November 12, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Thu, November 18, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li></ul>
<h2>Ticket</h2>
<p>If you use this <a href="https://ti.to/smashingmagazine/online-workshops/discount/welcometomyworkshop">special link</a> you get a 15% discount on the original price.</p>
<p>If you have any questions about the workshop, feel free to get in touch <a href="manuel@matuzo.at">via mail</a>. I hope to see you soon!</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWorkshop%3A+Deep+Dive+on+Accessibility+Testing%E2%80%9D">blog@matuzo.at</a>.</p> A year in review: 20212022-01-09T00:00:00+00:00https://www.matuzo.at/blog/a-year-in-review
<p>2021 was wild! That’s why I’ve decided to write an “A year in review” post for the first time.</p><h2>Personal</h2>
<p>Let me start with the most important thing that happened in 2021: Our daughter Johanna was born on May 19th. I can’t describe how beautiful the entire experience was and still is. For the first two days, I wasn’t able to look at her for longer than 5 seconds without tearing up. Everything was just so emotional, beautiful, and overwhelming.</p>
<img alt="My fiance, me and 1 month old Johanna in front of a sunrise" loading="lazy" src="https://www.matuzo.at/media/pages/blog/a-year-in-review/3052307839-1698841493/baby.jpg">
<p>Don’t get me wrong, life with a baby is exhausting, especially compared to how our life was before, but the positive things outweigh the lack of sleep and the new fears you have as a parent. Watching her grow and learn new stuff every day is amazing.</p>
<p>Fun fact: I hid some Easter eggs on HTMHell regarding my daughter. The example I’m using in <a href="https://www.htmhell.dev/tips/iframe-accessibility/">Issue #1 - iframe accessibility</a> is <a href="https://www.youtube.com/watch?v=AwuCF5lYqEE">“Visions of Johanna” by Bob Dylan</a> and I’ve listed her as an executive producer in the credits of <a href="https://www.youtube.com/watch?v=52eL1CCJdi8">my first YouTube video</a>. </p>
<p>Another personal highlight of 2021 was our first vacation as a family in Croatia. We enjoyed the time at the sea and camping in our van a lot!</p>
<img alt="My fiance, me and 1 month old Johanna in front of a sunrise" loading="lazy" src="https://www.matuzo.at/media/pages/blog/a-year-in-review/669a89e90b-1698841493/baby_urlaub.jpg">
<h3>Conclusion personal</h3>
<p>My personal year 2021 was overall great. I’m getting tired of the whole pandemic situation, but my fiance and I enjoyed the time she was pregnant a lot and the rest of the year as well, of course. I was always bad at work-life-balance and this year it has changed little. While I could keep most weekends work-free and I spent them with my fiance and my daughter, I didn’t have enough time and energy left for the rest of my family and my friends.</p>
<h2>Work</h2>
<h3>Angular and performance</h3>
<p>In the first few months, I worked with a team of Angular devs on improving the performance of a large all-in-one productivity app. The job was challenging because most people involved were very skilled and I felt a lot of pressure to deliver visible improvements every week, but I learned a lot in these few weeks and I got to know super nice people. It was a joy to work with them.</p>
<h3>Cats</h3>
<p>I worked a lot with the Smashing Magazine team, which is a great honor to me because I’ve been a fan of Smashing Magazine and their work for many years. I became part of their <a href="https://www.smashingmagazine.com/about/#smashing-magazine-s-experts-panel">experts panel</a>, I <a href="https://vimeo.com/showcase/9087993/video/654497457">gave a talk at a meetup</a> and I held a <a href="https://smashingconf.com/online-workshops/workshops/manuel-matuzovic-apr/">workshop</a>.</p>
<h3>The City</h3>
<p>At my day job nothing too exceptional happened. This sounds negative, but it’s actually a good thing because we had time to focus on our new design system and the pattern library. One thing worth mentioning is that I worked quite a lot with <a href="https://craftcms.com/">CraftCMS</a> and twig. I like Craft because it gives me as a frontend developer a lot of freedom to build templates the way I want.</p>
<h3>Other</h3>
<p>Other than that I did some accessibility audits, consulting for an university in Vienna, and several workshops. My favorite gig was a 2-day workshop with the designers and developers of <a href="https://www.sipgate.de/">sipgate</a>. They were so interested, involved and eager to improve the accessibility of their products that it didn’t feel like work for me. I had a lot of fun and I also learned from them.</p>
<h3>Conclusion work</h3>
<p>Considering that I have a full-time job, that sounds like a lot, and it was. All in all, I’m happy with the quality of my work, but I was stressed out a lot this year, and I did fuck-up one or two tasks.</p>
<h2>Side projects</h2>
<p>I’m really happy with my side projects. I created some pretty cool and useful stuff.</p>
<ul>
<li><a href="https://github.com/matuzo/DevToolsSnippets">DevToolsSnippets</a>, a collection of front-end debugging script snippets to be used in the Sources panel in Chrome DevTools. </li>
<li><a href="https://www.buttoncheatsheet.com/">The Button Cheat Sheet</a></li>
<li>I started the <a href="https://www.htmhell.dev/newsletter/">HTMHell newsletter</a>.</li>
<li>I recorded the first video for the <a href="https://www.youtube.com/watch?v=52eL1CCJdi8">HTMHell YouTube channel</a>.</li>
<li>HTMHell Advent Calendar</li>
</ul>
<h2>Talks</h2>
<p>I didn’t take part in many calls for papers, but I prepared a new talk and I presented it at 3 events. You can watch <a href="https://vimeo.com/654497457">“Building The Most Inaccessible Site Possible with Manuel Matuzovic - Smashing Meets For All December 2021”</a> from <a href="https://vimeo.com/smashingmagazine">Smashing Magazine</a> on Vimeo. I gave the same talk at <a href="https://webdirections.org/aaa/index.php">Web Directions Access All Areas</a> (I highly recommend this event) and at the German version of <a href="https://www.youtube.com/watch?v=WFlb0wpXm0s&list=PLLuwoY7nxnR3AXdmA8JYdbjdFMKaereMr">technica11y</a>.</p>
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/VulKOpb6uCw?start=1691" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
<p>I also did a live stream with my friend Christoph, which was super fun. We tested the accessibility of his website <a href="https://larastreamers.com/">larastreamers</a>.</p>
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/Xr0r2Ft_ROQ?start=221" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
<h2>Articles</h2>
<p>I've written 30 blog posts this year, which means on average 2.5 per month. That's not too bad considering how much other stuff I was doing this year.</p>
<h3>Top 3</h3>
<p>If I had to pick 3 favorite articles, I'd go with:</p>
<ul>
<li><a href="https://www.matuzo.at/blog/html-boilerplate/">My current HTML boilerplate</a> on my blog</li>
<li><a href="https://www.htmhell.dev/tips/the-section-element/">Issue #8 - the section element</a> on <a href="https://www.htmhell.dev/">HTMHell</a></li>
<li><a href="https://www.htmhell.dev/26-tasty-buttons/">#26 HTMHell special: tasty buttons</a> on HTMHell</li>
</ul>
<h3>All posts</h3>
<p>Here's the complete list of articles:</p>
<ul>
<li><a href="https://css-tricks.com/meta-theme-color-and-trickery/">Meta Theme Color and Trickery</a> on <a href="https://css-tricks.com">CSS-Tricks</a></li>
<li><a href="https://www.smashingmagazine.com/2021/06/custom-emmet-snippets-vscode/">Creating Custom Emmet Snippets In VS Code</a> on <a href="https://www.smashingmagazine.com/">Smashing Magazine</a></li>
<li><a href="https://www.matuzo.at/blog/ordering-css-properties/">Ordering CSS properties</a> on my blog</li>
<li><a href="https://www.matuzo.at/blog/dev-tools-debugging-dom-tree-modifications/">Dev Tools: Debugging DOM Tree modifications</a> on my blog</li>
<li><a href="https://www.matuzo.at/blog/highlighting-columns/">Highlighting columns in HTML tables</a> on my blog</li>
<li><a href="https://www.matuzo.at/blog/html-boilerplate/">My current HTML boilerplate</a> on my blog</li>
<li><a href="https://www.matuzo.at/blog/element-diversity/">Element diversity</a> on my blog</li>
<li><a href="https://www.htmhell.dev/tips/">19 “Heaven” posts</a> on HTMHell</li>
<li><a href="https://www.htmhell.dev/">4 “Hell” posts</a> on HTMHell</li>
</ul>
<p>And I’ve translated an older article to German, which I’m planning to do again with other articles in 2022.</p>
<p>Read “<a href="https://www.matuzo.at/blog/de/testing-with-tab/">Eines meiner Lieblingswerkzeuge für Barrierefreiheit-Checks: Die Tabulator-Taste</a>.”.</p>
<h2>Misc</h2>
<p>I'm proud of this meme I made. 😄</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Me after a whole day of doing accessibility audits. <a href="https://t.co/qWZUFioAjX">pic.twitter.com/qWZUFioAjX</a></p>— Manuel Matuzović (@mmatuzo) <a href="https://twitter.com/mmatuzo/status/1413235099240370177?ref_src=twsrc%5Etfw">July 8, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<h2>Plans for 2022</h2>
<p>I didn’t spend much time thinking about my plans for 2022, but off the top of my head here’s what I want to do:</p>
<ul>
<li>Work less and enjoy life with my family and friends</li>
<li>Spend more time on writing for HTMHell</li>
<li>Record videos for HTMHell</li>
<li>Redesign my personal website</li>
<li>Finally update my profile pic</li>
</ul>
<p>I'm looking forward to 2022! 🤗🥳</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CA+year+in+review%3A+2021%E2%80%9D">blog@matuzo.at</a>.</p> Here’s what I didn’t know about :where()2022-01-25T00:00:00+00:00https://www.matuzo.at/blog/2022/heres-what-i-didnt-know-about-where
<p>This is part 4 of my series <a href="/blog/heres-what-i-didnt-know/">Here’s what I didn’t know about…</a> in which I try to learn new things about CSS. This time I'm trying to find out what I didn’t know about the <code>:where()</code> pseudo-class.</p><hr>
<p>Okay, I’ll be honest. When I heard about the <code>:where()</code> pseudo-class for the first time, I wasn’t impressed because reading a rule like the following hurt my brain. </p>
<pre><code class="language-css">:where(header, main, footer) p:hover {
color: red;
}</code></pre>
<p>It feels like using a shorthand property instead of longhand properties; fewer lines of code, but harder to process. I didn’t get the point, but a few days ago it clicked. </p>
<p>Here’s what I didn’t know about <code>:where()</code>:</p>
<h2>You can use it to lower the specificity of a selector</h2>
<p>I believe it was on <a href="https://piccalil.li/">Andys</a> blog where I saw this smart rule:</p>
<pre><code class="language-css">ul[class] {
margin: 0;
padding: 0;
list-style: none;
}</code></pre>
<p>If a <code><ul></code> has no <code>class</code>, it’s probably a list in the true sense, so leave it untouched and display bullets. If the list has a class, it’s semantically still a list (<a href="https://www.scottohara.me/blog/2019/01/12/lists-and-safari.html">in most browsers</a>), but it’s likely that it doesn’t look like one, so reset the styling. Yeah, I know, dangerous assumptions, but it worked well for me in the past. The only issue I had with this solution is the high specificity caused by the combination of a tag and a class selector. If I wanted to change one of the default properties using just a <code>class</code> selector, it wouldn’t work.</p>
<pre><code class="language-html"><ul class="space">
<li>HTML</li>
<li>CSS</li>
<li>JS</li>
</ul></code></pre>
<pre><code class="language-css">ul[class] {
margin: 0;
padding: 0;
list-style: none;
}
.space {
/* This doesn't apply because ul[class]
has higher specificity than .space */
margin: 2rem;
}</code></pre>
<p>Open <a href="https://codepen.io/matuzo/pen/BamayaR">the first example on CodePen</a>.</p>
<p>This is where <code>:where()</code> comes into play. No matter which selectors you pass to the pseudo-class, <code>:where()</code> will always have a specificity of 0.</p>
<pre><code class="language-css">ul:where([class]) {
margin: 0;
padding: 0;
list-style: none;
}
.space {
/* This applies because .space has higher
specificity than ul:where([class]) */
margin: 2rem;
}</code></pre>
<p>Even though the selector contains a tag, a pseudo-class and an attribute, only the tag selector adds to the specificity.</p>
<p>Open <a href="https://codepen.io/matuzo/pen/podovex">the second example on CodePen</a>.</p>
<p>Another great example is input styling (thanks for the tip, <a href="https://twitter.com/ckirknielsen/status/1484962960632123397">Christopher</a>).</p>
<pre><code class="language-css">input:where([type="text"],
[type="email"],
[type="password"]) {
border: 2px solid #000;
}
[aria-invalid] {
border-color: red;
}</code></pre>
<p>or if you're a fan of the <code>:not()</code> selector:</p>
<pre><code class="language-css">input:where(
:not(
[type="button"],
[type="reset"],
[type="image"]
)
) {
border: 2px solid #000;
}</code></pre>
<p>Open <a href="https://codepen.io/matuzo/pen/KKyKwor">the third example on CodePen</a>.</p>
<h2><code>:where()</code> is a forgiving selector</h2>
<p>If a list of selectors contains an invalid selector, none of the selectors will match.</p>
<pre><code class="language-html"><button>
Send
</button></code></pre>
<pre><code class="language-css">/* The background color won't change on :hover or :focus
because the invalid :touch pseudo-class makes the whole
list invalid */
button:focus,
button:hover,
button:touch {
background: red;
}</code></pre>
<style>
.btn1:focus,
.btn1:hover,
.btn1:touch {
background: red;
}
.btn2:where(:focus,
:hover,
:touch) {
background: red;
}
</style>
<p><button type="button" class="btn1">
Send
</button></p>
<p>Open <a href="https://codepen.io/matuzo/pen/eYeYmLq">example 4 on CodePen</a>.</p>
<p>If you use <code>:where()</code> instead, <code>:focus</code> and <code>:hover</code> will still match.</p>
<pre><code class="language-css">button:where(:focus,
:hover,
:touch) {
background: red;
}</code></pre>
<p><button type="button" class="btn2">
Send
</button></p>
<p>Open <a href="https://codepen.io/matuzo/pen/eYeYNNN">example 4 on CodePen</a>.</p>
<h2>Conclusion</h2>
<p>I haven’t used <code>:where()</code> in production yet, but considering that <a href="https://caniuse.com/mdn-css_selectors_where">browsers support</a> is pretty great, I’m going to try it out soon. The only concern I still have is the cognitive overload selectors like <code>:where()</code> or <code>:not()</code> create.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CHere%E2%80%99s+what+I+didn%E2%80%99t+know+about+%3Awhere%28%29%E2%80%9D">blog@matuzo.at</a>.</p> CSS Specificity Demo2022-01-26T00:00:00+00:00https://www.matuzo.at/blog/2022/specificity
<p>I built an interactive demo to illustrate how specificity in CSS works.</p><style>
main {
background: rgb(250 250 250 /0.6) !important;
}
</style>
<p>Press the “Add selector” and “Remove selector” buttons to add or remove a selector in the list of declarations and see how the background color changes accordingly. Each selector will be added to the top of the list to prove that it has a higher specificity than the previous selector.</p>
<div role="alert" class="u-vh"></div>
<button id="remove" disabled type="button">
Remove selector
</button>
<button id="add" type="button">
Add selector
</button>
<script>
document.body.classList.add('body')
document.body.setAttribute('id', 'body')
document.documentElement.classList.add('step0')
let step = 0;
const messages = [
'Selector: :where(body), background-color: gray',
'Selector: body, background-color: red',
'Selector: .body, background-color: blue',
'Selector: body.body, background-color: green',
'Selector: .body.body, background-color: orange',
'Selector: #body, background-color: brown',
'Selector: body#body, background-color: fuchsia',
'Selector: .body#body, background-color: salmon',
'Selector: #body#body, background-color: aqua',
'Selector: #body:is(#body, #bla#bla), background-color: hotpink',
'Selector: div with animated background-color, background-color: yellow',
'Selector: body with !important, background-color: rebeccapurple'
]
const remove = document.getElementById('remove');
const add = document.getElementById('add');
remove.addEventListener('click', () => {
if (step > 0) {
add.removeAttribute('disabled')
step--;
document.documentElement.className = `step${step}`
document.querySelector('[role="alert"]').textContent = messages[step]
}
if (step === 0) {
remove.setAttribute('disabled', 'disabled')
}
})
add.addEventListener('click', () => {
if (step < messages.length) {
remove.removeAttribute('disabled')
step++;
document.documentElement.className = `step${step}`
document.querySelector('[role="alert"]').textContent = messages[step]
}
if (step === messages.length - 1) {
add.setAttribute('disabled', 'disabled')
}
})
</script>
<style>
button {
background: #80d2db;
border: 2px solid #153a51;
color: #153a51;
padding: 0.7em 0.8em 0.5em;
font-family: inherit;
font-size: 0.9rem;
font-weight: bold;
}
button:not([disabled]):hover {
background: #153a51;
color: #fff;
}
button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
pre[class*="language-"] {
margin-top: 0;
padding: 1rem;
border: none;
}
pre[class*="language-"]:not(:last-of-type) {
margin-bottom: 0;
}
header {
background: #efefef;
}
.banner {
background: #d9dddf;
}
main {
background: #fafafa;
}
pre[class*="language-"] {
display: none;
}
@keyframes bg {
to {
background: yellow;
}
}
.step11 body { background: rebeccapurple !important; }
.step10 body { animation: bg 0s forwards; }
.step9 #body:is(#body, #bla#bla) { background: hotpink; }
.step8 #body#body { background: aqua; }
.step7 .body#body { background: salmon; }
.step6 body#body { background: fuchsia; }
.step5 #body { background: brown; }
.step4 .body.body { background: orange; }
.step3 body.body { background: green; }
.step2 .body { background: blue; }
.step1 body { background: red; }
.step0 :where(body) { background: #efefef; }
.step0 pre[class*="language-"]:nth-last-of-type(1),
.step1 pre[class*="language-"]:nth-last-of-type(-n+2),
.step2 pre[class*="language-"]:nth-last-of-type(-n+3),
.step3 pre[class*="language-"]:nth-last-of-type(-n+4),
.step4 pre[class*="language-"]:nth-last-of-type(-n+5),
.step5 pre[class*="language-"]:nth-last-of-type(-n+6),
.step6 pre[class*="language-"]:nth-last-of-type(-n+7),
.step7 pre[class*="language-"]:nth-last-of-type(-n+8),
.step8 pre[class*="language-"]:nth-last-of-type(-n+9),
.step9 pre[class*="language-"]:nth-last-of-type(-n+10),
.step10 pre[class*="language-"]:nth-last-of-type(-n+11),
.step11 pre[class*="language-"]:nth-last-of-type(-n+12) {
display: block;
}
body {
transition: background 0.5s;
}
</style>
<pre><code class="language-css">/* !important overwrites everything */
body {
background: rebeccapurple !important;
}</code></pre>
<pre><code class="language-css">/* Animation declarations take precedence over normal
declarations */
body {
animation: bg 0s forwards;
}
@keyframes bg {
to {
background: yellow;
}
}</code></pre>
<pre><code class="language-css">/* any selector with higher specificity, even if it doesn't
match any element, inside :is() overwrites id + id */
#body:is(#body, #bla#bla) {
background: hotpink;
}</code></pre>
<pre><code class="language-css">/* id + id overwrites id + class */
#body#body {
background: aqua;
}</code></pre>
<pre><code class="language-css">/* id + class overwrites id + tag */
.body#body {
background: salmon;
}</code></pre>
<pre><code class="language-css">/* id + tag overwrites id */
body#body {
background: fuchsia;
}</code></pre>
<pre><code class="language-css">/* id overwrites classes */
#body {
background: brown;
}</code></pre>
<pre><code class="language-css">/* class + class overwrites tag + class */
.body.body {
background: orange;
}</code></pre>
<pre><code class="language-css">/* tag + class overwrites class */
body.body {
background: green;
}</code></pre>
<pre><code class="language-css">/* class overwrites tag */
.body {
background: blue;
}</code></pre>
<pre><code class="language-css">/* tag selector overwrites universal :where() selector */
body {
background: red;
}</code></pre>
<pre><code class="language-css">/* universal :where() selector */
:where(body) {
background: #efefef;
}</code></pre>
<h2>Resources</h2>
<ul>
<li><a href="https://codepen.io/matuzo/pen/dyZyrRw?editors=1100">Demo on CodePen</a></li>
<li><a href="https://www.w3.org/TR/css-cascade-5/#cascade-origin">Cascade Sorting Order</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:where">:where() on MDN</a></li>
<li><a href="https://codepen.io/t_afif/pen/QWOWKXY">specificity animation hack</a></li>
<li><a href="https://specifishity.com/">SpeciFISHity</a></li>
<li><a href="https://wattenberger.com/blog/css-cascade#specificity">The CSS Cascade</a></li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CCSS+Specificity+Demo%E2%80%9D">blog@matuzo.at</a>.</p> Cascade Layers: First Contact2022-01-28T00:00:00+00:00https://www.matuzo.at/blog/2022/cascade-layers
<p>Earlier this week I learned about <a href="https://www.w3.org/TR/css-cascade-5/#layering">CSS Cascade Layers</a> and now I’m all hyped up because I really like the concept. I’m eager to find out how we can use them to improve and rethink the architecture of our styles.</p><p>I will not explain how CSS Cascade Layers work because Bramus and Stephanie have already done that and they did it much better than I ever could. I just want to get my feet wet and share my first impressions. If you’re new to the topic, read their articles first.</p>
<ul>
<li><a href="https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/">Getting Started With CSS Cascade Layers</a> by Stephanie Eckles</li>
<li><a href="https://www.bram.us/2021/09/15/the-future-of-css-cascade-layers-css-at-layer/">The Future of CSS: Cascade Layers (CSS @layer)</a> by Bramus van Damme</li>
</ul>
<p>While I’m writing this blog post, I’m looking at a large site I’ve been working on and I’m trying to find scenarios in which cascade layers could’ve been useful.</p>
<h2>Overly specific base styles</h2>
<p>If you have a layer for base styles and another for your components, you don’t have to worry about selectors with high specificity in the base layer anymore.</p>
<pre><code class="language-css">@layer base, components;
@layer base {
ul[class] {
margin: 0;
padding: 0;
list-style-type: none;
}
input[type="text"] {
border: 1px solid #000;
}
}
@layer components {
.nav {
margin-left: 40px;
}
.error {
border: 2px dashed #F00;
}
}</code></pre>
<p>Rules in <code>.nav</code> and <code>.error</code> overwrite rules in <code>ul[class]</code> and <code>input[type="text"]</code> even though the specificity of the latter is higher.</p>
<p>Try it yourself:</p>
<ul>
<li><a href="https://codepen.io/matuzo/pen/mdqJqGo">“Base Styles with Cascade Layers” on CodePen</a></li>
<li><a href="https://codepen.io/matuzo/pen/LYOVOqB">“Base Styles without Cascade Layers” on CodePen</a></li>
</ul>
<p>Hint: You have to enable support for Cascade Layers in your browser, they’re still <a href="https://caniuse.com/css-cascade-layers">behind a flag</a>.</p>
<h2>Third party styles</h2>
<p>One of the third-party tools we’re using uses highly specific selectors. In some places we’re overwriting rules and we either have to use selectors with the same specificity 🤢 or <code>!important</code> 🤮.</p>
<p>I imagine having a dedicated layer for third party styles could make sense. Within this layer, you nest other layers, one or more for the styles you import and another one for your custom overwrites.</p>
<pre><code class="language-css">@layer base, third-party, components;
@layer base {
/* Base styles */
}
@layer third-party {
@layer slider {
.slider.slider-horizontal > .slider-item {
background: #fefefe;
padding: 10px;
border: 1px solid #000;
}
}
/*
In reality 3rd party styles would probabaly live in separate files
@import url(slider.css) layer(third-party);
*/
@layer overwrites {
.slider-item {
border-color: #F00;
}
}
}
@layer components {
/* Component styles */
}</code></pre>
<ul>
<li><a href="https://codepen.io/matuzo/pen/NWwqwVx">“3rd party styles with Cascade Layers” on CodePen</a></li>
</ul>
<h2>Utility classes</h2>
<p>Having a dedicated layer for utility classes feels way more intuitive than having to apply <code>!important</code> to each rule in a utility class.</p>
<pre><code class="language-html"><ul id="list" class="u-df">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul></code></pre>
<pre><code class="language-css">@layer base, components, utility;
@layer base {
/* Base styles */
}
@layer components {
#list {
display: block;
list-style-type: none;
padding: 0;
}
}
@layer utility {
.u-df {
display: flex;
}
}</code></pre>
<p><a href="https://codepen.io/matuzo/pen/JjOdMKe">“Utility classes with Cascade Layers” on CodePen</a></p>
<h2>Conclusion</h2>
<p>This all makes sense to me in theory, but I guess we’ll see how useful layers will be in reality once support gets better and more people begin experimenting. Either way, I’m really stoked about the progress happening with layers and new selectors like <code>:is()</code> and <a href="/blog/2022/heres-what-i-didnt-know-about-where/">:where()</a>. Super useful stuff that will make the lives of those specialized in CSS easier. I don’t believe that it will make everyone’s lives easier because with features like these CSS becomes more powerful, but this also increases its complexity. Mastering CSS is already pretty hard, but it will take even more in the future to become really good at writing CSS.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CCascade+Layers%3A+First+Contact%E2%80%9D">blog@matuzo.at</a>.</p> Web Security Basics: XSS2022-02-16T00:00:00+00:00https://www.matuzo.at/blog/2022/web-security-basics-xss
<p>I decided to learn more about areas of web development I don’t know a lot about. You know,…stuff like SEO and web security. I’ll share my findings here on my blog and I’ll try to do as much research as possible, but please keep in mind that I’m a noob concerning these topics.</p><p>I began watching <a href="https://feross.org/">Feross Aboukhadijeh</a>’s fantastic <a href="https://www.youtube.com/watch?v=5JJrJGZ_LjM&list=PL1y1iaEtjSYiiSGVlL1cHsXN_kvJOOhu-&index=1">Web Security lecture</a>, which inspired me to learn more about web security. This first post is about Cross Site Scripting (XSS). </p>
<h2>XSS</h2>
<p>XSS describes the practice of injecting malicious code into an otherwise trusted website, or, in other words, injecting JavaScript into an HTML document. The point of XSS is that the attacker can execute JavaScript on a page by supplying untrusted data and do stuff they otherwise wouldn’t be able to do. For example, reading users’ cookies and sending HTTP requests with these cookies.</p>
<p>There are two types of XSS attacks: server and client XSS.</p>
<h3>Server XSS</h3>
<p>Server XSS occurs when the vulnerability is in the server-side code and the browser renders and executes the HTTP response generated by the server, which includes the user supplied untrusted data. For example, this can happen when a hacker exploits the comment field in a blog post by using it to save JavaScript in the database. This JavaScript code then runs every time anyone opens the page with the “comment”.</p>
<h3>Client XSS</h3>
<p>Client XSS occurs when the vulnerability is in the client-side code and untrusted user supplied data is used to update the DOM (Document Object Model) with an unsafe JavaScript call. For example, hackers could send out links with malicious <code>GET</code> parameters that inject JavaScript into the DOM when the user opens the link.</p>
<p>You can learn more about the different types of XSS and more detailed definitions on the <a href="https://owasp.org/www-community/Types_of_Cross-Site_Scripting">owasp - Types of XSS</a> page.</p>
<h2>Hack the Planet!</h2>
<p>Okay, now let me show how this may look like in practice.</p>
<h3>A simple PHP search form</h3>
<p>Let’s say we have a form that allows users to search for a custom term.</p>
<pre><code class="language-html"><form role="search" method="GET">
<label for="term">Search term</label>
<input type="text" id="term" name="q">
<button>search</button>
</form></code></pre>
<img alt="A input field labelled “Search term” and a search button." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/web-security-basics-xss/34ed16ce46-1698950563/sec1_demo1_1.jpg" width="500">
<p>When the user enters a term and submits the form, we show the results and a message that says “Showing results for [TERM]”.</p>
<pre><code class="language-php"><?php
if (isset($_GET['q'])):
echo "<p>Showing results for “<mark class=\"term\">".$_GET['q']."</mark>”</p>";
?>
<ol>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ol>
<?php endif; ?></code></pre>
<img alt="A Search form that has been submitted showing some dummy results for the term “flowers”." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/web-security-basics-xss/efb49a52c5-1698950563/sec1_demo1_2.jpg" width="500">
<p>Since we’re using the <code>GET</code> method, the URL changes <code>/search?q=flowers</code>, too. This means that users can share the page with the search query pre-filled. This is where it gets dangerous because instead of searching for a simple string, someone could enter JavaScript.</p>
<img alt="A search form, not yet submitted, with “<script>Zer0 Cool was here</script>” as the search value" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/web-security-basics-xss/3bfa65884b-1698950563/sec1_demo1_3.jpg" width="500">
<p>If you press <kbd>Enter</kbd>, PHP renders the code on the server which means that it also interprets the user supplied JavaScript. If you share the url <code>/search?q=%3Cscript%3Ealert%28%22Zer0+cool+was+here%22%29%3C%2Fscript%3E</code> with someone, the JavaScript in the GET parameter will run in their browser.</p>
<img alt="An alert with the message “Zer0 Cool was here” on the search page." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/web-security-basics-xss/ff3efcd968-1698950562/sec1_demo1_4.jpg" width="500">
<h3>A simple JS search form</h3>
<p>Now let’s try the same on a static page with JavaScript.</p>
<pre><code class="language-html"><!-- Search form -->
<form role="search" method="GET">
<label for="term">Search term</label>
<input type="text" id="term" name="q">
<button>search</button>
</form>
<!-- Results, hidden by default -->
<div data-results hidden>
<p>
Showing results for “<mark class="term"></mark>”
</p>
<ol>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ol>
</div>
<!-- Get search parameters and show the results -->
<script>
let params = (new URL(document.location)).searchParams;
let q = params.get('q');
if (q) {
document.querySelector('[data-results]').removeAttribute('hidden')
document.querySelector('input').value = q
document.querySelector('.term').innerHTML = q
}
</script></code></pre>
<p>You can see this in action in the <a href="https://matuzo-test1.netlify.app/search">simple JS search form demo</a>.</p>
<p>If we try to search for a string that contains JavaScript <code><script>alert("Cereal Killer was here")</script></code> on this static page, the alert doesn't pop up. This is because the HTML string is injected and parsed, but the JavaScript doesn’t execute. </p>
<img alt="The script tag is visible in dev tools but the search term is empty and there's no alert." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/web-security-basics-xss/b78ca2c981-1698950563/sec1_demo2_1.jpg" width="500">
<p>This is by design:</p>
<div class="quote">
<blockquote>When inserted using the document.write() method, script elements usually execute (typically blocking further script execution or HTML parsing). When inserted using the innerHTML and outerHTML attributes, they do not execute at all.</blockquote>
<p><a href="https://html.spec.whatwg.org/#the-script-element">The script element</a></p>
</div>
<p>I was trying really hard “to make this work” and the only two ways I found were outdated or obviously wrong.</p>
<p><code>document.writeln()</code> writes to a document and executes the code.</p>
<pre><code class="language-js">document.writeln(q)</code></pre>
<p>The last time I've used <code>document.writeln()</code> or <code>document.write()</code> was more than a decade ago, but it's still good to know.</p>
<p><code>eval()</code> would execute Javascript if you pass it directly like <code>alert("Cereal Killer was here")</code>, but you know what they say, <code>eval()</code> is evil, so don't use it.</p>
<pre><code class="language-js">document.querySelector('.term').innerHTML = eval(q)</code></pre>
<p>Injecting a <code><script></code> tag in a static page seems to be harmless, but there are ways to execute JavaScript without using the <code><script></code> element.</p>
<p>Consider the following “search term”:<br />
<strong><img src="idontexist.jpg" onerror="alert('Acid Burn was here')"></strong>.</p>
<p>Here's what happens when someone tries to submit this search query: <code>innerHTML</code> injects the string and parses it as HTML. The browser tries to load <code>idontexist.jpg</code>, but fails because the image doesn't exist on the server. This triggers the <a href="https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror">onerror event</a> which fires when a resource fails to load. The event then runs our JavaScript. </p>
<img alt="An alert with the message Acid Burn was here” on the search page." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/web-security-basics-xss/b02d907b6b-1698950563/sec1_demo2_2.jpg" width="500">
<p>Try it yourself <a href="https://matuzo-test1.netlify.app/search">in this demo</a>.</p>
<h2>XSS prevention</h2>
<p>The are many things you should and could do to prevent XSS attacks. I suggest you read the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#xss-prevention-rules">XSS Prevention Rules</a> on the owasp website. Most importantly, you should control where users can enter data and you should escape/HTML encode the data. This means turning critical characters like <code>&</code>, <code><</code>, <code>></code>,<code>"</code>, and <code>'</code> into entities. You can use a package like <a href="https://www.npmjs.com/package/escape-html">escape-html</a> for that.</p>
<pre><code class="language-html"><script type="module">
import { escapeHtml } from './escape-html.mjs';
let params = (new URL(document.location)).searchParams;
let q = params.get('q');
if (q) {
document.querySelector('[data-results]').removeAttribute('hidden')
document.querySelector('input').value = q
document.querySelector('.term').innerHTML = escapeHtml(q)
}
</script></code></pre>
<img alt="A input field labelled “Search term” and a search button." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/web-security-basics-xss/1f068824b4-1698950563/sec1_demo2_3.jpg" width="710">
<p>Try it yourself in the <a href="https://matuzo-test1.netlify.app/search2?q=%3Cimg+src%3D%22idontexist.jpg%22+onerror%3D%22alert%28%27Acid+Burn+was+here%27%29%22%3E">search form with escaped content demo</a>.</p>
<h2>Alerta Alerta</h2>
<p>Most examples of XSS you find online use <code>alert()</code> to demonstrate vulnerabilities. You can’t do much harm with <code>alert()</code>, it’s just a nice way of showing that JavaScript can be injected. There are other dangerous methods you can execute, though. I’ll show you in the next blog post how XSS can be used to steal cookies.</p>
<p>As already mentioned, I'm pretty new to this topic. If I got something wrong or if I missed something, please get in touch <a href="mailto:manuel@matuzo.at">via e-mail</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWeb+Security+Basics%3A+XSS%E2%80%9D">blog@matuzo.at</a>.</p> Bonn Meetup 7: Introduction to web accessibility and deceitful Lighthouse scores2022-02-21T00:00:00+00:00https://www.matuzo.at/blog/2022/bonncode7
<p>This week I’ll be speaking at the Bonn Code Meetup about accessibility testing. I’m joining Konstantin Tieber who’ll talk about the What, Why, Who and How of building accessible web applications. I’ll show you how I built “The Most Inaccessible Site Possible With A Perfect Lighthouse Score”.</p><p><a href="https://www.meetup.com/de-DE/Bonn-Code/events/283995153/">Join us Online</a> at 7pm CEST on Wednesday, February 23rd</p>
<h2>Schedule</h2>
<p>19:00 - 19:15 Arrival, get to know each other<br />
19:15 - 20:00 Talk 1: Building accessible web applications: What, Why, Who and How<br />
20:00 - 20:10 Discussion and Q&A<br />
20:10 - 20:45 Talk 2: Building The Most Inaccessible Site Possible With A Perfect Lighthouse Score<br />
20:45 - open end Discussion and Q&A</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CBonn+Meetup+7%3A+Introduction+to+web+accessibility+and+deceitful+Lighthouse+scores%E2%80%9D">blog@matuzo.at</a>.</p> Please, stop disabling zoom2022-05-05T00:00:00+00:00https://www.matuzo.at/blog/2022/please-stop-disabling-zoom
<p>I know that you’re not supposed to tell people what to do, but in this particular case I’m really tempted because recently I’ve noticed that a lot of websites are preventing users on mobile to zoom.</p><p>I don’t know whether this is a trend, or just a coincidence, but I see a lot of these meta tags on sites:</p>
<pre><code class="language-html"><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> </code></pre>
<pre><code class="language-html"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> </code></pre>
<pre><code class="language-html"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> </code></pre>
<p>This prevents users from being able to zoom a page on some browsers and operating systems. This can have serious negative consequences for people with low vision, elderly people and pretty much anyone who has to or wants to zoom in. If you want to see how problematic this is, visit <a href="https://www.espn.com">espn.com</a>, <a href="https://www.duesseldorf.de">duesseldorf.de</a>, <a href="https://www.thesun.co.uk">thesun.co.uk</a>, <a href="https://www.discord.com">discord.com</a>, or <a href="https://www.dhl.de">dhl.de</a>, just to name a few.</p>
<p>According to the <a href="https://almanac.httparchive.org/en/2021/accessibility#zooming-and-scaling">HTTP Archive Web Alamac</a>, 24% of desktop homepages and 29% of mobile homepages attempt to disable scaling by setting either <code>maximum-scale</code> to a value less than or equal to 1, or <code>user-scalable</code> set to 0 or none. These numbers are way too high.</p>
<p><strong>So, please stop disabling zoom!</strong></p>
<p>Based on some quick tests by me and <a href="https://twitter.com/mmatuzo/status/1522107174230573056">friendly people on Twitter</a>, Safari seems to ignore <code>maximum-scale=1</code> and <code>user-scalable=no</code>, which is great, but it still works on Chrome and Firefox on Android. Samsung Internet on Android seems to ignore it, too.</p>
<p>As a user, you can force allow zooming: </p>
<p>In Firefox find the settings, select “Accessibility” and activate “Zoom on all website”<br />
In Chrome find the settings, select “Accessibility” and check “Force enable zoom”</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CPlease%2C+stop+disabling+zoom%E2%80%9D">blog@matuzo.at</a>.</p> Divs are bad!2022-05-11T00:00:00+00:00https://www.matuzo.at/blog/2022/divs-are-bad
<p>Yes, clickbait, I’m so sorry! Of course, divs are not bad. For example, they can be really useful,…</p>
<ul>
<li>…when you need additional elements for styling.</li>
<li>…for structuring content, when there’s no other suitable element.</li>
<li>…when you need custom <a href="https://www.w3.org/TR/wai-aria-practices/examples/landmarks/HTML5.html">landmarks</a>.</li>
</ul>
<p>Even though there’s nothing wrong with the <code>div</code> per se, some people, including me, still like to complain when they’re not used consciously.</p><p>The issue with <code>div</code>s is not the quantity, although a large <a href="https://www.youtube.com/watch?v=kLm0grHWHxI">DOM can affect performance</a> negatively, and an unnecessarily large document is harder to read and debug. The real problem is the placement. If you put a <code>div</code> in the wrong place, it can have serious negative side effects. </p>
<p>Here are some examples:</p>
<h2>details and summary</h2>
<p>If you wrap the contents of a <code>details</code> element in a <code>div</code>, the browser or a screen reader might not recognize the <code>summary</code> element properly and display and announce a fallback text instead.</p>
<p class="code-label">
<strong>Wrong</strong>
</p>
<pre><code class="language-html"><details>
<div>
<summary>Show info</summary>
Hi, I'm the info!
</div>
</details></code></pre>
<!-- [html-validate-disable element-required-content] -->
<details>
<div>
<summary>Show info</summary>
Hi, I'm the info!
</div>
</details>
<p class="code-label">
<strong>Right</strong>
</p>
<pre><code class="language-html"><details>
<summary>Show info</summary>
Hi, I'm the info!
</details></code></pre>
<details>
<summary>Show info</summary>
Hi, I'm the info!
</details>
<h2>figure</h2>
<p>If you wrap the contents of a <code>figure</code> element in a <code>div</code>, the <code>figcaption</code> might not be recognized as the caption for the <code>figure</code>. Visually this probably has no effect, but the <code>figure</code> might not be exposed to assistive technology when it doesn't have an accessible name.</p>
<p class="code-label">
<strong>Wrong</strong>
</p>
<pre><code class="language-html"><figure>
<img src="/images/gus.jpg" width="400" alt="Gus Polinski.">
<div>
<figcaption>
The Polka King of the Midwest talking to a desperate mother.
</figcaption>
</div>
</figure></code></pre>
<figure>
<img alt="Gus Polinski, the Polka King of the Midwest talking to a desperate mother." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/divs-are-bad/6ae89fba7d-1698950578/gus.jpg" width="400">
<div>
<figcaption>
The Polka King of the Midwest talking to a desperate mother.
</figcaption>
</div>
</figure>
<p class="code-label">
<strong>Right</strong>
</p>
<pre><code class="language-html"><figure>
<img src="/images/gus.jpg" width="400" alt="Gus Polinski.">
<figcaption>
The Polka King of the Midwest talking to a desperate mother.
</figcaption>
</figure></code></pre>
<figure>
<img alt="Gus Polinski" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/divs-are-bad/6ae89fba7d-1698950578/gus.jpg" width="400">
<figcaption>
The Polka King of the Midwest talking to a desperate mother.
</figcaption>
</figure>
<h2>fieldsets</h2>
<p>A <code>fieldset</code> gets its label from the <code>legend</code> element. If the <code>legend</code> is wrapped in a <code>div</code>, the <code>fieldset</code> has no accessible name. This may cause the group of radio buttons not being announced as a group and the <code>legend</code> might not have a sematic relation with the radio buttons. The visual appearance breaks as well. The legend isn’t positioned on the border of the fieldset, but below it.</p>
<p class="code-label">
<strong>Wrong</strong>
</p>
<pre><code class="language-html"><fieldset>
<div>
<legend>Shirt sizes</legend>
<div>
<input type="radio" id="l" name="shirt1">
<label for="l">Large</label>
</div>
<div>
<input type="radio" id="m" name="shirt1">
<label for="m">Medium</label>
</div>
</div>
</fieldset></code></pre>
<!-- [html-validate-disable wcag/h71] -->
<fieldset>
<div>
<legend>Shirt sizes</legend>
<div>
<input type="radio" id="l" name="shirt1">
<label for="l">Large</label>
</div>
<div>
<input type="radio" id="m" name="shirt1">
<label for="m">Medium</label>
</div>
</div>
</fieldset>
<p class="code-label">
<strong>Right</strong>
</p>
<pre><code class="language-html"><fieldset>
<legend>Shirt sizes</legend>
<div>
<input type="radio" id="l2" name="shirt2">
<label for="l2">Large</label>
</div>
<div>
<input type="radio" id="m2" name="shirt2">
<label for="m2">Medium</label>
</div>
</fieldset></code></pre>
<fieldset>
<legend>Shirt sizes</legend>
<div>
<input type="radio" id="l2" name="shirt2">
<label for="l2">Large</label>
</div>
<div>
<input type="radio" id="m2" name="shirt2">
<label for="m2">Medium</label>
</div>
</fieldset>
<h2>lists</h2>
<p><code>ul</code> and <code>ol</code> must only directly contain <code>li</code>, <code>script</code> or <code>template</code> elements. Wrapping all list items in a <code>div</code> may change the way list items get announced by a screen reader.</p>
<p class="code-label">
<strong>Wrong</strong>
</p>
<pre><code class="language-html"><ul>
<div>
<li>A</li>
<li>B</li>
<li>C</li>
</div>
</ul></code></pre>
<ul>
<div>
<li>A</li>
<li>B</li>
<li>C</li>
</div>
</ul>
<p class="code-label">
<strong>Right</strong>
</p>
<pre><code class="language-html"><ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul></code></pre>
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
<p><em>A quick side note</em>: It’s fine to use divs in a definition list (<code>dl</code>).</p>
<p class="code-label">
<strong>Right</strong>
</p>
<pre><code class="language-html"><dl>
<div>
<dt>Key:</dt>
<dd>Value</dd>
</div>
<div>
<dt>Key:</dt>
<dd>Value</dd>
</div>
</dl></code></pre>
<style>
dl div {
display: flex;
gap: 1rem;
}
</style>
<dl>
<div>
<dt>Key:</dt>
<dd>Value</dd>
</div>
<div>
<dt>Key:</dt>
<dd>Value</dd>
</div>
</dl>
<h2>paragraphs</h2>
<p>If you put a <code>div</code> in a <code>p</code>, it breaks the paragraph. For example, it could cause the browser to close the paragraph implicitly, render the content outside of it, and add another paragraph. </p>
<p>This is especially evident, if you try to style the paragraph, e.g. by adding a red border.</p>
<style>
.broken-p p {
border: 2px solid red;
}
</style>
<pre><code class="language-css">p {
border: 2px solid red;
}</code></pre>
<p class="code-label">
<strong>Wrong</strong>
</p>
<pre><code class="language-html"><p>
<div>I'm wrapped in a div.</div>
I'm not wrapped in a div.
</p></code></pre>
<p class="code-label">
<strong>May result in:</strong>
</p>
<pre><code class="language-html"><p></p>
<div>I'm wrapped in a div.</div>
I'm not wrapped in a div.
<p></p></code></pre>
<!-- [html-validate-disable no-implicit-close, close-order] -->
<div class="broken-p">
<p>
<div>I'm wrapped in a div.</div>
I'm not wrapped in a div.
</p>
</div>
<p class="code-label">
<strong>Right</strong>
</p>
<pre><code class="language-html"><p>
I'm wrapped in a div.<br>
I'm not wrapped in a div.
</p></code></pre>
<div class="broken-p">
<p>
I'm wrapped in a div.<br>
I'm not wrapped in a div.
</p>
</div>
<hr>
<p>I’ve seen all these wrong implementations (or variations of them) on several websites.</p>
<h2>How do we prevent that?</h2>
<p>Before launching a site, when you deploy changes to an existing component or page or when you add a new component, validate the rendered markup. You can use the <a href="https://dequeuniversity.com/validator">validation bookmarklet</a> by Deque for that. It works both with server- and client-side rendered pages. </p>
<p>If you’re working with a JavaScript library or framework that demands that you wrap all items within the component in a parent element, before immediately going for the <code>div</code>, check first if you can use a <a href="https://reactjs.org/docs/fragments.html">fragment</a> instead or if there’s a more suitable semantic element you could use.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDivs+are+bad%21%E2%80%9D">blog@matuzo.at</a>.</p> Analyzing pages in a particular state with Lighthouse2022-06-27T00:00:00+00:00https://www.matuzo.at/blog/2022/lighthouse-snapshot
<p>Historically, Lighthouse has analyzed the cold pageload of a page only. Clicking the “Generate report” button reloads the page before Lighthouse runs its tests. This can be problematic when you want to run tests on parts of the UI that are only visible when the user interacts with it. For example, a fly-out navigation, a modal window, or the content in a disclosure widget.</p><p>That has changed with Lighthouse v10. A new <a href="https://developer.chrome.com/blog/new-in-devtools-103/#lighthouse">experimental feature in Chrome DevTools</a> allows us now to analyze the page in a particular state. Here’s an example:</p>
<p><a href="/images/lh-snapshot1.webp"></p>
<img alt="Browser screenshot. A button with the label “show”" height="484" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/lighthouse-snapshot/87103765ca-1698950571/lh-snapshot1.webp" width="751">
</a>
<p>When you click the “Show” button in this <a href="https://cdpn.io/pen/debug/mdXNMzQ">simple disclosure widget</a>, an image appears. </p>
<p><a href="/images/lh-snapshot2.webp"></p>
<img alt="Button and a random image visible below it. Browser screenshot." height="484" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/lighthouse-snapshot/e25105542f-1698950570/lh-snapshot2.webp" width="751">
</a>
<p>The image has no <code>alt</code> attribute and Lighthouse should report an error, but it reloads the page, and the image is gone when it runs the tests, because the state of the page doesn't persist.</p>
<p><a href="/images/lh-snapshot3.webp"></p>
<img alt="Page after the reload with Lighthouse open. Just the button visible and a score of 100 in the accessibility category" height="484" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/lighthouse-snapshot/3f49b51c37-1698950571/lh-snapshot3.webp" width="751">
</a>
<p>Starting with Lighthouse v10, you can also run tests on snapshots of a page. First, you have to enable this feature because it’s still an experiment: Open Chrome DevTools, press <kbd>F1</kbd>, click “Experiments”, and tick the checkbox “Use Lighthouse panel with timespan and snapshot modes”.</p>
<p><a href="/images/lh-snapshot4.webp"></p>
<img alt="The experiments page in Chrome Dev Tools with the timespan and snapshot modes option highlighted." height="484" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/lighthouse-snapshot/7ce0ae81b4-1698950570/lh-snapshot4.webp" width="751">
</a>
<p>Now Reload Chrome DevTools and open Lighthouse again. There’s a new column “Mode”. Select “snapshot” and run another Lighthouse test.</p>
<p><a href="/images/lh-snapshot5.webp"></p>
<img alt="Button and image on the left hand side. Lighthouse panel in Dev Tools with the new options on the right" height="484" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/lighthouse-snapshot/abbf100599-1698950570/lh-snapshot5.webp" width="751">
</a>
<p>Lighthouse doesn’t reload the page, but it checks it as it is in its current state. Instead of a score, it now shows how many tests of the total number of applicable tests the page has passed.</p>
<p><a href="/images/lh-snapshot6.webp"></p>
<img alt="Lighthouse, after it ran in snapshot mode, showing that 9 of 10 tests have passed. Below it reports that the image is missing an alt attribute" height="484" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/lighthouse-snapshot/7b7161c2c0-1698950571/lh-snapshot6.webp" width="751">
</a>
<p>This is a really useful addition to Lighthouse I always wanted to have. It’s not just useful for accessibility, but for all categories. Thanks a lot to <a href="https://csswizardry.com">Harry</a> for drawing my attention to this feature in his course <a href="https://csswizardry.gumroad.com/l/perfect-devtools">“Setting up DevTools for Performance Testing”</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CAnalyzing+pages+in+a+particular+state+with+Lighthouse%E2%80%9D">blog@matuzo.at</a>.</p> outline is your friend2022-08-17T00:00:00+00:00https://www.matuzo.at/blog/2022/focus-outline
<p>If you open a plain HTML document with no CSS and you focus an interactive element like a button, link, or textarea, you’ll see that by default browsers use the <code>outline</code> property to highlight these elements.</p><figure>
<img alt="A simple demo of a link a button and a textarea" loading="auto" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/fea59d7b8a-1698950581/focus_default.webp">
<figcaption>A blue outline around a focused button in Firefox</figcaption>
</figure>
<h2>outline is great</h2>
<p>The <code>outline</code> property is the perfect candidate for this job for several reasons.</p>
<ul>
<li>
<p><strong>outline doesn’t break layout.</strong></p>
<p>Unlike the <code>border</code> property (if <code>box-sizing</code> is not set to <code>border-box</code>), <code>outline</code> doesn’t add to the width and height of an element. It just creates an outline without taking up any space.</p>
</li>
<li>
<strong>You can add spacing between the content and the outline using <code>outline-offset</code>.</strong>
<figure>
<img alt="A focused link in a paragraph" height="155" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/cf63eeac4f-1698950581/focus_se.webp" width="710">
<figcaption>Outlines on Stephanie Eckles website have some extra spacing.</figcaption>
</figure>
</li>
<li>
<p><strong>You can customize the width, style, and color.</strong></p>
<figure>
<img alt="A random teaser of a blog post." height="293" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/383d4a8806-1698950582/focus_sm.webp" width="751">
<figcaption>Big, red, dotted outline around a link in a teaser on smashingmagazine.com</figcaption>
</figure>
<pre><code class="language-css">:focus {
outline-width: 3px;
outline-style: dotted;
outline-color: #d33a2c;
outline-offset: 2px;
}</code></pre>
</li>
<li>
<p><strong><code>outline</code> works great in <a href="https://www.smashingmagazine.com/2022/06/guide-windows-high-contrast-mode/">forced color modes</a> like <a href="https://blogs.windows.com/msedgedev/2020/09/17/styling-for-windows-high-contrast-with-new-standards-for-forced-colors">Windows High Contrast Mode (WHCM)</a></strong></p>
<figure>
<img alt="Dark background color, white text and blueish highlights instead of white background, black text color and red highlights." height="403" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/ae1e406d92-1698950582/focus_sm2.webp" width="751">
<figcaption>The same outline with different colors in a dark Windows theme.</figcaption>
</figure>
</li>
</ul>
<h2>outline sucks</h2>
<p>There are also disadvantages to using <code>outline</code>.</p>
<ul>
<li>
<p><strong>Outlines don’t look nice.</strong></p>
<p>Yes, outlines don’t necessarily look great, but 1. Oh my god, who cares? It doesn’t have to look nice, it just has to work well for your users and 2. you can tweak them to a certain degree and make them work with your design (see <a href="https://smashingmagazine.com">smashingmagazine.com</a>).</p>
</li>
<li>
<p><strong>The default outline might not be visible enough.</strong></p>
<p>I advice against leaving the default focus styles untouched because they might not work with every design. Overwrite them with something that works better with your theme.</p>
<pre><code class="language-css"> /* Removes the default outline only in browsers that support :focus-visible */
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 2px dashed #282828;
outline-offset: 2px;
}</code></pre>
<img alt="A clearly visible, 2px dashed outline around a button." height="280" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/37c65f465d-1698950582/focus_custom.webp" width="710">
</li>
</ul>
<h2>What about other properties?</h2>
<p>If you remember, one of the advantages of <code>outline</code> is that it works great in forced color mode. If this property works well, it means that there must be properties that don’t work well. </p>
<p>Here’s an example. Instead of using <code>outline</code>, I change the <code>background</code> and add a <code>box-shadow</code> on focus-visible.</p>
<pre><code class="language-css">:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: none;
background: #000;
color: #fff;
box-shadow: 0 2px 0 0 #fff, 0 5px 0 0 #000;
}</code></pre>
<p>This is how focus styles on the button look like with regular contrast settings.</p>
<img alt="Dark background color on the button and a thick dark line below" height="280" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/582c2d7a7c-1698950582/focus_background.webp" width="710">
<p>And here’s the same focused button in <abbr title="Windows High Contrast Mode">WHCM</abbr>. We don’t see anything because these properties are reverted in forced color modes.</p>
<img alt="The styling of the button didn’t change at all" height="280" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/7139edf4a4-1698950581/focus_background_forced.webp" width="710">
<h2>Transparent outlines</h2>
<p>Now you might be thinking “But…but <a href="http://a11yproject.com">a11yproject.com</a> and <a href="http://gov.uk">gov.uk</a> are using background colors for some of their focus styles”. Yes, they are using <code>background-color</code>, but they’re doing it in combination with <code>outline</code>. This is a nice trick to avoid visible outlines in normal color mode and assure that they’re displayed in forced color mode.</p>
<pre><code class="language-css">.c-homepage-card__cta:focus {
background-color: #fb37ff;
color: #000;
outline: 3px solid #fb37ff;
}</code></pre>
<figure>
<img alt="pink background on focused links." width="710" height="267" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/a0970c5bbb-1698950581/focus_a11yproject.webp">
<figcaption>
<p><a href="http://a11yproject.com">a11yproject.com</a> uses the same color for the outline as for the background.</p>
</figcaption>
</figure>
<figure>
<img alt="just an outline on focused links." width="710" height="261" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/02b9e2dcc6-1698950582/focus_a11yproject_forced.webp">
<figcaption>
No background color in WHCM.
</figcaption>
</figure>
<pre><code class="language-css">.govuk-link:focus {
outline: 3px solid transparent;
color: #0b0c0c;
background-color: #fd0;
box-shadow: 0 -2px #fd0,0 4px #0b0c0c;
text-decoration: none;
}</code></pre>
<figure>
<img alt="yellow background an think black underline on focused links" width="710" height="228" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/67c72eb996-1698950582/focus_govuk.webp">
<figcaption>
<p><a href="http://gov.uk">gov.uk</a> uses a background color and box-shadow in combination with a transparent outline</p>
</figcaption>
</figure>
<figure>
<img alt="just an outline on focused links." width="710" height="221" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/focus-outline/152688be4a-1698950581/focus_govuk_forced.webp">
<figcaption>
<p>No background color and box shadow in WHCM.</p>
</figcaption>
</figure>
<h2>Conclusion</h2>
<p>Be careful when customizing focus styles, don’t forget to test in forced color modes, and always remember that <code>outline</code> is your friend.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9Coutline+is+your+friend%E2%80%9D">blog@matuzo.at</a>.</p> Parents counting children in CSS2022-08-29T00:00:00+00:00https://www.matuzo.at/blog/2022/counting-children
<p>The other day I was driving home when suddenly it hit me: We can use <code>:has()</code> to determine how many children a parent element has.</p><p>You might be thinking that Heydon Pickering already solved that 7 years ago in <a href="https://alistapart.com/article/quantity-queries-for-css/">Quantity Queries for CSS</a>, but that's not what I'm talking about.</p>
<pre><code class="language-css">/* Quantity Queries for CSS by Heydon Pickering */
/* Three or more items */
li:nth-last-child(n+3),
li:nth-last-child(n+3) ~ li {
background: red;
}</code></pre>
<p>What I mean is that now we can style the parent element and other children differently depending on the number of items present anywhere in the parent element.</p>
<div style="border: 10px dotted hotpink; padding: 1rem;">
<p>
<strong>Note</strong>: <code>:has()</code> is only available in Safari 15.4+, Chrome 105+ or behind a flag in Firefox.
</p>
</div>
<p>The following code checks if there are at least 3 list items in the list and adds a border to the parent if that's the case.</p>
<pre><code class="language-css">ul:has(li:nth-child(3)) {
border: 1px solid red;
}</code></pre>
<style>
.demo:has(li:nth-child(3)) {
border: 1px solid red;
}
</style>
<ul class="demo">
<li>A</li>
<li>B</li>
</ul>
<p>(<em>↑ no border</em>)</p>
<ul class="demo">
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
<p>(<em>↑ red border</em>)</p>
<ul class="demo">
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
</ul>
<p>(<em>↑ red border</em>)</p>
<p>We can adapt the selector a bit and only apply the styling if there are exactly three list items. </p>
<pre><code class="language-css">ul:has(li:nth-child(3):last-child) {
border: 1px solid blue;
}</code></pre>
<style>
.demo2:has(li:nth-child(3):last-child) {
border: 1px solid blue;
}
</style>
<ul class="demo2">
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
<p>(<em>↑ blue border</em>)</p>
<ul class="demo2">
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
</ul>
<p>(<em>↑ no border</em>)</p>
<h2>A demo</h2>
<p>I’ve built a demo to illustrate what can be done. If you click on the element, start typing and press Enter, the parent element tells you how many items you should add.</p>
<style>
.list {
--color: black;
color: var(--color);
}
.list ul {
border: 3px solid var(--color);
padding: 0;
color: #000;
list-style: none;
counter-reset: count;
}
.list li {
counter-increment: count;
margin: 0;
padding: 0.3rem 0.5rem 0.2rem 4.5ch;
height: 2.8rem;
position: relative;
}
.list li::before {
content: counter(count);
display: inline-block;
background: #000;
color: #fff;
padding: 0.5rem;
margin-right: 1rem;
width: 4ch;
position: absolute;
left: 0;
top: 0;
height: 100%;
box-sizing: border-box;
}
.list:focus-within {
--color: rgb(11, 103, 162);
}
.list::before {
content: attr(data-default);
}
.list:focus-within::before {
content: attr(data-empty-focus);
}
.list:has(li:nth-child(2)) {
--color: rebeccapurple;
}
.list:has(li:nth-child(3)) {
--color: rgb(255, 111, 0);
}
.list:has(li:nth-child(4) > :first-child) {
--color: rgb(203, 157, 0);
}
.list:has(li:nth-child(4)) {
--color: rgb(76, 113, 32);
}
.list:has(li:nth-child(5)) {
--color: red;
}
.list:has(li:nth-child(2))::before {
content: attr(data-great-choice)
}
.list:has(li:nth-child(3) > :first-child)::before,
.list:has(li:nth-child(3))::before{
content: attr(data-half);
}
.list:has(li:nth-child(4):last-child)::before {
content: attr(data-done);
}
.list:has(li:nth-child(4) > :first-child)::before {
content: attr(data-one-more);
}
.list:has(li:nth-child(5))::before {
content: attr(data-too-much);
}
.list:has(li:nth-child(5))::before {
content: attr(data-too-much);
}
.list:has(li:nth-child(5) > :first-child)::before {
content: attr(data-almost-too-much);
}
.list.list:has(li:nth-child(4)) button {
opacity: 1;
}
.list:has(li:nth-child(4) > :first-child) button{
opacity: 0;
}
.list.list:has(li:nth-child(5)) button {
opacity: 0;
}
.list:has(li:nth-child(5) > :first-child) button {
opacity: 1;
}
.list button {
opacity: 0;
transition: opacity .3s;
background: #0080dd;
font-family: inherit;
padding-inline: 1.2rem;
border: 0;
color: #fff;
font-weight: bold;
font-size: 1.2rem;
padding-block: 0.8rem 0.5rem;
letter-spacing: 1px;
}
:focus-visible {
outline: 2px solid transparent;
}
</style>
<div class="list" data-default="Please add exactly 4 items!" data-great-choice="Fantastic choice! What's next?" data-empty-focus="Add the first item!" data-half="Great! We're half way there!" data-one-more="Just one more, you can do it!" data-done="Perfect! 🎉 Please submit your selection!" data-almost-too-much="No! 😱 That's too much! Don't even start typing!" data-too-much="I said that's too much! 🤬">
<ul contenteditable>
<li></li>
</ul>
<button type="button">
Submit
</button>
</div>
<p>Pretty awesome, right? <a href="https://codepen.io/matuzo/pen/YzaoRLJ">Try it on CodePen.</a></p>
<h2>Use cases</h2>
<p>I can image that this can be useful in content builders in CMS. You can use CSS to give users visual feedback depending on the number of items they have added to a block or component.</p>
<div style="border: 10px dotted hotpink; padding: 1rem;">
<p style="margin-bottom: 1.5rem">
<strong>Disclaimer:</strong> This is not tested and not production-ready. Only use this technique for progressive enhancement and only with proper testing. You shouldn’t use it to communicate important information, because changes may only be reflected visually and not semantically.
</p>
<p>Safari and Chrome are the only browsers that support <code>:has()</code> at the moment. No, it won't stay like that. Other browsers will follow soon.</p>
<p>This solution might break, it's just a fun experiment. Don't use it in production.</p>
<p>I've extended this disclaimer and I've added a pink, dotted border because some people missed it before.</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CParents+counting+children+in+CSS%E2%80%9D">blog@matuzo.at</a>.</p> Buttons and the Baader–Meinhof phenomenon2022-09-15T00:00:00+00:00https://www.matuzo.at/blog/2022/button-baader
<p>Shortly after we got our new car, a Volkswagen T5, I noticed many people seemed to have the same car. Actually, it was everywhere.</p><p>It felt like everyone had the same car. While sales of camping vans and cars that can be used for camping in fact have increased during the pandemic, there’s another reason I saw so many of them, the <a href="https://en.wikipedia.org/wiki/Frequency_illusion">Frequency illusion or Baader–Meinhof phenomenon</a>.</p>
<div class="quote">
<blockquote>The Frequency illusion, also known as the Baader–Meinhof phenomenon or frequency bias, is a cognitive bias in which, after noticing something for the first time, there is a tendency to notice it more often, leading someone to believe that it has an increased frequency of occurrence.</blockquote></div>
<p>Shortly after I started <a href="https://www.htmhell.dev/">HTMHell</a> a similar thing happened. Every other website seemed to have inaccessible buttons. While this can be explained by the Baader–Meinhof phenomenon, there’s actually data that confirms my feeling. According to the <a href="https://webaim.org/projects/million/#errors">WebAim 1 Million report</a>, 50.1% of 1 million tested websites contained empty links and 27.2% empty buttons. </p>
<p>So, it was <a href="https://www.youtube.com/watch?v=zOILAZHf2pE">more than a feeling</a>. We are terrible at labelling buttons. I understand that this can be confusing, because there are many different ways of getting the job done.<br />
To help with that issue, I've collected different correct and wrong ways of labelling buttons:</p>
<h2>How to label buttons</h2>
<style>
button {
border: 2px solid #153a51;
background-color: #fff;
font-family: inherit;
font-size: 1.3rem;
padding: 0.3em 0.6em;
border-radius: 4px;
font-weight: bold;
display: flex;
gap: 0.5rem;
}
.post button img {
border: none;
}
button:where(:hover, :focus) {
background-color: hsl(186, 56%, 48%);
color: #fff;
}
</style>
<h3>Text only</h3>
<p>The all-time classic “just using visible text” works for everyone.</p>
<p><button type="button">Tweet!</button></p>
<pre><code class="language-html"><button type="button">Tweet!</button></code></pre>
<h3>Text and Icon</h3>
<p>It’s absolutely fine if you want to add an image or icon to the button. Just make sure to hide it from assistive technology using <code>aria-hidden="true"</code>. The text label of the button should be sufficient, we don’t need extra information.</p>
<p>
<button type="button">
<svg aria-hidden="true" width="28" height="28">
<use href="#twitter"></use>
</svg>
Tweet!
</button>
</p>
<pre><code class="language-html"><button type="button">
<svg aria-hidden="true" width="28" height="28">
<use href="#twitter"></use>
</svg>
Tweet!
</button></code></pre>
<p>Sometimes you want to use an icon only. First, you should be really sure that people understand the purpose of the button without text, and second, label it properly. Just because there’s no text visible on screen doesn’t mean that there doesn’t have to be text in the document.<br />
There are different ways of creating icon-only buttons. </p>
<h3>Icon-only using a sr-only class</h3>
<p>You can put text inside the button and hide it visually using a custom class.</p>
<p>
<button type="button">
<svg aria-hidden="true" width="28" height="28">
<use href="#twitter"></use>
</svg>
<span class="u-vh">Tweet!</span>
</button>
</p>
<pre><code class="language-html"><button type="button">
<span class="sr-only">Tweet!</span>
<svg aria-hidden="true" width="28" height="28">
<use href="#twitter"></use>
</svg>
</button></code></pre>
<pre><code class="language-css">.sr-only {
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
left: 0;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
top: 0;
white-space: nowrap;
width: 1px;
}</code></pre>
<h3>Icon-only using aria-label on the button</h3>
<p>You can add a text alternative for the icon using <code>aria-label</code> on the button.</p>
<p>
<button aria-label="Tweet!" type="button">
<svg aria-hidden="true" width="28" height="28">
<use href="#twitter"></use>
</svg>
</button>
</p>
<pre><code class="language-html"><button aria-label="Tweet!" type="button">
<svg aria-hidden="true" width="28" height="28">
<use href="#twitter"></use>
</svg>
</button></code></pre>
<h3>Icon-only using aria-labelledby on the button</h3>
<p>If the label you want to use already exists somewhere on the page, you can reference it using <code>aria-labelledby</code>.</p>
<h2 id="heading" role="presentation" style="font-size: 1.5rem">Tweet!</h2>
<p>
<button aria-labelledby="heading" type="button">
<svg aria-hidden="true" width="28" height="28">
<use href="#twitter"></use>
</svg>
</button>
</p>
<pre><code class="language-html"><h2 id="heading">Tweet!</h2>
<button aria-labelledby="heading" type="button">
<svg aria-hidden="true" width="28" height="28">
<use href="#twitter"></use>
</svg>
</button></code></pre>
<h3>Icon-only using title inside the svg</h3>
<p>The <code><title></code> element inside the <code><svg></code> can serve as the accessible name for the button. Unfortunately, in order for this to work properly with all screen readers and browsers, you have to label the <code><svg></code> using <code>aria-labelledby</code> explicitly.</p>
<p>
<button type="button">
<svg aria-labelledby="title_twitter_h72d" width="28" height="28">
<title id="title_twitter_h72d">Tweet!</title>
<use href="#twitter"></use>
</svg>
</button>
</p>
<pre><code class="language-html"><button type="button">
<svg aria-labelledby="title_twitter_h72d" width="28" height="28">
<title id="title_twitter_h72d">Tweet!</title>
<use href="#twitter"></use>
</svg>
</button></code></pre>
<h3>Icon-only using alt attribute</h3>
<p>If you’re using an <code><img></code> element instead of <code><svg></code>, then the <code>alt</code> attribute serves as the accessible name of the button.</p>
<p>
<button type="button">
<img alt="" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/button-baader/6ed255792d-1698950567/twitter.png" width="28 alt=Tweet!">
</button>
</p>
<pre><code class="language-html"><button type="button">
<img src="/images/twitter.png" alt="Tweet!" width="28">
</button> </code></pre>
<h3>Icon-only using background-image</h3>
<p>If you’re using a background image, the best option is to add visually hidden text to the button. <code>aria-label</code> or <code>aria-labelledby</code> would work, too.</p>
<style>
.bgimg {
background: url(/blog/2022/button-baader/twitter.png);
width: 3.57rem;
height: 2.8rem;
background-size: 56%;
background-repeat: no-repeat;
background-position: center;
}
</style>
<p>
<button type="button" class="bgimg">
<span class="u-vh">Tweet!</span>
</button>
</p>
<pre><code class="language-html"><button type="button">
<span class="sr-only">Tweet!</span>
</button> </code></pre>
<pre><code class="language-css">button {
background: url(/images/twitter.png) no-repeat center;
}</code></pre>
<h2>How not to label buttons</h2>
<p>Here are some of the wrong solutions I see often. Don’t do this!</p>
<h3>No Text</h3>
<p>Don’t do this!</p>
<p><button type="button" class="bgimg"></button></p>
<pre><code class="language-html"><button type="button"></button></code></pre>
<pre><code class="language-css">button {
background: url(/images/twitter.png) no-repeat center;
}</code></pre>
<h3>No text alternative for svg</h3>
<p>Don’t do this!</p>
<p><button type="button">
<svg width="28" height="28">
<use href="#twitter"></use>
</svg>
</button>
</p>
<pre><code class="language-html"><button type="button">
<svg width="28" height="28">
<use href="#twitter"></use>
</svg>
</button></code></pre>
<h3>No text alternative for img</h3>
<p>Don’t do this!</p>
<p>
<button type="button">
<img alt="" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/button-baader/6ed255792d-1698950567/twitter.png" width="28">
</button>
</p>
<pre><code class="language-html"><button type="button">
<img src="/images/twitter.png" alt="" width="28">
</button></code></pre>
<p>or</p>
<p><button type="button">
<img alt="" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/button-baader/6ed255792d-1698950567/twitter.png" width="28">
</button></p>
<pre><code class="language-html"><button type="button">
<img src="/images/twitter.png" width="28">
</button></code></pre>
<div>
<svg>
<symbol id="twitter" fill="currentcolor" viewBox="0 0 512 512">
<path d="M512 97.248c-19.04 8.352-39.328 13.888-60.48 16.576 21.76-12.992 38.368-33.408 46.176-58.016-20.288 12.096-42.688 20.64-66.56 25.408C411.872 60.704 384.416 48 354.464 48c-58.112 0-104.896 47.168-104.896 104.992 0 8.32.704 16.32 2.432 23.936-87.264-4.256-164.48-46.08-216.352-109.792-9.056 15.712-14.368 33.696-14.368 53.056 0 36.352 18.72 68.576 46.624 87.232-16.864-.32-33.408-5.216-47.424-12.928v1.152c0 51.008 36.384 93.376 84.096 103.136-8.544 2.336-17.856 3.456-27.52 3.456-6.72 0-13.504-.384-19.872-1.792 13.6 41.568 52.192 72.128 98.08 73.12-35.712 27.936-81.056 44.768-130.144 44.768-8.608 0-16.864-.384-25.12-1.44C46.496 446.88 101.6 464 161.024 464c193.152 0 298.752-160 298.752-298.688 0-4.64-.16-9.12-.384-13.568 20.832-14.784 38.336-33.248 52.608-54.496z"></path>
</symbol>
</svg>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CButtons+and+the+Baader%E2%80%93Meinhof+phenomenon%E2%80%9D">blog@matuzo.at</a>.</p> Day 1: custom properties and fallbacks2022-09-26T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day1
<p>You can pass a second value to the <code>var()</code> CSS function which acts as a fallback for when the property has not been set.</p><h2>Fallbacks</h2>
<style>
.demo {
width: 5rem;
height: 5rem;
border: 5px solid;
}
.one {
background-color: var(--not-set, #000);
}
.two {
background-color: var(--not-set, var(--also-not-set, #00F));
}
.three {
background-color: #F00;
background-color: blahaha;
}
.four {
background-color: #F00;
background-color: var(--not-set);
}
</style>
<pre><code class="language-css">div {
background-color: var(--not-set, #000);
}
/* Result: #000 background */</code></pre>
<div data-sample="demo">
<div class="demo one"></div>
</div>
<p>The fallback can also be a custom property (with its own fallback).</p>
<pre><code class="language-css">div {
background-color: var(--not-set, var(--also-not-set, #00F));
}
/* Result: #00F background */</code></pre>
<div data-sample="demo">
<div class="demo two"></div>
</div>
<h2>When Fallbacks fail</h2>
<p>If you're not working with custom properties and you set a valid value for a property followed by another declaration with an invalid value, the second declaration will be ignored.</p>
<pre><code class="language-css">div {
background-color: #F00;
background-color: blahaha;
}
/* Result: #F00 background */</code></pre>
<div data-sample="demo">
<div class="demo three"></div>
</div>
<p>When the value in the second declaration is a custom property that doesn't exist, the declaration is not ignored. Either the property’s inherited value or its initial value, depending on whether the property is inherited or not, will be used instead.</p>
<pre><code class="language-css">div {
background-color: #F00;
background-color: var(--not-set);
}
/* Result: transparent background */</code></pre>
<div data-sample="demo">
<div class="demo four"></div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+1%3A+custom+properties+and+fallbacks%E2%80%9D">blog@matuzo.at</a>.</p> 100 Days Of More Or Less Modern CSS2022-09-26T00:00:00+00:00https://www.matuzo.at/blog/2022/100-days-of-more-or-less-modern-css
<p>It’s time to get me up to speed with modern CSS. There’s so much new in CSS that I know too little about. To change that I’ve started #100DaysOfMoreOrLessModernCSS. Why more or less modern CSS? Because some topics will be about cutting-edge features, while other stuff has been around for quite a while already, but I just have little to no experience with it.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9C100+Days+Of+More+Or+Less+Modern+CSS%E2%80%9D">blog@matuzo.at</a>.</p> Day 2: logical properties2022-09-27T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day2
<p>Logical properties are a new way of working with directions and dimensions, one that allows you to control layout through logical, rather than physical mappings. This is especially useful, if you’re dealing with websites that are presented in different languages and writing modes, like right-to-left.</p><h2><strong>Physical properties</strong></h2>
<p>We're used to working with physical properties like <code>margin-right</code>, <code>top</code>, or <code>border-left</code>. In the following example, list items are positioned horizontally, and each item has a right margin.</p>
<p class="code-label"><strong>HTML:</strong></p>
<pre><code class="language-html"><ul>
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul></code></pre>
<p class="code-label"><strong>CSS:</strong></p>
<pre><code class="language-css">li {
background-color: #6befef;
margin-right: 2rem;
}</code></pre>
<style>
.physical,
.logical {
display: flex;
list-style: none;
padding: 0.5rem 0;
margin: 0 0 2rem 0;
max-width: 30rem;
border: 2px solid;
}
.physical li {
margin: 0 2rem 0 0;
background-color: #6befef;
}
[data-sample].margin li {
box-shadow: 2rem 0 0 rgb(255 165 0 / 0.5);
}
[data-sample].marginlogical li {
box-shadow: -2rem 0 0 rgb(255 165 0 / 0.5);
}
</style>
<figure>
<div data-sample="ltr demo">
<ul class="physical">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
<button aria-pressed="false" class="show-margin">Visualise margin</button>
</div>
<figcaption>The first item “One” is at the left edge of its parent element.</figcaption>
</figure>
<p>When you change the reading direction from right to left, you'd expect the first item to be positioned at the right edge of its parent, but it's not because physical properties don't change with the reading direction.</p>
<p class="code-label"><strong>HTML:</strong></p>
<pre><code class="language-html"><ul dir="rtl">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul></code></pre>
<div data-sample="rtl demo" class="margin">
<ul class="physical" dir="rtl">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
<button aria-pressed="false" class="show-margin">Visualise margin</button>
</div>
<h2><strong>Logical properties</strong></h2>
<p>Using logical properties, “One” is at the very right in right-to-left languages because logical properties don't work with the concept of top and bottom or left and right, but start and end, which switches depending on the writing mode.</p>
<style>
.logical li {
margin: 0;
background-color: #6befef;
margin-inline-end: 2rem;
}
</style>
<p class="code-label"><strong>HTML:</strong></p>
<pre><code class="language-css">li {
margin-inline-end: 2rem;
}</code></pre>
<div data-sample="rtl demo" class="margin marginlogical">
<ul class="logical" dir="rtl">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
</div>
<script>
document.querySelector('.post').addEventListener('click', e => {
if (e.target.closest('.show-margin')) {
const btn = e.target
btn.setAttribute('aria-pressed', btn.getAttribute('aria-pressed') === 'false')
console.log(btn.getAttribute('aria-pressed'))
btn.parentNode.classList.toggle('margin')
}
})
</script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+2%3A+logical+properties%E2%80%9D">blog@matuzo.at</a>.</p> Day 3: logical property shorthands2022-09-28T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day3
<p>If you use a shorthand property like <code>margin</code> with all 4 values, the properties will always be applied in the direction <em>top</em> - <em>right</em> - <em>bottom</em> - <em>left</em>, no matter the reading direction.</p><pre><code class="language-html"><button>Physical margin</button>
<button dir="rtl">Physical margin rtl</button></code></pre>
<pre><code class="language-css">button {
margin: 20px 40px 10px 100px;
}
/*
LTR: 20px 40px 10px 100px
RTL: 20px 40px 10px 100px
*/</code></pre>
<style>
.physical {
margin: 20px 40px 10px 100px;
}
button {
width: 100px;
height: 100px;
border: 4px solid #000;
display: block;
background: none;
}
.logical {
margin-inline: 100px 40px;
margin-block: 20px 10px;
}
</style>
<button type="button" class="physical">Physical margin</button>
<button type="button" dir="rtl" class="physical">Physical margin rtl</button>
<p>This might be desired, but it could also happen that you want <code>margin</code> to respect the reading direction. <a href="/blog/2022/100daysof-day2/">Logical Properties</a> introduce 2 new shorthand properties, <code>margin-inline</code> and <code>margin-block</code>. These properties take 1 or 2 values.</p>
<pre><code class="language-css">.logical {
margin-inline: 5rem; /* start and end value (= left and right) */
margin-block: 5rem; /* start and end value (= top and bottom) */
margin-inline: 1rem 2rem; /* start / end value (= left / right in ltr) */
margin-block: 3rem 4rem; /* start / end value (= top / bottom in ltr) */
}</code></pre>
<p>Unlike <code>margin</code>, <code>margin-inline</code> and <code>margin-block</code> respect the reading direction.</p>
<pre><code class="language-html"><button>Logical margin</button>
<button dir="rtl">Logical margin rtl</button></code></pre>
<pre><code class="language-css">button {
margin-inline: 100px 40px; /* 100px = start/left, 40px = end/right */
margin-block: 20px 10px; /* 20px = start/top, 10px = end/bottom */
}
/*
LTR: 20px 40px 10px 100px
RTL: 20px 100px 10px 40px
*/</code></pre>
<button type="button" class="logical">Logical margin</button>
<button type="button" dir="rtl" class="logical">Logical margin rtl</button><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+3%3A+logical+property+shorthands%E2%80%9D">blog@matuzo.at</a>.</p> Day 4: the min() function2022-09-29T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day4
<p>The <code>min()</code> function takes a comma separated list of expressions. The smallest value in the list will be selected.</p><pre><code class="language-css">div {
width: min(400px, 200px, 300px);
/* width = 200px */
}</code></pre>
<p>This example doesn’t make much sense because the value will always be <code>200px</code>.<br />
<code>min()</code> shows its true power when you use relative units.</p>
<pre><code class="language-css">div {
width: min(100%, 800px);
}</code></pre>
<p>If the available space is below 800px, it matches <code>100%.</code> If it's more than 800px, it matches <code>800px</code>. This is basically a shorter version of this.</p>
<pre><code class="language-css">div {
width: 100%;
max-width: 800px;
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+4%3A+the+min%28%29+function%E2%80%9D">blog@matuzo.at</a>.</p> Day 5: the max() function2022-09-30T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day5
<p>The <code>max()</code> function takes a comma separated list of expressions. The largest value in the list will be selected.</p><pre><code class="language-css">div {
width: max(400px, 200px, 300px);
/* width = 400px */
}</code></pre>
<p>This example doesn’t make much sense because the value will always be <code>400px</code>.<br />
<code>max()</code> shows its true power when you use relative units.</p>
<pre><code class="language-css">div {
width: max(300px, 50vw);
}</code></pre>
<p>If <code>50vw</code> is lower than 300px, <code>width</code> matches <code>300px</code>. If <code>50vw</code> is larger than 300px, it matches <code>50vw</code>. This is basically a shorter version of this.</p>
<pre><code class="language-css">div {
min-width: 300px;
width: 50vw;
}</code></pre>
<p>PS: The next post is coming on Monday because weekends are for family and friends. ❤️</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+5%3A+the+max%28%29+function%E2%80%9D">blog@matuzo.at</a>.</p> Day 6: the :has() pseudo-class2022-10-03T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day6
<p><code>:has()</code> allows you to check whether a parent element contains specific children.</p><p>In the following example, each <code>.form-item</code> that contains/has a child with the <code>aria-invalid</code> attribute set to “true” displays text in red color. (currently only in <a href="https://caniuse.com/css-has">Chrome/Edge 105+ and Safari 15.4+</a>)</p>
<style>
.form-item {
--color: #000;
color: var(--color);
}
input {
border: 1px solid var(--color);
}
.form-item:has([aria-invalid="true"]) {
--color: #F00;
}
</style>
<form>
<div class="form-item">
<label for="name">Name</label><br>
<input type="text" id="name" required aria-invalid="true">
</div>
<div class="form-item">
<label for="email">E-Mail</label><br>
<input type="text" id="email">
</div>
</form>
<pre><code class="language-html"><form>
<div class="form-item">
<label for="name">Name</label><br>
<input type="text" id="name" required aria-invalid="true">
</div>
<div class="form-item">
<label for="email">E-Mail</label><br>
<input type="text" id="email">
</div>
</form></code></pre>
<pre><code class="language-css">
.form-item {
--color: #000;
/* The default color is #000 */
color: var(--color);
}
input {
/* The default border-color is #000 */
border: 1px solid var(--color);
}
/* If the .form-item contains an element with [aria-invalid="true"],
the text and border color changes to #F00 */
.form-item:has([aria-invalid="true"]) {
--color: #F00;
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+6%3A+the+%3Ahas%28%29+pseudo-class%E2%80%9D">blog@matuzo.at</a>.</p> Day 7: subgrids2022-10-04T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day7
<p>Subgrid allows a grid-item with its own grid to align with its parent grid (currently only in <a href="https://caniuse.com/css-subgrid">Firefox 71+ and Safari 16+</a>).</p><p>In the following example, the <code>div</code> elements use the grid defined in the <code>dl</code> element. The result is that all the <code>dt</code> and <code>dd</code> elements are aligned in the same “columns” respectively, even though they’re not on the same level in the DOM.</p>
<pre><code class="language-html"><dl>
<div>
<dt>HTML</dt>
<dd>The HyperText Markup Language or HTML is the standard markup language for documents designed to be displayed in a web browser.</dd>
</div>
<div>
<dt>JavaScript</dt>
<dd>JavaScript often abbreviated JS, is a programming language that is one of the core technologies of the World Wide Web, alongside HTML and CSS</dd>
</div>
</dl></code></pre>
<pre><code class="language-css">dl {
display: grid;
gap: 0.5rem 2rem;
grid-template-columns: auto 1fr;
}
div {
display: inherit;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}</code></pre>
<style>
.subgrid {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem 2rem;
}
.subgrid div {
grid-column: 1 / -1;
display: inherit;
grid-template-columns: subgrid;
}
.no-subgrid {
display: grid;
gap: 0.5rem 2rem;
grid-template-columns: auto 1fr;
}
.no-subgrid div {
display: inherit;
gap: inherit;
grid-column: 1 / -1;
grid-template-columns: auto 1fr;
}
</style>
<figure>
<dl class="subgrid">
<div>
<dt>HTML</dt>
<dd>The HyperText Markup Language or HTML is the standard markup language for documents designed to be displayed in a web browser.</dd>
</div>
<div>
<dt>JavaScript</dt>
<dd>JavaScript often abbreviated JS, is a programming language that is one of the core technologies of the World Wide Web, alongside HTML and CSS</dd>
</div>
</dl>
<figcaption>Both columns are neatly aligned in the same grid.</figcaption>
</figure>
<p>To make it a bit more obvious, here’s how the same grid looks like if you don’t use <code>subgrid</code> but copy the <code>grid-template-columns</code> declaration from the <code>dl</code> and use it on the <code>div</code>.</p>
<pre><code class="language-css">dl {
display: grid;
gap: 0.5rem 2rem;
grid-template-columns: auto 1fr;
}
div {
display: inherit;
gap: inherit;
grid-column: 1 / -1;
grid-template-columns: auto 1fr;
}</code></pre>
<figure>
<dl class="no-subgrid">
<div>
<dt>HTML</dt>
<dd>The HyperText Markup Language or HTML is the standard markup language for documents designed to be displayed in a web browser.</dd>
</div>
<div>
<dt>JavaScript</dt>
<dd>JavaScript often abbreviated JS, is a programming language that is one of the core technologies of the World Wide Web, alongside HTML and CSS</dd>
</div>
</dl>
<figcaption>The width of each element differs because each div forms its own grid.</figcaption>
</figure><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+7%3A+subgrids%E2%80%9D">blog@matuzo.at</a>.</p> Day 8: nesting :has()2022-10-05T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day8
<p>The <code>:has()</code> pseudo-class cannot be nested; <code>:has()</code> is not valid within <code>:has()</code>.</p><pre><code class="language-html"><div>
<p>
<strong>I have a red and blue border in supporting browsers.</strong>
</p>
</div></code></pre>
<pre><code class="language-css">/* valid */
div:has(p) {
border: 4px solid red;
}
/* valid */
p:has(strong) {
border: 4px solid blue;
}
/* invalid */
div:has(p:has(strong)) {
border: 4px solid green;
}</code></pre>
<style>
.div:has(p) {
border: 4px solid red;
}
/* valid */
.div p:has(strong) {
border: 4px solid blue;
}
/* invalid */
.div:has(p:has(strong)) {
border: 4px solid green;
}
.div2:has(p strong) {
border: 4px solid green;
}
</style>
<div class="div">
<p>
<strong>I have a red and blue border in <a href="https://caniuse.com/css-has">supporting browsers</a>.</strong>
</p>
</div>
<p>Using a combined selector instead of nesting <code>:has()</code> is valid.</p>
<pre><code class="language-css">/* valid */
div:has(p strong) {
border: 4px solid green;
}</code></pre>
<div class="div2">
<p>
<strong>I have a green border in <a href="https://caniuse.com/css-has">supporting browsers</a>.</strong>
</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+8%3A+nesting+%3Ahas%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 9: the inset shorthand property2022-10-06T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day9
<p>The <code>inset</code> property is a shorthand for the <code>top</code>, <code>right</code>, <code>bottom</code>, and/or <code>left</code> properties. It implements the same multi-value syntax like <code>margin</code>.</p><pre><code class="language-html"><div class="parent">
<div class="child">
At bottom right with 20px offset
</div>
</div></code></pre>
<pre><code class="language-css">.parent {
width: 12rem;
height: 12rem;
position: relative;
}
.child {
position: absolute;
/* top: auto; right: 20px; bottom: 20px; left: auto */
inset: auto 20px 20px auto;
width: 50%;
height: 50%;
}</code></pre>
<style>
.parent {
width: 20rem;
height: 20rem;
position: relative;
border: 10px solid;
}
.child {
background: red;
position: absolute;
inset: auto 20px 20px auto;
width: 50%;
height: 50%;
}
.child2 {
background: red;
position: absolute;
inset-block: auto 20px;
inset-inline: auto 20px;
width: 50%;
height: 50%;
}
</style>
<div class="parent">
<div class="child">
At bottom right with 20px offset
</div>
</div>
<p>Just like <a href="/blog/2022/100daysof-day3/">margin</a>, <code>inset</code> does not respect the reading direction. To work around that, use the <code>inset-inline</code> and <code>inset-block</code> shorthands.</p>
<pre><code class="language-css">.child {
position: absolute;
/* top: auto; bottom: 20px; in ltr */
inset-block: auto 20px;
/* left: auto; right: 20px; in ltr */
inset-inline: auto 20px;
}</code></pre>
<pre><code class="language-html"><div class="parent" dir="rtl">
<div class="child">
At bottom right with 20px offset
</div>
</div></code></pre>
<div class="parent" dir="rtl">
<div class="child2">
At bottom right with 20px offset
</div>
</div>
<p>Are these shorthands useful? I don't know. I'll cover some use cases later in the series, but I wasn't able to come up with many scenarios where <code>inset</code> makes things much easier.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+9%3A+the+inset+shorthand+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 10: global styles and web components2022-10-07T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day10
<p>I was wondering what happens with HTML elements in web components when I add styles to the document. Under which circumstances do global styles defined in a style element or external stylesheet apply to these elements?</p><p>As it turns out, it depends on how you create and use the components. In my test setup I have an HTML document, a stylesheet and three different components.</p>
<style>
.div {
border: 2px solid red;
}
slot-component,
basic-component,
shadow-component {
display: block;
margin-top: 0 !important;
}
</style>
<p class="code-label"><strong>styles.css</strong></p>
<pre><code class="language-css">div {
border: 2px solid red;
}</code></pre>
<p class="code-label"><strong>index.html</strong></p>
<pre><code class="language-html"><head>
…
<link rel="stylesheet" href="styles.css">
</head>
<body>
<basic-component></basic-component>
<shadow-component></shadow-component>
<slot-component>
<div>Bye World!</div>
</slot-component>
<script src="basic-component.js" type="module"></script>
<script src="shadow-component.js" type="module"></script>
<script src="slot-component.js" type="module"></script>
</body></code></pre>
<h3>Web component without shadow DOM</h3>
<p>If you add an element to a web component using JavaScript and you don’t attach it to the shadow DOM, styles defined outside the web component apply.</p>
<p class="code-label"><strong>basic-component.js</strong></p>
<pre><code class="language-js">class BasicComponent extends HTMLElement {
constructor() {
super();
this.innerHTML = `<div>Hello World!</div>`
}
}
customElements.define('basic-component', BasicComponent);</code></pre>
<div class="sample">
<basic-component></basic-component>
</div>
<h3>Web component with shadow DOM</h3>
<p>If you attach an element to the shadow DOM inside the web component, style declarations from the outside don’t apply.</p>
<p class="code-label"><strong>shadow-component.js</strong></p>
<pre><code class="language-js">class ShadowComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div>Hello World!</div>`
}
}
customElements.define('shadow-component', ShadowComponent);</code></pre>
<div class="sample">
<shadow-component></shadow-component>
</div>
<h3>Web Component with slotted content</h3>
<p>If you attach an element to the shadow DOM inside the web component and you pass slotted content, style declarations from the outside only apply to the slotted content.</p>
<pre><code class="language-js">class SlotComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Hello World!</div>
<slot></slot>
`
}
}
customElements.define('slot-component', SlotComponent);</code></pre>
<div class="sample">
<slot-component>
<div class="div">Bye World!</div>
</slot-component>
</div>
<script>
class BasicComponent extends HTMLElement {
constructor() {
super();
this.innerHTML = `<div class="div">Hello World!</div>`
}
}
customElements.define('basic-component', BasicComponent);
class ShadowComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div class="div">Hello World!</div>`
}
}
customElements.define('shadow-component', ShadowComponent);
class SlotComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div class="div">Hello World!</div>
<slot></slot>
`
}
}
customElements.define('slot-component', SlotComponent);
</script>
<p>PS: The next post is coming on Monday because weekends are for family and friends. ❤️</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+10%3A+global+styles+and+web+components%E2%80%9D">blog@matuzo.at</a>.</p> Day 11: space-separated functional color notations2022-10-10T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day11
<p>Functional color notations that existed before CSS Color Module Level 4 (<code>rgb()</code>, <code>rgba()</code>, <code>hsl()</code>, <code>hsla()</code>) used to only except comma-separated lists of arguments. That changes with Module Level 4, now you can also provide space-separated arguments.</p><style>
.div {
width: 100px;
height: 100px;
}
.div1 { background-color: rgb(255 0 0);}
.div2 { background-color: rgb(0% 100% 0%);}
.div3 { background-color: rgb(0 0 255 / 0.5);}
.div4 { background-color: rgb(255 0 0 / 50%);}
</style>
<h2>rgb values</h2>
<pre><code class="language-css">div {
background-color: rgb(255 0 0);
}</code></pre>
<div class="div div1"></div>
<h2>Percentages</h2>
<pre><code class="language-css">div {
background-color: rgb(0% 100% 0%);
}</code></pre>
<div class="div div2"></div>
<h2>rgb values + alpha</h2>
<pre><code class="language-css">div {
background-color: rgb(0 0 255 / 0.5);
}</code></pre>
<div class="div div3"></div>
<h2>rgb values + percentage alpha</h2>
<pre><code class="language-css">div {
background-color: rgb(255 0 0 / 50%);
}</code></pre>
<div class="div div4"></div>
<h2>Summary</h2>
<pre><code class="language-css">body {
/* Comma-separated arguments */
background-color: rgb(255, 0, 0);
background-color: rgb(0%, 100%, 0%);
background-color: rgba(255, 0, 0, 0.5);
background-color: rgba(0%, 0%, 100%);
/* Space-separated arguments */
background-color: rgb(255 0 0);
background-color: rgb(0% 100% 0%);
background-color: rgb(255 0 0 / 0.5);
background-color: rgba(255 0 0 / 0.5);
background-color: rgb(0% 0% 100%);
background-color: rgb(0% 100% 0% / 0.5);
background-color: rgb(100% 0 0 / 50%);
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+11%3A+space-separated+functional+color+notations%E2%80%9D">blog@matuzo.at</a>.</p> Day 12: max() trickery2022-10-11T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day12
<p>I saw this interesting one-liner in a demo by <a href="https://twitter.com/ChallengesCss">Temani Afif</a>.</p><pre><code class="language-css">.wrapper {
margin-inline: max(0px, ((100% - 64rem) / 2));
}</code></pre>
<p>It’s basically a shorter way of writing this:</p>
<pre><code class="language-css">.wrapper {
max-width: 64rem;
margin: 0 auto;
width: 100%;
}</code></pre>
<p>It’s not up to me to decide whether it’s smart to use this in production or not, but I want to break it down to fully understand what’s going on.</p>
<p>Let’s work our way from the inside out.</p>
<pre><code class="language-css">(100% - 64rem)</code></pre>
<p>This takes the available width and subtracts the maximum width of the wrapper. What’s left is the remaining space.</p>
<pre><code class="language-css">((100% - 64rem) / 2)</code></pre>
<p>We take the remaining space and divide it by 2. We have to divide it because we want to use it for the left and right margin of the wrapper.</p>
<pre><code class="language-css">max(0px, ((100% - 64rem) / 2));</code></pre>
<p>If 100% is less than 64rem, we would get a negative number, which would break the layout. The <a href="/blog/2022/100daysof-day5/">max() function</a> ensures that the <code>margin</code> is always at least zero. It takes a comma separated list of expressions. The largest value in the list will be selected. If the value of our calculation is less than 0, <code>max()</code> takes 0 instead because it’s larger than the negative number.</p>
<pre><code class="language-css">margin-inline: max(0px, ((100% - 64rem) / 2));</code></pre>
<p><a href="/blog/2022/100daysof-day3/">margin-inline</a> sets both the left and the right margin to either 0 or our calculated value, which centers the element.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+12%3A+max%28%29+trickery%E2%80%9D">blog@matuzo.at</a>.</p> Day 13: the :where() and :is() pseudo-classes2022-10-12T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day13
<p>The <code>:where()</code> and <code>:is()</code> pseudo-classes allow you to write large lists of selectors in a more compact form. You can combine selectors instead of writing repetitive lists.</p><p class="code-label"><strong>Combining input types</strong></p>
<pre><code class="language-css">/* Before */
input[type="text"],
input[type="email"],
input[type="url"],
input[type="tel"],
input[type="password"],
input[type="search"] {
border: 2px solid;
}
/* After */
input:where(
[type="text"],
[type="email"],
[type="url"],
[type="tel"],
[type="password"],
[type="search"]
) {
border: 2px solid;
}</code></pre>
<p class="code-label"><strong>Combining pseudo-classes</strong></p>
<pre><code class="language-css">a:is(:link, :visited) {
color: green;
}
a:is(:hover, :focus) {
text-decoration: none;
}</code></pre>
<p>Now you might be thinking, <em>“Uhm,…can’t I just use :where() instead of :is() and vice versa? What’s the difference?”</em>.</p>
<p>A fantastic question that I’ll answer in tomorrows post! :)</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+13%3A+the+%3Awhere%28%29+and+%3Ais%28%29+pseudo-classes%E2%80%9D">blog@matuzo.at</a>.</p> Day 14: the difference between :is() and :where()2022-10-13T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day14
<p>There's an important difference between <code>:is()</code> and <code>:where()</code>.</p><p>Let's take the following example. We have two buttons and we use <code>:where()</code> on the first button to apply a background color and <code>:is()</code> on the second button.</p>
<style>
button {
border: none;
color: #fff;
font: inherit;
font-size: 1.2rem;
padding: 0.5rem 1rem;
}
button:where(.button1) {
background-color: rebeccapurple;
}
button:is(.button2) {
background-color: rebeccapurple;
}
.specificty button:where(.button1) {
background-color: rebeccapurple;
}
.specificty button:is(.button2) {
background-color: rebeccapurple;
}
.specificty button {
background-color: salmon;
}
.specificty .button2 {
background-color: green;
}
</style>
<pre><code class="language-html"><button class="button1">where</button>
<button class="button2">is</button></code></pre>
<pre><code class="language-css">button:where(.button1) {
background-color: rebeccapurple;
}
button:is(.button2) {
background-color: rebeccapurple;
}</code></pre>
<p><button class="button1">where</button>
<button class="button2">is</button></p>
<p>Visually the buttons are identical, but the difference is the specificity of the selector. </p>
<p>The button with the <code>:where()</code> pseudo-class has the same specificity as a simple tag selector (for example <code>button {}</code>) because the specificity of <code>:where()</code> is 0. The arguments in <code>:where()</code> don't add to the specificity of the selector. </p>
<p>The button with the <code>:is()</code> pseudo-class has the same specificty as a combined selector (for example <code>button.button2 {}</code>) because <code>:is()</code> takes on the specificity of the most specific selector in its arguments.</p>
<pre><code class="language-css">
/* Specificity: 0 0 1 (0 ids, 0 classes, 1 tag) */
button:where(.button1) {
background-color: rebeccapurple;
}
/* Specificity: 0 1 1 */
button:is(.button2) {
background-color: rebeccapurple;
}
/* Specificity: 0 0 1
-> Same as the first button. Overwrites rebeccapurple.
-> Lower than the second button. Doesn't overwrite rebeccapurple.
*/
button {
background-color: salmon;
}
/* Specificity: 0 1 0
-> Still lower than the second button. Doesn't overwrite rebeccapurple.
*/
.button2 {
background-color: green;
}</code></pre>
<div class="specificty">
<button class="button1">where</button>
<button class="button2">is</button>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+14%3A+the+difference+between+%3Ais%28%29+and+%3Awhere%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 15: the :modal pseudo-class2022-10-14T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day15
<p>There are two methods you can use to open a <code><dialog></code> element, <code>show()</code> and <code>showModal()</code>. <code>show()</code> opens a dialog on top of the rest of the content, but you can still interact with content beneath. <code>showModal()</code> opens a modal dialog with a backdrop on top of the rest of the content, and you can’t interact with the rest of the page.</p><p>You can use the <code>:modal</code> pseudo-class to style modal dialogs (dialogs opened via <code>showModal()</code>) differently.</p>
<style>
dialog {
border: 10px solid aqua;
}
:modal {
border-style: dotted;
border-color: fuchsia;
padding: 4rem;
}
</style>
<dialog>
yo!
<button class="closeButton">Close</button>
</dialog>
<p><button class="showButton">Show</button></p>
<p><button class="showModalButton">Show modal</button></p>
<pre><code class="language-css">dialog {
border: 10px solid aqua;
}
:modal {
border-style: dotted;
border-color: fuchsia;
padding: 4rem;
}</code></pre>
<pre><code class="language-html"><dialog>
yo!
<button class="closeButton">Close</button>
</dialog>
<button class="showButton">Show</button>
<button class="showModalButton">Show modal</button></code></pre>
<pre><code class="language-js">const showButton = document.querySelector('.showButton')
const showModalButton = document.querySelector('.showModalButton')
const closeButton = document.querySelector('.closeButton')
const dialog = document.querySelector('dialog')
showButton.addEventListener('click', e => {
dialog.show()
})
showModalButton.addEventListener('click', e => {
dialog.showModal()
})
closeButton.addEventListener('click', e => {
dialog.close()
})</code></pre>
<script>
const showButton = document.querySelector('.showButton')
const showModalButton = document.querySelector('.showModalButton')
const closeButton = document.querySelector('.closeButton')
const dialog = document.querySelector('dialog')
showButton.addEventListener('click', e => {
dialog.show()
})
showModalButton.addEventListener('click', e => {
dialog.showModal()
})
closeButton.addEventListener('click', e => {
dialog.close()
})
</script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+15%3A+the+%3Amodal+pseudo-class%E2%80%9D">blog@matuzo.at</a>.</p> Day 16: the specificity of :has()2022-10-17T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day16
<p>Just like with <code>:is()</code> and <code>:not()</code>, the specificity of <code>:has()</code> is replaced by the specificity of the most specific selector in its selector list argument. Unlike <code>:nth-child()</code> or <code>:link</code>, <code>:has()</code> itself doesn't add to the specificity.</p><pre><code class="language-html"><div class="parent">
<p class="child">yo!</p>
</div></code></pre>
<pre><code class="language-css">/* A tag and a class */
div:has(.child) {
background: red;
}
/* A tag: specificty too low */
div {
background: blue;
}
/* A class: specificty too low */
.parent {
background: green;
}
/* A tag and a class: same specificty as div:has(.child) */
div.parent {
background: orange;
}</code></pre>
<style>
.demo div:has(.child) {
background: red;
}
/* Specificty too low */
.demo div {
background: blue;
}
/* Specificty too low */
.demo .parent {
background: green;
}
/* Same specificty as div:has(.child) */
.demo div.parent {
background: orange;
}
</style>
<div class="demo">
<div class="parent">
<p class="child">yo!</p>
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+16%3A+the+specificity+of+%3Ahas%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 17: the :picture-in-picture pseudo-class2022-10-18T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day17
<p>You can use the <code>:picture-in-picture</code> pseudo-class to style an element, usually a <code><video></code>, which is currently in picture-in-picture mode (PIP).</p><p>Clicking the following button puts the video in picture-in-picture mode in supporting browsers (Chrome, Edge, Safari, Opera). Firefox doesn't support the API, but you can right-click the video and select “Watch in Picture-in-Picture“.</p>
<p><button>Toggle PIP</button></p>
<video src="/blog/2022/100daysof-day17/workshop_promo.mp4" controls>
<track default kind="captions" srclang="en" src="/blog/2022/100daysof-day17/workshop_promo.vtt" label="English">
<track default kind="subtitles" srclang="de" src="/blog/2022/100daysof-day17/workshop_promo_de.vtt" label="Deutsch">
Sorry, your browser doesn't support embedded videos.
</video>
<style>
:picture-in-picture {
opacity: 0.3;
filter: blur(5px);
}
</style>
<p>When the video is playing in PIP mode, the placeholder for the video switches to the <code>:picture-in-picture</code> state. Contrary to the information in the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:picture-in-picture#browser_compatibility">support table on MDN</a>, none of the browsers, except for Safari, supports the pseudo-class. At least, according to my tests.</p>
<pre><code class="language-css">:picture-in-picture {
opacity: 0.3;
filter: blur(5px);
}</code></pre>
<script>
const button = document.querySelector('button')
const video = document.querySelector('video')
button.addEventListener('click', () => {
togglePictureInPicture()
})
function togglePictureInPicture() {
if (document.pictureInPictureElement) {
document.exitPictureInPicture();
} else if (document.pictureInPictureEnabled) {
video.requestPictureInPicture();
}
}
</script>
<p>Here's how the video placeholder, which by default is not blurred and has a black background color, looks like in Safari.</p>
<p><a href="/images/100days-17.jpg"></p>
<img alt="Video playing in the bottom right corner. The placeholder for the video has 50% opacity and is a bit blurred" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day17/bd054f5343-1698950559/100days-17.jpg">
</a><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+17%3A+the+%3Apicture-in-picture+pseudo-class%E2%80%9D">blog@matuzo.at</a>.</p> Day 18: inheritable styles and web components2022-10-19T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day18
<p>We already know that we can <a href="/blog/2022/100daysof-day10/">encapsulate styles within a web component</a> by adding elements along with the styles to the shadow DOM. Global style declarations from outside don’t overwrite styles inside the web component.<br />
Shadow DOM doesn't provide total encapsulation, though.</p><p>If you look at the following component, you’ll notice that it uses the same font as the rest of the page, even though I haven't applied any styles to the web component. If styles were completely encapsulated, I would expect the component to use a default font like <em>Times</em>, but the web component inherits styles from its parent elements. </p>
<div class="sample">
<p>
<shadow-component></shadow-component>
</p>
</div>
<p class="code-label"><strong>HTML:</strong></p>
<pre><code class="language-html"><shadow-component></shadow-component></code></pre>
<p class="code-label"><strong>JS:</strong></p>
<pre><code class="language-js">class ShadowComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div>Hello World!</div>`
}
}
customElements.define('shadow-component', ShadowComponent);</code></pre>
<script>
class ShadowComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div class="div">Hello World!</div>`
}
}
customElements.define('shadow-component', ShadowComponent);
</script>
<p>If I wrap the component in a <code><div></code> and a set a <code>color</code> on the div, the color of the text inside the web component changes as well. That's because <a href="https://www.w3.org/TR/css-scoping-1/#inheritance">top-level elements of a shadow tree inherit from their host element</a>. In other words, <a href="https://web.dev/learn/css/inheritance/#which-properties-are-inheritable">inhertiable styles</a> will be passed to the shadow DOM.</p>
<pre><code class="language-css">.parent {
color: red;
}</code></pre>
<pre><code class="language-html"><div class="parent">
<shadow-component></shadow-component>
</div></code></pre>
<style>
.parent {
color: red;
}
</style>
<div class="sample">
<div class="parent">
<shadow-component></shadow-component>
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+18%3A+inheritable+styles+and+web+components%E2%80%9D">blog@matuzo.at</a>.</p> Day 19: the placeholder-shown pseudo-class2022-10-20T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day19
<p>You can use the <code>:placeholder-shown</code> pseudo-class to select input fields with a placeholder that haven't been filled out yet.</p><pre><code class="language-css">input:placeholder-shown {
outline: 5px solid blue;
}</code></pre>
<pre><code class="language-html"><p>
<label for="name">Name</label>
<input type="text" id="name" placeholder="your name…" value="Johanna">
</p>
<p>
<label for="email">E-Mail</label>
<input type="email" id="email" placeholder="name@domain.com">
</p></code></pre>
<style>
input:placeholder-shown {
outline: 5px solid blue;
}
</style>
<p>
<label for="name">Name</label>
<input type="text" id="name" placeholder="your name…" value="Johanna">
</p>
<p>
<label for="email">E-Mail</label>
<input type="email" id="email" placeholder="name@domain.com">
</p>
<p>PS: Yeah, I know, that one has been around for quite a while already, but I started writing CSS 15 years ago. So anything that was published after 2015 is modern CSS for me. :)</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+19%3A+the+placeholder-shown+pseudo-class%E2%80%9D">blog@matuzo.at</a>.</p> Day 20: the scrollbar-gutter property2022-10-21T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day20
<p>The <code>scrollbar-gutter</code> property allows you decide whether content within an element fills the total available space or if it stops at the scrollbar gutter. The scrollbar gutter is the space between the inner border edge and the outer padding edge of an element used by the scrollbar.</p><p>By default, if the content in an element is not overflowing, there's no gutter.</p>
<style>
.div {
width: min(100%, 31rem);
height: 28rem;
overflow: auto;
border: 2px solid;
}
.gutter {
scrollbar-gutter: stable;
}
</style>
<h2>Overflowing content</h2>
<div class="div">
Choose Life. Choose a job. Choose a career. Choose a family. Choose a fucking big television, choose washing machines, cars, compact disc players and electrical tin openers. Choose good health, low cholesterol, and dental insurance. Choose fixed interest mortgage repayments. Choose a starter home. Choose your friends. Choose leisurewear and matching luggage. Choose a three-piece suit on hire purchase in a range of fucking fabrics. Choose DIY and wondering who the fuck you are on Sunday morning. Choose sitting on that couch watching mind-numbing, spirit-crushing game shows, stuffing fucking junk food into your mouth. Choose rotting away at the end of it all, pissing your last in a miserable home, nothing more than an embarrassment to the selfish, fucked up brats you spawned to replace yourselves. Choose your future. Choose life... But why would I want to do a thing like that? I chose not to choose life. I chose somethin' else. And the reasons? There are no reasons. Who needs reasons when you've got heroin?
</div>
<h2>scrollbar-gutter: auto</h2>
<div class="div">
Choose Life. Choose a job. Choose a career. Choose a family. Choose a fucking big television, choose washing machines, cars, compact disc players and electrical tin openers. Choose good health, low cholesterol, and dental insurance. Choose fixed interest mortgage repayments. Choose a starter home. Choose your friends. Choose leisurewear and matching luggage. Choose a three-piece suit on hire purchase in a range of fucking fabrics.
</div>
<p>You can set <code>scrollbar-gutter</code> to <code>stable</code> to always show the gutter. This allows you to create more consistent and stable layouts.</p>
<pre><code class="language-css">.gutter {
scrollbar-gutter: stable;
}</code></pre>
<h2>Overflowing content</h2>
<div class="div">
Choose Life. Choose a job. Choose a career. Choose a family. Choose a fucking big television, choose washing machines, cars, compact disc players and electrical tin openers. Choose good health, low cholesterol, and dental insurance. Choose fixed interest mortgage repayments. Choose a starter home. Choose your friends. Choose leisurewear and matching luggage. Choose a three-piece suit on hire purchase in a range of fucking fabrics. Choose DIY and wondering who the fuck you are on Sunday morning. Choose sitting on that couch watching mind-numbing, spirit-crushing game shows, stuffing fucking junk food into your mouth. Choose rotting away at the end of it all, pissing your last in a miserable home, nothing more than an embarrassment to the selfish, fucked up brats you spawned to replace yourselves. Choose your future. Choose life... But why would I want to do a thing like that? I chose not to choose life. I chose somethin' else. And the reasons? There are no reasons. Who needs reasons when you've got heroin?
</div>
<h2>scrollbar-gutter: stable</h2>
<div class="gutter div">
Choose Life. Choose a job. Choose a career. Choose a family. Choose a fucking big television, choose washing machines, cars, compact disc players and electrical tin openers. Choose good health, low cholesterol, and dental insurance. Choose fixed interest mortgage repayments. Choose a starter home. Choose your friends. Choose leisurewear and matching luggage. Choose a three-piece suit on hire purchase in a range of fucking fabrics.
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+20%3A+the+scrollbar-gutter+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 21: conic gradients2022-10-24T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day21
<p>You can create gradients with color transitions rotated around a center point, rather than radiating from the center, by using the <code>conic-gradient()</code> function.</p><p>There are many options to customize conic gradients.</p>
<style>
.uno {
background-image: conic-gradient(aqua, fuchsia, salmon, aqua);
}
.due {
background-image: conic-gradient(aqua, fuchsia);
}
.tre {
background-image: conic-gradient(from 90deg, aqua, fuchsia);
}
.quattro {
background-image: conic-gradient(from 90deg at 4rem 4rem, aqua, fuchsia);
}
.cinque {
background-image: conic-gradient(from 90deg at 25% 75%, aqua, fuchsia);
}
.sei {
background-image: conic-gradient(from 90deg at center left, aqua, fuchsia);
}
.sette {
background-image: conic-gradient(aqua 0deg, fuchsia 120deg, salmon 240deg, aqua 360deg);
}
.otto {
background-image: conic-gradient(aqua 0deg, fuchsia 80deg, salmon 130deg, aqua 360deg);
}
.nove {
background-image: conic-gradient(aqua 0deg 50deg, fuchsia 80deg 100deg, salmon 130deg 140deg, aqua 360deg)
}
.dieci {
background-image: conic-gradient(aqua 0deg 120deg, fuchsia 120deg 240deg, salmon 240deg 360deg);
}
.div {
width: 10rem;
height: 10rem;
border-radius: 50%;
}
</style>
<h2>Colors only</h2>
<p>The simplest way to create a conic gradient is to pass a list of colors to the function.</p>
<pre><code class="language-css">div {
background-image: conic-gradient(aqua, fuchsia, salmon, aqua);
}</code></pre>
<div class="div uno"></div>
<h2>Angle</h2>
<p>By default, the gradient starts at 0 degrees and rotates clockwise.</p>
<pre><code class="language-css">div {
background-image: conic-gradient(aqua, fuchsia);
}</code></pre>
<div class="div due"></div>
<p>You can pass an angle, preceded by the “from” keyword, to change the starting point.</p>
<pre><code class="language-css">div {
background-image: conic-gradient(from 90deg, aqua, fuchsia);
}</code></pre>
<div class="div tre"></div>
<h2>Off-centered gradient</h2>
<p>By default, the center pointer of the gradient is at the center of the element. You can change that by passing a length, percentage, or keyword.</p>
<h3>Length</h3>
<pre><code class="language-css">div {
background-image: conic-gradient(from 90deg at 4rem 4rem, aqua, fuchsia);
}</code></pre>
<div class="div quattro"></div>
<h3>Percentage</h3>
<pre><code class="language-css">div {
background-image: conic-gradient(from 90deg at 25% 75%, aqua, fuchsia);
}</code></pre>
<div class="div cinque"></div>
<h3>Keywords</h3>
<pre><code class="language-css">div {
background-image: conic-gradient(from 90deg at center left, aqua, fuchsia);
}</code></pre>
<div class="div sei"></div>
<h2>Custom color stop positions</h2>
<p>By default, color stops are placed halfway between the one that precedes it and the one that follows it, with color transitioning smoothly. The first is at 0deg and the last at 360deg.<br />
The following two gradients are equivalent.</p>
<pre><code class="language-css">div {
background-image: conic-gradient(aqua, fuchsia, salmon, aqua);
background-image: conic-gradient(aqua 0deg, fuchsia 120deg, salmon 240deg, aqua 360deg);
}</code></pre>
<div class="div sette"></div>
<p>You can move the transitiom midpoint for any color. </p>
<pre><code class="language-css">div {
background-image: conic-gradient(aqua 0deg, fuchsia 80deg, salmon 130deg, aqua 360deg);
}</code></pre>
<div class="div otto"></div>
<p>You can pass a second color stop value to define where the color starts and stops.</p>
<pre><code class="language-css">div {
background-image: conic-gradient(aqua 0deg 50deg, fuchsia 80deg 100deg, salmon 130deg 140deg, aqua 360deg)
}</code></pre>
<div class="div nove"></div>
<p>If two or more colors are at the same location, there will be a hard line between the colors.</p>
<pre><code class="language-css">div {
background-image: conic-gradient(aqua 0deg 120deg, fuchsia 120deg 240deg, salmon 240deg 360deg);
}</code></pre>
<div class="div dieci"></div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+21%3A+conic+gradients%E2%80%9D">blog@matuzo.at</a>.</p> I broke the rules.2022-10-25T00:00:00+00:00https://www.matuzo.at/blog/2022/twitter-ban
<p>When I opened Twitter on Monday morning, I saw this:</p><img alt="Screenshot of parts of my Twitter profile that says: Your account is permanently banned. After careful review, we determined you account broke the Twitter Rules. Your account is permanently in read-only mode, which means you can't Tweet, Retweet, or Like content. You won't be able to create new accounts. If you think we got this wrong, you can submit an appeal." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/twitter-ban/15b5955c15-1698950559/twitter_ban.jpg">
<p>After careful review, they determined that my account broke the <a href="https://help.twitter.com/en/rules-and-policies/twitter-rules">Twitter Rules</a>. Okay, let's have a look at the rules to get an idea of what could've caused this ban. You break the rules if your tweets contain content related to anything in this list:</p>
<ul>
<li>Violence</li>
<li>Terrorism/violent extremism</li>
<li>Child sexual exploitation</li>
<li>Abuse/harassment</li>
<li>Hateful conduct</li>
<li>Perpetrators of violent attacks</li>
<li>Suicide or self-harm</li>
<li>Sensitive media, including graphic violence and adult content</li>
<li>Illegal or certain regulated goods or services</li>
</ul>
<p>I can assure you I tweeted nothing related to any of these things. Also, I didn't spam, I didn't try to manipulate elections, I didn't impersonate anyone, I didn't share synthetic or manipulated media, and I did not violate others’ intellectual property rights.</p>
<p>So, what happened? I have absolutely no idea! Here's a rough outline of the events:</p>
<style>
ol li + li {
margin-top: 1.5rem;
}
</style>
<ol>
<li>
<p><strong>21.10. Comment on Unas question</strong></p>
<p>On Friday Una asked on Twitter <a href="https://twitter.com/mmatuzo/status/1583522924299841537">What do you want to see on the web platform next year?”</a> and I responded “A native sr-only attribute.”. My reply got a bunch of likes and some retweets.<br>
So, nothing special happened here.</p>
</li>
<li>
<p><strong>22.10. Hidden replies</strong></p>
<p>But then suddenly on Saturday people reported that <a href="https://twitter.com/openuicg/status/1583680688938631169">they did't see my reply anymore</a>.</p>
<p>
<img alt="Screenshot Twitter. Instead of my reply it says 'this tweet is unavailable" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/twitter-ban/d1af67ae64-1698950560/twitter_ban2.jpg">
</p>
<p><a href="https://twitter.com/bramus/status/1583822357201711105">Bramus assumed that this was a bug in the Twitter app</a>, but Šime pointed out that <a href="https://twitter.com/simevidas/status/1583913449716711425">I got shadowbanned</a>.</p>
</li>
<li>
<p><strong>23.10. Contacted Twitter support</strong></p>
<p>On Sunday I contacted Twitter support and reported the ban. I told them that I got shadowbanned for no reason and I asked them to look into the matter.
Being shadowbanned means that I can still like, comment, and tweet, but people don't see my content. Also, people can't search for my content on Twitter. For me, everything looks fine, but in reality I'm not reaching anyone. This is usually a measure against spammers.</p>
</li>
<li>
<p><strong>24.10. Permanently suspeneded</strong></p>
<p>I woke up to the news that my account was permanently banned.</p>
</li>
<li>
<p><strong>24.10. A reply from Twitter</strong></p>
<p>Later that day, I got a reply to my report:</p>
<div class="quote">
<blockquote>Hello,
<p>Thank you for contacting us. </p>
<p>We do not block, limit, or remove content based on an individual’s views or opinions. In some situations, your Tweet may not be seen by everyone, as outlined in this Help Center article. You can find more information about shadow banning and our efforts to promote a healthy public conversation in our <a href="https://blog.twitter.com/official/en_us/topics/company/2018/Setting-the-record-straight-on-shadow-banning.html">blog post</a>.</p>
<p>If you would also like more information on how to control your Twitter experience, see this <a href="https://support.twitter.com/articles/20170134">help article</a>. </p>
<p>Please see the <a href="https://help.twitter.com/en/safety-and-security/tweet-visibility">Help Center Article</a> for more information:</p>
<p>Best,<br />
Twitter</blockquote></div></p>
<p>In the linked post it says <q>We do not shadow ban. You are always able to see the tweets from accounts you follow.</q>.
This is a lie. There's proof in the screenshot above.</p>
</li>
<li>
<p><strong>24.10. Submitted an appeal</strong> </p>
<p>I've submitted an appeal and asked them again to look into the matter and tell me what happened.</p>
</li>
<li><p><strong>25.10 You ❤️</strong>
<p>Today I had multiple mails in my inbox from people offering their help and asking if I was okay. You're amazing, thank you! ❤️ I'm fine, it's just annoying.</p>
<p>Thank you everyone for @-ing Twitter Support (<a href="https://twitter.com/spuz78/status/1584470084231516161">Bernhard</a> and <a href="https://twitter.com/m_ott/status/1584648614282133504">Matthias</a>) and retweeting, and especially thank you Bernhard, Matthias, Mike, David, Jenn, Jens, and Sara for getting in touch via e-mail.</p>
</li>
<p>What's next? I don't know. I'll just wait for their reply. I'll keep you updated in this blog post.</p>
<li id="update_2610">
<p><strong>26.10 Support and reply from Twitter Support</strong></p>
<p>
I'm blown away by the amount of support I've received. Literally 100s of tweets @-ing Twitter-Support. Thank you! Thank you! Thank you!
</p>
<p>
<img alt="Twitter search for 'Bring back @mmatuzo, @TwitterSupport' showing dozens of results by all kinds of people." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/twitter-ban/52906e0c60-1698950561/twitter_ban5.jpg">
</p>
<p> Some of you even put the message in their Twitter name. 😁</p>
<img alt="Zack Leathermans profil showing his portrait, bio and twitter name 'Zack Leatherman–bring back @mmatuzo'" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/twitter-ban/92f1737b17-1698950560/twitter_ban3.jpg">
<p>And others created memes. 😁</p>
<img alt="Bring back @mmatuzo paid for by the Manuel Hates Lobester Foundation written in Lobster font" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/twitter-ban/70a3ef83a0-1698950561/twitter_ban4.jpg">
<img alt="Crowd from the TV show Southpark protesting and holding up signs that say Freedom for @mmatuzo, free @mmatuzo or @mmatuzo didn't do it." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/twitter-ban/2202dcf7aa-1698950561/twitter_ban6.jpg">
<p>Other than that, nothing much happened except that I've filed another appeal and someone from Twitter Support replied to one of the tweets asking for a case number, which Max provided for me. Let's see what happens next.</p>
</li>
<li id="update_3110">
<p><strong>31.10 no news</strong></p>
<p>I'm still suspended and I still haven't heard a word from Twitter. You can find me on <a href="https://mastodon.social/@matuzo">Mastodon</a> now.</p>
</li>
</ol>
<p>Yours truly,<br />
Manuel</p>
<p>PS: A div is still not a button.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CI+broke+the+rules.%E2%80%9D">blog@matuzo.at</a>.</p> Day 22: the ::backdrop pseudo-element2022-10-25T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day22
<p>You can use the <code>::backdrop</code> pseudo-element to style the backdrop of <a href="/blog/2022/100daysof-day15/">modal dialogs</a> and elements which have been placed in fullscreen mode using the Fullscreen API.</p><style>
::backdrop {
background-color: rgb(0 0 155 / 0.5);
}
.dialog2::backdrop {
background: conic-gradient(red, orange, yellow, green, red);
outline: 20px solid white;
outline-offset: -40px;
}
dialog {
width: min(30rem, 100%);
aspect-ratio: 16 / 9;
}
</style>
<pre><code class="language-css">::backdrop {
background-color: rgb(0 0 0 / 0.5);
}</code></pre>
<dialog class="dialog1">
yo!
<button class="close1">close</button>
</dialog>
<p><button class="showModal1">show modal</button></p>
<p>You don't have to limit yourself to semi-transparent background color.</p>
<pre><code class="language-css">::backdrop {
background: conic-gradient(red, orange, yellow, green, red);
outline: 20px solid white;
outline-offset: -20px;
}</code></pre>
<dialog class="dialog2">
yo!
<button class="close2">close</button>
</dialog>
<p><button class="showModal2">show modal</button></p>
<script>
const showModal1 = document.querySelector('.showModal1')
const showModal2 = document.querySelector('.showModal2')
const close1 = document.querySelector('.close1')
const close2 = document.querySelector('.close2')
const dialog1 = document.querySelector('.dialog1')
const dialog2 = document.querySelector('.dialog2')
showModal1.addEventListener('click', e => {
dialog1.showModal()
})
showModal2.addEventListener('click', e => {
dialog2.showModal()
})
close1.addEventListener('click', e => {
dialog1.close()
})
close2.addEventListener('click', e => {
dialog2.close()
})
</script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+22%3A+the+%3A%3Abackdrop+pseudo-element%E2%80%9D">blog@matuzo.at</a>.</p> Day 23: the lab() color function2022-10-26T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day23
<p>The <code>lab()</code> color function allows you to pick colors from the <a href="https://en.wikipedia.org/wiki/CIELAB_color_space">CIELAB color space</a>, which is device-independant and covers the entire gamut (range) of human color perception.</p><p>Currently, the CSS colors we can define are in the sRGB color space. For the longest time, professional monitors weren’t able to display all possible colors in this range. So, using sRGB colors was absolutely sufficient, but that’s not true anymore. Nowadays, monitors can display much more colors than exist in the sRGB color space. With <code>lab()</code> we get access to these colors (currently <a href="https://caniuse.com/css-lch-lab">Safari 15+ only</a>).</p>
<p>The function takes 3 space-separated values.</p>
<pre><code class="language-css">div {
background-color: lab(78% -64 -160);
}</code></pre>
<figure style="max-width: 250px">
<img alt="bright light blue color" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day23/cdc2984729-1698950565/lab_1.png" width="250">
<figcaption> Screenshot of Michelle Barkers <a href="https://codepen.io/smashingmag/pen/JjywKNK">“LAB color explorer”</a> on CodePen</figcaption>
</figure>
<h2>l - lightness</h2>
<p>The first value defines the lightness. It's typically a number between 0% (representing black) and 100% (representing white). It's <em>typically</em> a number between 0% and 100% because the value can exceed 100% up to 400% representing extra-bright whites on some systems.</p>
<pre><code class="language-css">div {
background-color: lab(0% 0 0);
}</code></pre>
<img alt="black" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day23/e89b597bc1-1698950565/lab_2.png" width="250">
<h2>a-axis and b-axis</h2>
<p>The a and b axes convey hue. The value for each axis is theoretically unbounded, but in practice doesn't exceed -160 and 160.</p>
<p>Negative values along the a-axis are green. Positive values are red.</p>
<pre><code class="language-css">div {
background-color: lab(33% -123 0);
}</code></pre>
<img alt="dark green color" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day23/c4c0ce11c6-1698950565/lab_3.png" width="250">
<p>Negative values along the b-axis are blue. Positive values are yellow.</p>
<pre><code class="language-css">div {
background-color: lab(85% 0 114);
}</code></pre>
<img alt="dark yellow color" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day23/4bdf8ab23f-1698950565/lab_4.png" width="250"><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+23%3A+the+lab%28%29+color+function%E2%80%9D">blog@matuzo.at</a>.</p> Day 24: the backdrop-filter property2022-10-27T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day24
<p>The <code>backdrop-filter</code> property allows you to apply CSS filters to the area behind an element. This could be the background of an element or the backdrop of a dialog.</p><p>In the following example, the parent element has a background image, nothing special about it, but the inner elements all have a <code>backdrop-filter</code> applied which changes how the image beneath them is displayed.</p>
<pre><code class="language-html"><div class="parent">
<div class="blur">Blur</div>
<div class="invert">Invert</div>
<div class="hue">Hue</div>
<div class="grayscale">Grayscale</div>
</div></code></pre>
<pre><code class="language-css">.parent {
background-image: url("/images/neue-donau.webp");
}
.blur {
backdrop-filter: blur(5px);
}
.invert {
backdrop-filter: invert(1);
}
.hue {
backdrop-filter: hue-rotate(260deg);
}
.grayscale {
backdrop-filter: grayscale(100%);
}</code></pre>
<style>
.parent {
background-image: url("/blog/2022/100daysof-day24/neue-donau.webp");
}
.blur {
backdrop-filter: blur(5px);
}
.invert {
backdrop-filter: invert(1);
}
.hue {
backdrop-filter: hue-rotate(260deg);
}
.grayscale {
backdrop-filter: grayscale(100%);
}
.parent {
max-width: 50rem;
aspect-ratio: 500 / 330;
height: 100%;
background-size: cover;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 1rem;
padding: 1rem;
box-sizing: border-box;
font-family: sans-serif;
}
.parent:is(:hover, :focus) > * {
opacity: 0;
}
.inner {
padding: 1rem;
font-size: 2rem;
text-shadow: 1px 1px 3px #fff;
transition: opacity 0.3s;
}
</style>
<div class="parent" tabindex="0">
<div class="inner blur">Blur</div>
<div class="inner invert">Invert</div>
<div class="inner hue">Hue</div>
<div class="inner grayscale">Grayscale</div>
</div>
<p>Notice how the filters don’t affect the text? Yeah, that’s the difference between <code>backdrop-filter</code> and <code>filter</code>. They both take the same values, but backdrop-filters only apply to the backdrop/background. In order for this to work the background of the element with the backdrop filter must either be fully or partially transparent.</p>
<p>You can also apply the filter to the backdrop of a dialog.</p>
<pre><code class="language-css">dialog::backdrop {
backdrop-filter: blur(5px);
}</code></pre>
<style>
dialog::backdrop {
backdrop-filter: blur(5px);
}
</style>
<dialog class="dialog">
yo!
<button class="closeButton">close</button>
</dialog>
<p><button class="showModalButton">show modal</button></p>
<script>
const showModalButton = document.querySelector('.showModalButton')
const closeButton = document.querySelector('.closeButton')
const dialog = document.querySelector('.dialog')
showModalButton.addEventListener('click', e => {
dialog.showModal()
})
closeButton.addEventListener('click', e => {
dialog.close()
})
</script>
<p>PS: Thanks Kilian for the pointer!</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+24%3A+the+backdrop-filter+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 25: scrollbar gutters in body and html2022-10-28T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day25
<p>When I wrote about the <a href="/blog/2022/100daysof-day20/">scrollbar-gutter property</a>, my first thought was “omg! I'll put this in my reset stylesheet and use it on the <code><body></code> by default”. I wanted to do that in order to prevent the page from “jumping” when switching from a long to a short page, a page with overflow to one without.</p><p>Here's a quick demo to illustrate the issue. </p>
<figure>
<figure class="video"><video controls><source src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day25/73a311c520-1698950570/scrollbar-gutter.mov" type="video/quicktime"></video></figure>
<figcaption>When switching from a page with a scrollbar to a page without, you can see how the whole page shifts to the left because the scrollbar takes up space on longer pages.</figcaption>
</figure>
<p>So I tried this…</p>
<pre><code class="language-css">body {
scrollbar-gutter: stable;
}</code></pre>
<p>…and it didn't work.</p>
<p>I looked at the spec and there it says:</p>
<blockquote>
However, unlike the overflow property, the user agent must not propagate scrollbar-gutter from the HTML body element.
</blockquote>
<p>So, <code>overflow</code> on the <code>body</code> is propagated to the viewport, which absolutely makes sense. Just try to set a width on the body with a lot of content, you'll see how the width changes, but the scrollbar is still on the inline end of the viewport.</p>
<img alt="" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day25/b7fdebf44f-1698950568/scrollbar-gutter1.png">
<p>If I understand correctly, <code>scrollbar-gutter</code> has no effect on the body because <code>overflow</code> is propagated to the viewport, but <code>scrollbar-gutter</code> isn't. If there's no overflow, then there's no scrollbar gutter. It kinda makes sense to me. (Please correct me if I'm wrong. :))</p>
<p>When I moved the declaration to the <code><html></code> element, it worked! That's because both <code>overflow</code> and <code>scrollbar-gutter</code> used on <code><html></code> are propagated to the viewport.</p>
<pre><code class="language-css">html {
scrollbar-gutter: stable;
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+25%3A+scrollbar+gutters+in+body+and+html%E2%80%9D">blog@matuzo.at</a>.</p> Day 26: using combinators in :has()2022-10-31T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day26
<p>You <a href="/blog/2022/100daysof-day6/">already know</a> that the <code>:has()</code> pseudo-class allows you to check whether a parent element contains certain children, but you can also make this selector more specific, or check other relations the element might have.</p><h2>Child combinators</h2>
<p>You can check whether an element contains a specific <em>direct</em> child element. </p>
<p>For example, if you have a <code>fieldset</code> and you want to make sure that it contains a <code>legend</code> and that this <code>legend</code> is actually a direct child item of the <code>fieldset</code>, <a href="/blog/2022/divs-are-bad/">which is important</a>, you could use the child combinator (<code>></code>) in your <code>:has()</code> pseudo-class.</p>
<style>
fieldset:not(:has(> legend)) {
border: 10px solid red;
}
</style>
<pre><code class="language-css">fieldset:not(:has(> legend)) {
border: 10px solid red;
}</code></pre>
<pre><code class="language-html"><fieldset>
<div>
<legend>Letters</legend>
</div>
<input type="radio" name="letters" id="a">
<label for="a">a</label>
<input type="radio" name="letters" id="b">
<label for="b">b</label>
</div>
</fieldset></code></pre>
<div data-sample="demo">
<fieldset>
<div>
<legend>Letters</legend>
</div>
<input type="radio" name="letters" id="a">
<label for="a">a</label>
<input type="radio" name="letters" id="b">
<label for="b">b</label>
</div>
</fieldset>
</div>
<h2>Next-sibling combinators</h2>
<p><code>:has()</code> is not just a parent selector, you can select elements based on other relations, too. By using the next-sibling combinator, you can check whether an element has a specific next sibling element, and style it accordingly.</p>
<pre><code class="language-css">h2 {
margin-block-end: 0.7em;
}
h2:has(+ time) {
margin-block-end: 0;
}</code></pre>
<p>The <code><h2></code> has a block end margin of <code>0.7em</code> by default, but when its next sibling is a <code><time></code> element, the margin is 0.</p>
<style>
h2:where(.demo) {
line-height: 1;
margin-block-end: 0.7em;
}
h2:has(+ time) {
margin-block-end: 0;
}
</style>
<div data-sample="demo - h2 followed by p">
<article>
<h2 class="demo">Heading</h2>
<p>Teaser text</p>
</article>
</div>
<div data-sample="demo - h2 followed by time">
<article>
<h2 class="demo">Heading</h2>
<time>31.10.2022</time>
<p>Teaser text</p>
</article>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+26%3A+using+combinators+in+%3Ahas%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Your account is permanently suspended2022-11-01T00:00:00+00:00https://www.matuzo.at/blog/2022/your-account-is-permanently-suspended
<p>On October 23rd I got <a href="/blog/2022/twitter-ban/">shadowbanned on Twitter</a>, followed by a permanent suspension on October 25th. As someone who was very active on Twitter, I was surprised, shocked, and sad that this happened. Especially because I didn’t know why it happened.</p><img alt="Screenshot of parts of my Twitter profile that says: Your account is permanently banned. After careful review, we determined you account broke the Twitter Rules. Your account is permanently in read-only mode, which means you can't Tweet, Retweet, or Like content. You won't be able to create new accounts. If you think we got this wrong, you can submit an appeal." height="366" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/your-account-is-permanently-suspended/fe6ae352d9-1698950569/twitter_ban.jpg" width="726">
<p>Some of you might think, <em>“Who cares, it’s just social media?!”</em>, and yeah, you’re right, it is just social media, but it was a pretty big deal. For me personally, I’ll go into detail about why later in this post, and generally because I got suspended without a clue why and with no notice. That’s something that shouldn’t happen on one of the biggest social media sites worldwide.<br />
This whole incident got many people thinking, and it started some interesting discussions. Most people were in disbelief at how someone who mostly posts harmless web dev related content could get suspended out of nowhere. Many talked about the importance of owning your content and the <a href="https://indieweb.org/">indie web</a>. Some were concerned about their freedom of speech, and some believed that things will get better once Elon Musk takes over. Others talked about <abbr title="artificial intelligence">AI</abbr> and how dangerous algorithms can be.<br />
I don’t want to get into these topics because there are smarter people who could do that way better than me, but I do want to summarise the thoughts that went through my head in the past couple of days.</p>
<h2>Owning your Content</h2>
<p>Many people pointed out that this suspension was a perfect example of how important it is to own your own content. Owning your content means that you don’t create content exclusively on a 3rd party site like Twitter or Facebook, but on your own web space and you only syndicate content to other sites. If you get suspended from Twitter, Facebook, Instagram, Medium, etc. and all your content only exists on these sites, it might happen that from one day to next you don’t have access to your thoughts, ideas, memories, images, demos, etc. anymore.</p>
<p>I’m really glad that I have a personal website and that I blog there and not on Medium anymore, like I used to several years ago. I also started posting shorter posts like <a href="/TIL/"><abbr title="Today I learned">TILs</abbr></a> on my site, but if I’m honest, I didn’t go all the way. I still post stuff on Twitter first or even Twitter only because it’s just more convenient to post directly to Twitter than to my site and then sharing it on Twitter. Of course, this is just a matter of tooling. I just have to take the time and make it easier for myself to post to my site and <a href="https://matthiasott.com/notes/syndicating-posts-personal-website-twitter-mastodon">syndicate content</a>.</p>
<p>So, I mostly already got that right, but what I learned with this suspension is that owning your content goes beyond blog posts and notes.</p>
<h3>Images</h3>
<p>That’s something that surprised me: I can still see my posts, but I don’t have access to my images anymore. Most photos are somewhere on my phone, but all the other images (screenshots, code snippets, etc.) are gone now.</p>
<img alt="Screenshot of my suspended Twitter profile. Two posts with images, but instead of the images you only see a placeholder." height="547" loading="auto" src="https://www.matuzo.at/media/pages/blog/2022/your-account-is-permanently-suspended/d5e4ea9408-1698950569/twitter_ban7.jpg" width="873">
<h3>Messages</h3>
<p>Being suspended on Twitter also means that you don’t have access to your messages anymore. I’ve used direct messaging quite a lot. Not just for small talk and other casual conversations, but sometimes also for discussing business, side projects, etc. Not having access to these conversations anymore sucks!<br />
I suppose the person on the other end still has access to all messages. They could just copy and paste the important bits and shift the conversation to email, but it’s still really annoying.</p>
<p>The lesson learned here is, once conversations get important, move them to email.</p>
<h3>Bookmarks</h3>
<p>This one is really bugging me. Bookmarking tweets was a feature I liked a lot. I used it to save tweets for later reference. All my “research” is gone now.</p>
<p>Lesson learned: Save important tweets elsewhere and/or take screenshots.</p>
<h3>Comments</h3>
<p>Most conversations regarding the content I created happened on Twitter or via e-mail. Believe it or not, but one of my favourite things about Twitter was that people could give me direct feedback on my posts or point out mistakes.</p>
<p>It’s personal preference whether you want to enable people to comment on your blog posts, but I’ll definitely add a comment section to my blog.</p>
<h3>Logins</h3>
<p>A day after my suspension I got an email from keybase telling me that my previously-proven twitter identity broke. </p>
<p>Lesson learned: It’s probably not the best idea to use social media accounts to log into or identify with other sites.</p>
<h2>Business</h2>
<p>Usually when people contact me with business inquiries I ask them how they found me. Often they say that someone recommended me, but sometimes they would tell me that they followed me on Twitter and that they liked my content.</p>
<p>Being active on Twitter and sharing content played a pretty big role for me as a freelancer. If I’m not on Twitter anymore and if I don’t post elsewhere, will I get less jobs? Yes, probably, because I believe that – at least for me – having a healthy and active online persona is essential. Do I need Twitter for that? If you would’ve had asked me 3 years ago, I would’ve said “Yes”, but now I don’t believe that anymore. I'm convinced that putting all the effort I used to put into social media into my own site will work just as well or even better.</p>
<h2>Responsibilities</h2>
<p>Most people were convinced that this suspension must have been a mistake because my account was pretty harmless, but some people shared ideas with me about why I could’ve been banned. Some theories were related to what I said in the past and how I said it. It surprised me that people remembered things I said because they perceived them so negatively. I know that I’m not very well known in the web dev community, but I literally <s>have</s> had thousands of followers. This means that many people read my posts and they care about what I say.</p>
<p>I guess what I learned here is that as a somewhat public person, even with a small audience, I have certain responsibilities and before I post something overly negative, I should reconsider my words. The ultimate goal is to not be an asshole like the many other people on social media I despise so much.</p>
<h2>Friendship</h2>
<p>I few years ago I talked with a colleague about something, I don’t remember anymore what it was. It doesn’t really matter, but in this conversation I said something like “…and my friends on Twitter”. My colleague asked “Are so many of your friends on Twitter?” and I replied “Oh, my <abbr title="in real life">IRL</abbr> friends? No. I’m talking about the friends I met online”. He then said, “But they aren’t real friends. You barely know these people and you haven’t met most of them.”. I replied that I consider them friends, but there was an uncertainty in my voice.</p>
<p>When my account got suspended, one of the worst things was that I wasn’t able to get in touch with my friends easily anymore – yes, they/you are my friends. I missed reading their ideas, jokes, and rants. It’s true that I barely know most of them and that I’ve only met a couple of people I interact with online every day in real life, but how would you call someone who writes you an e-mail shortly after your suspension to check whether you’re okay and if there’s anything they can do to help? How would you call people who flood Twitter Support with messages demanding them to reenable my account?</p>
<p>A site like Twitter is not just a sharing platform or a marketing tool, but also a place to hang out with friends.</p>
<h2>Conclusion</h2>
<p>It’s been a week since my suspension and I haven’t heard a word from Twitter. In the meantime Elon Musk took over and has already made some disturbing decisions and statements. Even if I get my account back, I will not use Twitter anymore, at least not how I used to. I’ve you want to stay in touch with me online, find me on <a href="https://mastodon.social/@matuzo">Mastodon</a>.</p>
<p>If there’s something I’ve learned from this whole thing, it’s that I must be more careful with how and where I share my content. A social media platform should not be the primary source. I’ve often advised small business owners to not just use their Facebook profile as their website, for obvious reasons. I, you probably as well, should also take my advice and do the same for my online presence. Create everything on my own website and syndicate elsewhere, because you never know what might happen to your content or profile tomorrow.</p>
<p>Now is a good time to reclaim control over your content.</p>
<ul>
<li><a href="https://matthiasott.com/articles/into-the-personal-website-verse">Into the Personal-Website-Verse</a></li>
<li><a href="https://matthiasott.com/articles/going-indie-securing-privacy">Going Indie. Step 1: Securing Privacy</a></li>
<li><a href="https://matthiasott.com/articles/going-indie-reclaiming-content">Going Indie. Step 2: Reclaiming Content</a></li>
<li><a href="https://mxb.dev/blog/the-return-of-the-90s-web/">The Return of the 90s Web</a></li>
<li><a href="https://matthiasott.com/notes/suspension">Suspension</a></li>
<li><a href="https://www.zachleat.com/web/own-my-tweets/">I’m Taking Ownership of My Tweets</a></li>
<li><a href="https://indieweb.org/Getting_Started">Indieweb: Getting Started</a></li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CYour+account+is+permanently+suspended%E2%80%9D">blog@matuzo.at</a>.</p> Day 27: the font-variation-settings property2022-11-01T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day27
<p>Adjustable features of a variable font are called axes. You can use the <code>font-variations-settings</code> property to change these features by specifying the four letter axis name along with a value.</p><style>
@font-face {
font-family: 'Saira';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(/blog/2022/100daysof-day27/Saira-VariableFont.woff2) format('woff2');
}
.demo {
font-family: 'Saira';
font-size: 3rem;
}
.demo2 {
font-variation-settings: 'wght' 736;
}
.demo3 {
font-variation-settings: 'wdth' 36;
}
.demo4 {
font-variation-settings: 'wght' 736, 'wdth' 36;
}
</style>
<p>For example, the <a href="https://fonts.google.com/specimen/Saira/tester?query=saira&vfonly=true">Saira variable font</a> has two axes, weight ('wght') and width ('wdth'). This is how the font looks like by default:</p>
<p class="demo">This is just a test.</p>
<p>You can set the weight to a value between 100 and 900.</p>
<pre><code class="language-css">p {
font-variation-settings: 'wght' 736;
}</code></pre>
<p class="demo demo2">This is just a test.</p>
<p>You can set the width to a value between 50 and 125.</p>
<pre><code class="language-css">p {
font-variation-settings: 'wdth' 36;
}</code></pre>
<p class="demo demo3">This is just a test.</p>
<p>Of course, you can also combine them.</p>
<pre><code class="language-css">p {
font-variation-settings: 'wght' 736, 'wdth' 36;
}</code></pre>
<p class="demo demo4">This is just a test.</p>
<p>The number and the kind of axes a font supports, depends on the font. Some have just one or two axes, <a href="https://fonts.google.com/specimen/Roboto+Flex/tester?vfonly=true&query=roboto+flex">others have many</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+27%3A+the+font-variation-settings+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 28: custom properties and web components2022-11-02T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day28
<p>We already know that we can <a href="/blog/2022/100daysof-day10/">encapsulate styles within a web component</a> and we know that <a href="/blog/2022/100daysof-day18/">web components inherit styles</a>. Another interesting feature of web components in terms of CSS is that custom properties used in a web component can be modified from the outside.</p><p>Let's take this basic alert component.</p>
<script>
class Alert extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const styles = document.createElement('style');
styles.textContent = `
div {
background-color: var(--alert-bg, rgb(136 177 255 / 0.5));
color: var(--alert-color, rgb(0 0 0));
font-weight: bold;
padding: var(--alert-spacing, 1rem);
}
`
const content = document.createElement('div');
content.innerHTML = `
<slot></slot>
`
this.shadowRoot.append(styles)
this.shadowRoot.append(content)
}
}
customElements.define('matuzo-alert', Alert);
</script>
<div class="sample">
<matuzo-alert>
Please confirm your e-mail address by clicking the link in the e-mail we just sent you.
</matuzo-alert>
</div>
<pre><code class="language-js">class Alert extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const styles = document.createElement('style');
styles.textContent = `
div {
background-color: rgb(136 177 255 / 0.5);
color: rgb(0 0 0);
font-weight: bold;
padding: 1rem;
}
`
const content = document.createElement('div');
content.innerHTML = `
<slot></slot>
`
this.shadowRoot.append(styles)
this.shadowRoot.append(content)
}
}
customElements.define('matuzo-alert', Alert);</code></pre>
<pre><code class="language-html"><matuzo-alert>
Please confirm your e-mail address by clicking the link in the e-mail we just sent you.
</matuzo-alert></code></pre>
<p>Now, let's say we want to reuse this component, but convey a different importance visually. We could add attributes (props) for styling to the web component.</p>
<pre><code class="language-html"><matuzo-alert type="error">
The amount must be a value between 1 and 16.
</matuzo-alert></code></pre>
<pre><code class="language-js">styles.textContent = `
div {
background-color: rgb(136 177 255 / 0.5);
color: rgb(0 0 0);
font-weight: bold;
padding: 1rem;
}
host([type="error"]) div {
…
}
`</code></pre>
<p>This works and it’s also sometimes the preferred way, but we could also open the component up by using custom properties.</p>
<pre><code class="language-js">const styles = document.createElement('style');
styles.textContent = `
div {
background-color: var(--alert-bg, rgb(136 177 255 / 0.5));
color: var(--alert-color, rgb(0 0 0));
font-weight: bold;
padding: var(--alert-spacing, 1rem);
}
`</code></pre>
<p>What's happening here is that we set the <code>background-color</code>, <code>color</code>, and <code>padding</code> to a custom property. If the custom property isn't defined, it <a href="/blog/2022/100daysof-day1">falls back to default value</a>. The web component still looks the same, but we can now change its styling according to our needs by modifying custom properties without touching the component.</p>
<style>
matuzo-alert {
display: block;
margin-bottom: 1rem;
}
.error {
--alert-bg: rgb(255 119 119);
--alert-spacing: 2rem;
}
.success {
--alert-bg: rgb(39 149 39);
--alert-color: rgb(255 255 255);
}
</style>
<h2>Default</h2>
<div class="sample">
<matuzo-alert>
Please confirm your e-mail address by clicking the link in the e-mail we just sent you.
</matuzo-alert>
</div>
<pre><code class="language-html"><matuzo-alert>
Please confirm your e-mail address by clicking the link in the e-mail we just sent you.
</matuzo-alert></code></pre>
<h2>Error</h2>
<div class="sample">
<matuzo-alert class="error">
The amount must be a value between 1 and 16.
</matuzo-alert>
</div>
<pre><code class="language-css">.error {
--alert-bg: rgb(255 119 119);
--alert-spacing: 2rem;
}</code></pre>
<pre><code class="language-html"><matuzo-alert class="error">
The amount must be a value between 1 and 16.
</matuzo-alert></code></pre>
<h2>Success</h2>
<div class="sample">
<matuzo-alert class="success">
Settings saved successfully.
</matuzo-alert>
</div>
<pre><code class="language-css">.success {
--alert-bg: rgb(39 149 39);
--alert-color: rgb(255 255 255);
}</code></pre>
<pre><code class="language-html"><matuzo-alert class="success">
The amount must be a value between 1 and 16.
</matuzo-alert></code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+28%3A+custom+properties+and+web+components%E2%80%9D">blog@matuzo.at</a>.</p> Day 29: !important custom properties2022-11-03T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day29
<p>Using <code>!important</code> with custom properties might not work as you expect.</p><p>If you look at the following example, which color does the text have?</p>
<style>
.example1 {
--color: red;
color: var(--color) !important;
color: blue;
}
.example2 {
--color: red !important;
color: var(--color);
color: blue;
}
.example3 {
--color: red;
--color: blue;
color: var(--color);
}
.example4 {
--color: red !important;
--color: blue;
color: var(--color);
}
.example5 {
--color: red !important;
--color: blue;
color: var(--color);
color: blue;
}
</style>
<pre><code class="language-css">.example1 {
--color: red;
color: var(--color) !important;
color: blue;
}</code></pre>
<details>
<summary>Show .example1</summary>
<div class="example1">I'm red!</div>
</details>
<p>Makes sense! By using <code>!important</code> we make the first color declaration more important than the second one.</p>
<p>Now, what about this? Which color does the text have now?</p>
<pre><code class="language-css">.example2 {
--color: red !important;
color: var(--color);
color: blue;
}</code></pre>
<details>
<summary>Show .example2</summary>
<div class="example2">I'm blue!</div>
</details>
<p>In order to understand that, we have to look at the spec. There it says:</p>
<blockquote>
<p>Custom properties can contain a trailing !important, but this is automatically removed from the property’s value by the CSS parser,…</p>
</blockquote>
<p>Aha! It's okay to use it, but it will be removed from the value by the parser, and since it's removed, the second color declaration overwrites the first one. </p>
<p>Why can we use it, if the CSS parser removes it anyway? Well, because the sentence continues:</p>
<blockquote>
<p>…and makes the custom property "important" in the CSS cascade.</p>
</blockquote>
<p>Custom properties are just like ordinary properties in CSS part of the cascade and they follow its rules. If you take the following example, which color does the text have?</p>
<pre><code class="language-css">.example3 {
--color: red;
--color: blue;
color: var(--color);
}</code></pre>
<details>
<summary>Show .example3</summary>
<div class="example3">I'm blue!</div>
</details>
<p>Why? Because declarations defined later in the document overwrite those defined earlier, if they have the same specificity. This applies to custom properties just like it does to ordinary properties.</p>
<p>We can change the specificity in CSS by using different selectors or by making a property <code>!important</code>.<br />
With that in mind, can you guess which color the text has in the following example?</p>
<pre><code class="language-css">.example4 {
--color: red !important;
--color: blue;
color: var(--color);
}</code></pre>
<details>
<summary>Show .example4</summary>
<div class="example4">I'm red!</div>
</details>
<p>I'm sure you got that one right! One last example, to conclude this topic. Which color does the text have?</p>
<pre><code class="language-css">.example5 {
--color: red !important;
--color: blue;
color: var(--color);
color: blue;
}</code></pre>
<details>
<summary>Show .example5</summary>
<div class="example5">I'm blue!</div>
</details>
<p>This post is based on a chapter in an <a href="https://dev.to/afif/what-no-one-told-you-about-css-variables-553o">article written bei Temani Afif</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+29%3A+%21important+custom+properties%E2%80%9D">blog@matuzo.at</a>.</p> Day 30: the hwb() color function2022-11-04T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day30
<p>Like the <a href="/blog/2022/100daysof-day23/">lab() color function</a>, <code>hwb()</code> is one of the more recent methods for defining colors in CSS. Just like <code>rgb()</code> and <code>hsl()</code> it uses colors from the sRGB color space. HWB, which stands for hue-whiteness-blackness, describes colors with a starting hue, then a degree of whiteness and blackness to mix into that base hue.</p><p>The function takes 3 space-separated values.</p>
<pre><code class="language-css">div {
background-color: hwb(234deg 30% 34%);
}</code></pre>
<h2>h - hue</h2>
<p>The first value defines the hue. It's an angle of the color circle given in <code>degs</code>, <code>rads</code>, <code>grads</code>, or <code>turns</code>. The value can also be a unitless number, which defaults to <code>deg</code>.</p>
<p>The color circle starts and ends with red (red=0deg=360deg), green is at 120deg and blue at 240deg.</p>
<pre><code class="language-css">div {
background-color: hwb(0 0% 0%);
}</code></pre>
<figure style="max-width: 710px">
<img alt="red color" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day30/047bdd0006-1698950566/hwb_1.jpg" width="710">
<figcaption> Screenshot of Stefan Judis' <a href="https://www.stefanjudis.com/blog/hwb-a-color-notation-for-humans/#%60hwb()%60-%E2%80%93-a-color-notation-%22easier-for-humans-to-work-with%22">“hwb() playground”</a></figcaption>
</figure>
<pre><code class="language-css">div {
background-color: hwb(206 0% 0%);
}</code></pre>
<img alt="blue color" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day30/56ee52b1fe-1698950566/hwb_2.jpg" width="710">
<h2>w – whiteness</h2>
<p>The second parameter specifies the amount of white to mix in, as a percentage from 0% (no whiteness) to 100% (full whiteness).</p>
<pre><code class="language-css">div {
background-color: hwb(206 68% 0%);
}</code></pre>
<img alt="light blue color" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day30/5fdb00f065-1698950566/hwb_3.jpg" width="710">
<h2>b - blackness</h2>
<p>The third parameter specifies the amount of black to mix in, as a percentage from 0% (no blackness) to 100% (full blackness).</p>
<pre><code class="language-css">div {
background-color: hwb(206 0% 42%);
}</code></pre>
<img alt="dark blue color" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day30/76139408c6-1698950566/hwb_4.jpg" width="710">
<h2>opacity</h2>
<p>You can also add a fourth parameter for the alpha value.</p>
<pre><code class="language-css">div {
background-color: hwb(206 0% 42% / 0.5);
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+30%3A+the+hwb%28%29+color+function%E2%80%9D">blog@matuzo.at</a>.</p> Day 31: logical border properties2022-11-07T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day31
<p>Just like for <code>margin</code> or <code>padding</code>, there are also logical property variations for <code>border</code> properties.</p><p>Originally there were 4 shorthand properties we could use for defining borders.</p>
<ul>
<li><code>border</code> - 1, 2, or 3 values (size, style, color)</li>
<li><code>border-width</code> - 1, 2, 3, or 4 size values for the different sides</li>
<li><code>border-style</code> - 1, 2, 3, or 4 style values for the different sides</li>
<li><code>border-color</code> - 1, 2, 3, or 4 color values for the different sides</li>
</ul>
<p>And we could do the same for each physical side (top, right, bottom, left).</p>
<ul>
<li><code>border-left</code> - 1, 2, or 3 values</li>
<li><code>border-left-width</code> - A size value</li>
<li><code>border-left-style</code> - A style value</li>
<li><code>border-left-color</code> - A color value</li>
</ul>
<p>Now there are a couple of more properties:</p>
<h2>border-block</h2>
<ul>
<li><code>border-block</code> - 1, 2, or 3 values (size, style, color)</li>
<li><code>border-block-width</code> - 1 or 2 size values (a single value for both sides or one for each)</li>
<li><code>border-block-style</code> - 1 or 2 style values</li>
<li><code>border-block-color</code> - 1 or 2 color values</li>
</ul>
<style>
.border-block-shorthand {
border-block: solid;
}
.border-block-shorthand2 {
border-block: 20px solid aqua;
}
.border-block-shorthand3 {
border-block-color: green;
border-block-width: 10px 20px;
border-block-style: dashed dotted;
}
.border-block-start {
border-block-start: 2em solid red;
}
.border-block-end {
border-block-end-color: blue;
border-block-end-width: 1rem;
border-block-end-style: dashed;
}
.border-inline-shorthand {
border-inline: solid;
}
.border-inline-shorthand2 {
border-inline: 20px solid aqua;
}
.border-inline-shorthand3 {
border-inline-color: green;
border-inline-width: 10px 20px;
border-inline-style: dashed dotted;
}
.border-inline-start {
border-inline-start: 2em solid red;
}
.border-inline-end {
border-inline-end-color: blue;
border-inline-end-width: 1rem;
border-inline-end-style: dashed;
}
.div {
padding: 1rem;
margin-bottom: 5rem;
background: #efefef;
}
</style>
<pre><code class="language-css">div {
border-block: solid;
}</code></pre>
<div class="div border-block-shorthand">
border-block
</div>
<pre><code class="language-css">div {
border-block: 20px solid aqua;
}</code></pre>
<div class="div border-block-shorthand2">
border-block
</div>
<pre><code class="language-css">div {
border-block-color: green;
border-block-width: 10px 20px;
border-block-style: dashed dotted;
}</code></pre>
<div class="div border-block-shorthand3">
border-block
</div>
<p>We can do the same for each individual side (block-start and block-end).</p>
<ul>
<li><code>border-block-start</code> - 1, 2, or 3 values</li>
<li><code>border-block-start-width</code> - A size value</li>
<li><code>border-block-start-style</code> - A style value</li>
<li><code>border-block-start-color</code> - A color value</li>
</ul>
<pre><code class="language-css">div {
border-block-start: 2em solid red;
}</code></pre>
<div class="div border-block-start">
border-block-start
</div>
<pre><code class="language-css">div {
border-block-end-color: blue;
border-block-end-width: 1rem;
border-block-end-style: dashed;
}</code></pre>
<div class="div border-block-end">
border-block-end
</div>
<h2>border-inline</h2>
<ul>
<li><code>border-inline</code> - 1, 2, or 3 values</li>
<li><code>border-inline-width</code> - 1 or 2 size values</li>
<li><code>border-inline-style</code> - 1 or 2 style values</li>
<li><code>border-inline-color</code> - 1 or 2 color values</li>
</ul>
<pre><code class="language-css">div {
border-inline: solid;
}</code></pre>
<div class="div border-inline-shorthand">
border-inline
</div>
<pre><code class="language-css">div {
border-inline: 20px solid aqua;
}</code></pre>
<div class="div border-inline-shorthand2">
border-inline
</div>
<pre><code class="language-css">div {
border-inline-color: green;
border-inline-width: 10px 20px;
border-inline-style: dashed dotted;
}</code></pre>
<div class="div border-inline-shorthand3">
border-inline
</div>
<p>We can do the same for each individual side (inline-start and inline-end).</p>
<ul>
<li><code>border-inline-start</code> - 1, 2, or 3 values</li>
<li><code>border-inline-start-width</code> - A size value</li>
<li><code>border-inline-start-style</code> - A style value</li>
<li><code>border-inline-start-color</code> - A color value</li>
</ul>
<pre><code class="language-css">div {
border-inline-start: 2em solid red;
}</code></pre>
<pre><code class="language-html"><div>
border-inline-start
</div></code></pre>
<div class="div border-inline-start">
border-inline-start
</div>
<pre><code class="language-html"><div dir="rtl">
border-inline-start
</div></code></pre>
<div class="div border-inline-start" dir="rtl">
border-inline-start
</div>
<pre><code class="language-css">div {
border-inline-end-color: blue;
border-inline-end-width: 1rem;
border-inline-end-style: dashed;
}</code></pre>
<div class="div border-inline-end">
border-inline-end
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+31%3A+logical+border+properties%E2%80%9D">blog@matuzo.at</a>.</p> Workshop: Deep Dive on Accessibility Testing2022-11-08T00:00:00+00:00https://www.matuzo.at/blog/2022/sm-a11y-testing
<p>I’ve teamed up with my friends at <a href="https://www.smashingmagazine.com/">Smashing Magazine</a> 😻 to share with you everything I know about web accessibility testing! In this <a href="https://smashingconf.com/online-workshops/workshops/manuel-matuzovic/">smashing workshop</a> we’ll talk about automatic and manual testing, screen reader basics, Single Page Applications, Dev Tools, and more.</p><!-- teaser -->
<p>Sounds interesting? Great! Here are some more details about the workshop:</p>
<video src="/images/workshop_promo.mp4" controls>
<track default kind="captions" srclang="en" src="/images/workshop_promo.vtt" label="English">
<track default kind="subtitles" srclang="de" src="/images/workshop_promo_de.vtt" label="Deutsch">
Sorry, your browser doesn't support embedded videos.
</video>
<h2>What you will learn in this workshop</h2>
<ul><li>Which <strong>testing tools</strong> are available and most commonly used.</li><li>How to <strong>assess the accessibility</strong> of a component or page.</li><li>The difference between <strong>automatic and manual testing</strong>.</li><li>How to use <strong>automatic accessibility testing</strong> tools and how to interpret the results.</li><li>How to use the <strong>keyboard to discover accessibility bugs</strong>.</li><li><strong>Screen reader basics</strong> and how to use them for accessibility testing, both on desktop and mobile devices.</li><li>How to test the accessibility of <strong>Single Page Applications</strong>.</li><li>Common <strong>pitfalls of Single Page Applications</strong> and how to avoid them.</li><li>How to <strong>integrate accessibility testing</strong> in your day-to-day development workflow.</li><li>Running <strong>tests on the command line</strong> and creating automated reports.</li><li><strong>Integrating accessibility testing in your build pipeline</strong>.</li><li>Where to find <strong>information and help on how to build complex components</strong>.</li></ul>
<h2>Time and schedule</h2>
<p>This workshop is split over <strong>five days</strong>. The workshop sessions will run on the following days:</p>
<ul><li>Mon, November 14, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 19:30 CET)</span></li><li>Tue, November 15, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 19:30 CET)</span></li><li>Mon, November 21, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Tue, November 22, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Mon, November 28, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li></ul>
<h2>Ticket</h2>
<p>If you use this <a href="https://ti.to/smashingmagazine/online-workshops-2022/discount/ihopeyouwillbejoiningmeinnovember">special link</a> you get a 20% discount on the original price.</p>
<p>If you have any questions about the workshop, feel free to get in touch <a href="manuel@matuzo.at">via mail</a>. I hope to see you soon!</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWorkshop%3A+Deep+Dive+on+Accessibility+Testing%E2%80%9D">blog@matuzo.at</a>.</p> Day 32: the clamp() function2022-11-08T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day32
<p>The <code>clamp()</code> function defines a minimum value, a preferred value, and a maximum value.</p><style>
.div {
border: 1em solid hwb(200 0% 0%);
padding: 1rem;
}
.minmax {
width: max(300px, min(90%, 700px));
}
.clamp {
width: clamp(300px, 90%, 700px);
}
.max {
width: max(300px, 90%);
}
.min {
width: min(90%, 700px);
}
</style>
<p>A quick recap of <a href="/blog/2022/100daysof-day4/">min()</a> and <a href="/blog/2022/100daysof-day5/">max()</a> before we talk about <code>clamp()</code>:</p>
<p>We can use the <code>min()</code> function to define a maximum value for a property. It's the maximum value we define because in the list of provided parameters, <code>min()</code> will always pick the smallest value. For example, <code>width: min(700px, 90%)</code> is always 700px or less, which means that the maximum width is 700px.</p>
<pre><code class="language-css">div {
width: min(90%, 700px);
}</code></pre>
<div class="sample">
<div class="div min">700px or less with no min-width</div>
</div>
<p>We can use the <code>max()</code> function to define a minimum value for a property. It's the minimum value we define because in the list of provided parameters, <code>max()</code> will always pick the largest value. For example, <code>width: max(300px, 90%)</code> is always 300px or more, which means that the minimum width is 300px.</p>
<pre><code class="language-css">div {
width: max(300px, 90%);
}</code></pre>
<div class="sample">
<div class="div max">300px or more with no explicit max-width</div>
</div>
<p>If we want to define a default value and both a minimum and maximum value, we could do this:</p>
<pre><code class="language-css">div {
width: max(300px, min(90%, 700px));
}</code></pre>
<div class="sample">
<div class="div minmax">90% with 300px min-width and 700px max-width</div>
</div>
<p>The <code>max()</code> function picks the largest value, either <code>300px</code> or the result of the <code>min()</code> function if it's larger than <code>300px</code>. This defines the minimum width. The <code>min()</code> function picks the lowest value, either <code>700px</code> or <code>90%</code> if it's less than <code>700px</code>. This the defines the maximum width with <code>90%</code> as the default value.<br />
Since nesting functions is super complicated and my brain still hurts from writing this paragraph, there's a handy alternative for this, <code>clamp()</code>.<br />
<code>clamp()</code> takes three parameters, a minimum value, a preferred value, and a maximum value.</p>
<pre><code class="language-css">div {
width: clamp(300px, 90%, 700px);
}</code></pre>
<div class="sample">
<div class="div clamp">90% with 300px min-width and 700px max-width</div>
</div>
<p>The <code>width</code> of the <code><div></code> is 90% with a minimum width of 300px and maximum width of 700px. It's basically a shorter way of writing:</p>
<pre><code class="language-css">div {
width: 90%;
min-width: 300px;
max-width: 700px;
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+32%3A+the+clamp%28%29+function%E2%80%9D">blog@matuzo.at</a>.</p> Day 33: Mathematical expressions in min(), max(), clamp()2022-11-09T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day33
<p>You can use full math expressions in the comparison functions <code>min()</code>, <code>max()</code>, and <code>clamp()</code>. There’s no need to nest a <code>calc()</code> function inside.</p><p>Writing…</p>
<pre><code class="language-css">div {
border: max(20px, calc(1vw + 10px)) solid;
}</code></pre>
<p>…is the same as writing…</p>
<pre><code class="language-css">div {
border: max(20px, 1vw + 10px) solid;
}</code></pre>
<p>You can also use custom properties.</p>
<pre><code class="language-css">.var {
--extra: 10px;
border-width: max(20px, 1vw + var(--extra));
}</code></pre>
<p>Complex expressions are also possible.</p>
<pre><code class="language-css">div {
width: clamp(50px * 4 * 1.5, (100% / 2) * 2, 400px * 2);
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+33%3A+Mathematical+expressions+in+min%28%29%2C+max%28%29%2C+clamp%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 34: :is() or :where()2022-11-10T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day34
<p>Thoughts on when it’s better to use <code>:is()</code> over <code>:where()</code> and vice versa.</p><p>The other day, I posted this code snippet on social media, asking people whether it was readable.</p>
<pre><code class="language-css">summary:where(:hover, :focus-visible)::after {
transform: rotate(180deg);
}</code></pre>
<p>Some people responded they prefer <code>:is()</code> over <code>:where()</code> because it’s shorter or less typing. Others said they liked <code>:where()</code> better because grammatically it made more sense to them or because it just sounded better. </p>
<p>These sound like valid reasons to pick one over the other, but they shouldn’t be (the main) reasons. As we already know, there is an important <a href="/blog/2022/100daysof-day14/">difference between :is() and :where()</a>. The specificity of <code>:where()</code> is always 0, while <code>:is()</code> takes on the specificity of the most specific selector in its arguments.</p>
<p>I thought that was clear to me, but a brief discussion following my initial question made me realize that I needed to put a bit more thinking into the topic. Kilian asked me whether I find myself defaulting to <code>:is()</code> or <code>:where()</code>, and I responded “:where() because I like to keep the specificity curve as flat as possible”. Then Šime responded “But <code>summary:hover</code> <em>should</em> have a higher specificity than <code>summary</code>. That’s a good thing.”, and he's right!</p>
<h2>The case for <code>:is()</code></h2>
<p>In the following example, I'm defining that the background color of the <code>summary</code> should be blue and change to red on hover and focus. </p>
<pre><code class="language-css">summary:hover,
summary:focus-visible {
background-color: red;
}
summary {
background-color: blue;
}</code></pre>
<p>You can see that the declaration for the default state of the element comes later in the document, but it doesn't overwrite the styles for the hover and focus states because its specificity is lower. I guess we can agree that it would make more sense to write it the other way around, but it's a good thing that it still works that way. The <code>:hover</code> and <code>:focus-visible</code> pseudo-classes make the selector more specific, because it's their job to define styles for a specific scenario.</p>
<style>
summary {
color: #fff;
padding: 0.3rem;
}
.default summary:hover,
.default summary:focus-visible {
background-color: red;
}
.default summary {
background-color: blue;
}
.where summary:where(:hover, :focus-visible) {
background-color: red;
}
.where summary {
background-color: blue;
}
.is summary:is(:hover, :focus-visible) {
background-color: red;
}
.is summary {
background-color: blue;
}
</style>
<div class="sample">
<details class="default">
<summary>Show more info</summary>
<p>Here's the info</p>
</details>
</div>
<p>Now, let’s do the same using <code>:where()</code>.</p>
<pre><code class="language-css">summary:where(:hover, :focus-visible) {
background-color: red;
}
summary {
background-color: blue;
}</code></pre>
<div class="sample">
<details class="where">
<summary>Show more info</summary>
<p>Here's the info</p>
</details>
</div>
<p>Now the background color is always blue because the specificity of the first and second selector is the same and the second selector, which comes later in the document, overwrites the styles of the first selector.</p>
<p><code>:where()</code> nulls the specificity of its selectors. While it’s a good practice to keep specificity low, sometimes we need higher specific to assure that certain rules will be applied.<br />
This is an example where we want to maintain the specificity of the selectors, and we can do that by using <code>:is()</code>. </p>
<pre><code class="language-css">summary:is(:hover, :focus-visible) {
background-color: red;
}
summary {
background-color: blue;
}</code></pre>
<div class="sample">
<details class="is">
<summary>Show more info</summary>
<p>Here's the info</p>
</details>
</div>
<h2>The case for <code>:where()</code></h2>
<style>
.default input[type="text"],
.default input[type="email"],
.default input[type="url"] {
background-color: #efefef;
border: 2px solid;
}
.default [aria-invalid="true"] {
border-color: red;
}
.where input:where(
[type="text"],
[type="email"],
[type="url"]) {
background-color: #efefef;
border: 2px solid;
}
.where [aria-invalid="true"] {
border-color: red;
}
</style>
<p>Sometimes we need selectors with higher specificity to improve readability and comprehensibility, or to limit the elements styles will be applied to.</p>
<p>For example, in your base styles you might have something like this.</p>
<pre><code class="language-css">input[type="text"],
input[type="email"],
input[type="url"] {
background-color: #efefef;
border: 2px solid;
}</code></pre>
<pre><code class="language-html"><label for="name">name</label>
<input type="text" id="name"></code></pre>
<div class="sample">
<div class="default">
<label for="name">Name</label><br>
<input type="text" id="name">
</div>
</div>
<p>Now, let's say you want to change the border color when the data provided by the user is invalid.</p>
<pre><code class="language-css">[aria-invalid="true"] {
border-color: red;
}</code></pre>
<pre><code class="language-html"><label for="name">name</label>
<input type="text" id="name" aria-invalid="true"></code></pre>
<div class="sample">
<div class="default">
<label for="name1">Name</label><br>
<input type="text" id="name1" aria-invalid="true">
</div>
</div>
<p>The color of the border doesn't change because <code>input[type="text"]</code> is more specific than <code>[aria-invalid="true"]</code>. We don't need <code>input[type="text"]</code> to be that specific, but the combination of tag and attribute selector makes it easier for us to understand what's going on and it rules out other elements which potentially might have a <code>type</code> attribute with these values.</p>
<p>We can use <code>:where()</code> to lower the specificity of the selector.</p>
<pre><code class="language-css">input:where(
[type="text"],
[type="email"],
[type="url"]) {
background-color: #efefef;
border: 2px solid;
}
[aria-invalid="true"] {
border-color: red;
}</code></pre>
<div class="sample">
<div class="where">
<label for="name2">Name</label><br>
<input type="text" id="name2" aria-invalid="true" placeholder="">
</div>
</div>
<p>Now <code>[aria-invalid="true"]</code> (specificity of a class/attribute) overrules <code>input:where(…)</code> (specificity of an element).</p>
<p>I will take <a href="https://kilianvalkhof.com/">Kilians</a> and <a href="https://webplatform.news/">Šimes</a> advice and use <code>:is()</code> by default from now on because most of the time we need our complex selector to have a certain specificity. Only in exceptional cases, it's better to use <code>:where()</code> to decrease the specificity.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+34%3A+%3Ais%28%29+or+%3Awhere%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 35: forgiving selectors2022-11-11T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day35
<p>There's a difference between listing selectors in <code>:where()</code>, <code>:is()</code>, and <code>:has()</code> and listing them in a regular selector list.</p><p>Let's say you have a button with the class <code>.button</code> and you apply the following styles.</p>
<pre><code class="language-css">.button:hover {
background-color: hwb(100 0% 20%);
}</code></pre>
<pre><code class="language-html"><button class="button">I'm a button</hover></code></pre>
<style>
.button1:hover {
background-color: hwb(100 0% 20%);
}
.button2:hover,
.button2:focus,
#btn {
background-color: hwb(100 0% 20%);
}
.button3:hover,
$btn {
background-color: hwb(200 20% 0%);
}
.button4:hover,
.button4:focus,
.button4:touch {
background-color: hwb(300 20% 0%);
}
.button5:where(:hover, :focus, $btn) {
background-color: hwb(90 20% 20%);
}
.button6:where(:hover, :focus, :touch) {
background-color: hwb(52 10% 20%);
}
</style>
<div class="sample">
<button class="button button1">I'm a button</hover>
</div>
<p>Nothing special, the color just changes on hover. If you add more and different selectors to this rule, it still works.</p>
<pre><code class="language-css">.button:hover,
.button:focus,
#btn {
background-color: hwb(100 0% 20%);
}</code></pre>
<div class="sample">
<button class="button button2">I'm a button</hover>
</div>
<p>Here's were it gets interesting: If one of the selectors in your list of selectors is invalid, the whole rule becomes invalid and declarations apply to none of the selectors.</p>
<pre><code class="language-css">.button:hover,
.button:focus,
$btn {
background-color: hwb(200 20% 0%);
}</code></pre>
<div class="sample">
<button class="button button3">I'm a button</hover>
</div>
<p>Even using a pseudo-class that doesn't exist or that isn't supported by the browser invalidates the whole rule.</p>
<pre><code class="language-css">.button:hover,
.button:focus,
.button:touch {
background-color: hwb(200 20% 0%);
}</code></pre>
<div class="sample">
<button class="button button4">I'm a button</hover>
</div>
<p>So, a downside to using a selector list is that a single invalid or unsupported selector in the list of selectors invalidates the entire rule.</p>
<p>That's different when you're using <a href="/blog/2022/100daysof-day6/">:has()</a>, <a href="/blog/2022/100daysof-day13/">:where() or :is()</a> because they're so-called “forgiving selectors”. They just ignore the invalid selectors and apply the rules to the others.</p>
<pre><code class="language-css">button:where(:hover, :focus, $btn) {
background-color: hwb(90 20% 20%);
}</code></pre>
<div class="sample">
<button class="button button5">I'm a button</hover>
</div>
<pre><code class="language-css">button:where(:hover, :focus, :touch) {
background-color: hwb(52 10% 20%);
}</code></pre>
<div class="sample">
<button class="button button6">I'm a button</hover>
</div>
<p>This means that another benefit of using <code>:is()</code> or <code>:where()</code>, besides less lines of code and more control over specificity, is that you can use selectors that don't work in every browser in a list of selectors without having to worry that they invalidate the whole rule.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+35%3A+forgiving+selectors%E2%80%9D">blog@matuzo.at</a>.</p> Day 36: :has() and pseudo-elements2022-11-14T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day36
<p>We already know that we can select an element based on the <a href="/blog/2022/100daysof-day6/">presence of a certain child element</a> (in Chrome/Edge 105+ and Safari 15.4+), but there are limitations.</p><pre><code class="language-html"><p>
<strong>World</strong>!
</p></code></pre>
<pre><code class="language-css">p:has(strong) {
background-color: aqua;
}</code></pre>
<style>
.p:has(strong) {
background-color: aqua;
}
.p2::before,
.p3::before{
content: "Hello";
}
.p3:has(:hover) {
background-color: salmon;
}
</style>
<div class="sample">
<p class="p">
<strong>World</strong>!
</p>
</div>
<p>This works well with actual elements, but it doesn't work with pseudo-elements.</p>
<pre><code class="language-css">p::before {
content: "Hello";
}
p:has(::before) {
background-color: salmon;
}</code></pre>
<div class="sample">
<p class="p2">
<strong>World</strong>!
</p>
</div>
<p>According to the spec, that's because <q>Pseudo-elements are generally excluded from :has() because many of them exist conditionally, based on the styling of their ancestors, so allowing these to be queried by :has() would introduce cycles.</q>.</p>
<p>For the sake of completeness, of course <code>:has()</code> works with pseudo-classes.</p>
<pre><code class="language-css">p:has(:hover) {
background-color: salmon;
}</code></pre>
<div class="sample">
<p class="p3">
<strong>World</strong>!
</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+36%3A+%3Ahas%28%29+and+pseudo-elements%E2%80%9D">blog@matuzo.at</a>.</p> Day 37: cascade layers2022-11-15T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day37
<p>Cascade layers introduce a new way of managing specificity in CSS.</p><p>Let’s say we’re using a combination of a tag and an attribute selector for styling e-mail input fields. This declaration is part of our base stylesheet and comes early in the stylesheet. Later in the document, we want to use a class to overwrite parts of the base styling:</p>
<pre><code class="language-css">input[type="text"],
input[type="email"] {
border-color: hwb(0 0% 0%);
border-style: solid;
border-width: 3px;
}
.form-item {
border-color: hwb(120 0% 40%);
}</code></pre>
<style>
.demo label {
display: block;
}
.default input[type="text"],
.default input[type="email"] {
border-color: hwb(0 0% 0%);
border-style: solid;
border-width: 3px;
}
.default .form-item {
border-color: hwb(120 0% 40%);
}
@layer base {
.cascade input[type="text"],
.cascade input[type="email"] {
border-color: hwb(0 0% 0%);
border-style: solid;
border-width: 3px;
}
}
@layer component {
.cascade .form-item {
border-color: hwb(120 0% 40%);
}
}
</style>
<div data-sample="demo">
<div class="demo default">
<label for="email">E-mail</label>
<input id="email" type="email" class="form-item">
</div>
</div>
<p>This won’t work because <code>input[type="email"]</code> is more specific than <code>.form-item</code>.<br />
There are several ways to work around that.</p>
<h2>Using !important</h2>
<p>We could use <code>!important</code>, but we all know that this probably isn’t the best idea in the long term.</p>
<pre><code class="language-css">input[type="text"],
input[type="email"] {
border-color:hwb(0 0% 0%);
border-style: solid;
border-width: 3px;
}
.form-item {
border-color: hwb(120 0% 40%) !important;
}</code></pre>
<h2>Increasing selector specificity</h2>
<p>We could increase the specificity of the second selector.</p>
<pre><code class="language-css">input[type="text"],
input[type="email"] {
border-color:hwb(0 0% 0%);
border-style: solid;
border-width: 3px;
}
.form-item.form-item {
border-color: hwb(120 0% 40%);
}</code></pre>
<p>This works, but isn't the most beautiful solution either.</p>
<h2>Decreasing selector specificity</h2>
<p>We could decrease the specificity of the first selector.</p>
<pre><code class="language-css">input:where([type="text"], [type="email"]) {
border-color:hwb(0 0% 0%);
border-style: solid;
border-width: 3px;
}
.form-item {
border-color: hwb(120 0% 40%);
}</code></pre>
<p>Using <code>:where()</code> to decrease specificity is a nice solution, and it works great if you only have a handful of selectors, but if you have a group of different selectors that you want on the same level in terms of specificity, there’s a more convenient way to do that.</p>
<h2>Cascade layers</h2>
<p>Cascade layers give us more control over the cascade. Using the <code>@layer</code> at-rule we can establish our own layers of the cascade. The rules of specificity we already know still apply within each layer, but there are no conflicts between rules in different layers because rules in a layer with higher priority always<span aria-describedby="not-always">*</span> win over rules in a layer with lower priority no matter how specific selectors are.</p>
<p id="not-always">*Okay, not always, but we'll talk about that in another post.</span>
<pre><code class="language-css">@layer base {
input[type="text"],
input[type="email"] {
border-color:hwb(0 0% 0%);
border-style: solid;
border-width: 3px;
}
}
@layer component {
.form-item {
border-color: hwb(120 0% 40%);
}
}</code></pre>
<div data-sample="demo">
<div class="demo cascade">
<label for="email2">E-mail</label>
<input id="email2" type="email" class="form-item">
</div>
</div>
<p>The specificity of <code>.form-item</code> is still lower than the specificity of <code>input[type="email"]</code>, but <code>.form-item</code> is in the component layer, which comes later in the document and thus overwrites styles in the base layer.</p>
<p>There’s a lot more to say about cascade layers, but I’ll save that for later. :)</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+37%3A+cascade+layers%E2%80%9D">blog@matuzo.at</a>.</p> Day 38: vh, svh, lvh, and dvh2022-11-16T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day38
<p>Using the viewport unit <code>vh</code> in desktop browsers is usually straight-forward, <code>100vh</code> matches the height of the viewport. On mobile that's different because the viewport height changes depending on whether or not certain user interface elements are visible, <code>100vh</code> doesn't always match the height of the viewport.</p><p>On mobile we have the small viewport and the large viewport. According to the spec, the small viewport is <q>the viewport sized assuming any UA [User Agent/Browser] interfaces [for example the address bar] that are dynamically expanded and retracted to be expanded</q>.</p>
<img alt="a red border marking the visible area of the viewport. Below address bar and other controls."" loading="auto" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day38/ae8fdfc6c2-1698950578/100days-38-1.jpg" width="300">
<p>The large viewport is <q>the viewport sized assuming any UA interfaces that are dynamically expanded and retracted to be retracted</q>.</p>
<img alt="a red border marking the visible area of the viewport, which fills almost the whole screen, no UA interface elements visible."" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day38/e4e7f4ff65-1698950578/100days-38-2.png" width="300">
<p>The problem with <code>100vh</code> on mobile is that it doesn’t respect whether user interface elements are expanded or not. It usually always matches the large viewport. CSS introduces new viewport units to address that issue.</p>
<p>You can use <code>svh</code> for the small viewport and <code>lvh</code> for the large viewport.</p>
<pre><code class="language-css">div {
height: 100svh;
/* See result in the first screenshot */
}</code></pre>
<pre><code class="language-css">div {
height: 100lvh;
/* See result in the second screenshot */
}</code></pre>
<p>That's great, but depending on if and how the user interacts with the page, they might sometimes see the large viewport and sometimes the small viewport. Setting the height to either unit probably isn't what you want because the height changes dynamically. Instead, you want to use the third new unit <code>dvh</code>, which dynamically either matches <code>svh</code> or <code>lvh</code>.</p>
<pre><code class="language-css">div {
height: 100dvh;
}</code></pre>
<img alt="comparisson of vh, lvh, svh, and dvh on a small and large viewport" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day38/c0b8968684-1698950578/100days-38-3.jpg" width="700"><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+38%3A+vh%2C+svh%2C+lvh%2C+and+dvh%E2%80%9D">blog@matuzo.at</a>.</p> Day 39: comma-separated functional color notations2022-11-17T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day39
<p>On day 11 I've introduced you to <a href="/blog/2022/100daysof-day11/">space-separated functional color notations</a>. Early color functions like <code>rgb()</code> and <code>hsl()</code> support both the old comma-separated and the new space-separated syntax.</p><pre><code class="language-css">/* ✅ works */
div {
background-color: rgb(255, 210, 210);
color: hsl(150, 76%, 20%);
}
/* ✅ works */
div {
background-color: rgb(255 210 210);
color: hsl(150 76% 20%);
}</code></pre>
<p>On the other hand, new color functions like <code>lab()</code>, <code>hwb()</code>, <code>lch()</code>, or <code>oklch()</code> only support the space-separated syntax.</p>
<pre><code class="language-css">/* ✅ works */
div {
background-color: hwb(0deg 82% 0%);
color: lab(33% -31 16);
}
/* ❌ doesn't work */
div {
background-color: hwb(0deg, 82%, 0%);
color: lab(33%, -31, 16);
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+39%3A+comma-separated+functional+color+notations%E2%80%9D">blog@matuzo.at</a>.</p> Day 40: unlayered styles2022-11-18T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day40
<p>On day 37 we learned that we can get <a href="/blog/2022/100daysof-day37/">more control over specificity</a> by creating layers. That first, simple example is pretty straightforward, but what happens if we mix layered and unlayered styles?</p><p>Let's start nice and simple with a single layer. The border of this quote is red.</p>
<pre><code class="language-html"><blockquote>
There's only one Return, okay, and it ain't of the king...
</blockquote></code></pre>
<pre><code class="language-css">@layer base {
blockquote {
border: 4px solid red;
padding: 1rem;
}
}</code></pre>
<style>
.four {
border-color: hotpink;
}
@layer base {
blockquote {
border: 4px solid red;
padding: 1rem;
}
}
@layer component {
.two {
border-color: green;
}
}
.three {
border-color: hotpink;
}
</style>
<div data-sample="demo">
<blockquote>
There's only one Return, okay, and it ain't of the king...
</blockquote>
</div>
<p>If we add another layer, the border color turns green because a layer defined later in the document has precedence over a layer defined earlier.</p>
<pre><code class="language-css">@layer base {
blockquote {
border: 4px solid red;
padding: 1rem;
}
}
@layer component {
blockquote {
border-color: green;
}
}</code></pre>
<div data-sample="demo">
<blockquote class="two">
There's only one Return, okay, and it ain't of the king...
</blockquote>
</div>
<p>If we add unlayered styles after the second layer, the color turns hotpink. Not because the declaration comes later in the document, but because unlayered styles have the highest priority and always<span aria-describedby="not-always">*</span> overwrite layered styles.</p>
<p id="not-always">*Okay, not always, but we'll talk about that in another post.</span>
<pre><code class="language-css">@layer base {
blockquote {
border: 4px solid red;
padding: 1rem;
}
}
@layer component {
blockquote {
border-color: green;
}
}
blockquote {
border-color: hotpink;
}</code></pre>
<div data-sample="demo">
<blockquote class="three">
There's only one Return, okay, and it ain't of the king...
</blockquote>
</div>
<p>This means that the color will be hotpink even if the unlayered styles come before the layered styles.</p>
<pre><code class="language-css">blockquote {
border-color: hotpink;
}
@layer base {
blockquote {
border: 4px solid red;
padding: 1rem;
}
}
@layer component {
blockquote {
border-color: green;
}
}</code></pre>
<div data-sample="demo">
<blockquote class="four">
There's only one Return, okay, and it ain't of the king...
</blockquote>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+40%3A+unlayered+styles%E2%80%9D">blog@matuzo.at</a>.</p> Day 41: custom properties and url()s2022-11-21T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day41
<p>Let’s say you want to swap the background image of an element based on a certain condition, like whether it’s pressed, using custom properties.</p><pre><code class="language-css">button {
--background-image: "/not-pressed.svg";
background: url(var(--background-image));
}
button[aria-pressed="true"] {
--background-image: "/pressed.svg";
}</code></pre>
<p>This looks fine, but it doesn't work because <code>var(--background-image)</code> contains invalid characters. The reason the argument is invalid is that <code>url()</code> works both with quotes <code>url("image.svg")</code> or <code>url('image.svg')</code> and without quotes <code>url(image.svg)</code>. By passing a value without quotes you're not passing a string to a CSS function, but you're creating an <a href="https://www.w3.org/TR/css-syntax-3/#url-token-diagram">url-token</a> and this token expects a certain format that requires characters like ( to be escaped.<br />
That's a very abbreviated explanation. For details, please read <a href="https://www.stefanjudis.com/today-i-learned/custom-properties-dont-work-with-the-url-css-function/">“Why custom properties don't work with the url() CSS function”</a> by the amazing Stefan Judis.</p>
<p>To work around that issue, you have to move the <code>url()</code> function into the value of the custom property.</p>
<pre><code class="language-css">button {
--background-image: url("/not-pressed.svg");
background: var(--background-image);
}
button[aria-pressed="true"] {
--background-image: url("/pressed.svg");
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+41%3A+custom+properties+and+url%28%29s%E2%80%9D">blog@matuzo.at</a>.</p> Day 42: aspect-ratio2022-11-22T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day42
<p>Yes, I know, <code>aspect-ratio</code> is not the hottest shit, but Safari only starting supporting it in version 15 and there’s a lot I didn’t know about the property. That’s reason enough for me to write about it. :)</p><h2>Defining ratios</h2>
<p>You can use the <code>aspect-ratio</code> property to define the preferred aspect ratio for a box by defining a preferred width and height.</p>
<p>The maximum width of the following element is <code>400px</code>, and due to the defined ratio of <code>16 / 9</code> the height equals <code>225px</code> (400 / 16 * 9) = 225). If the available width gets below 400, the height scales accordingly.</p>
<pre><code class="language-css">div {
aspect-ratio: 16 / 9; /* aspect-ratio: width / height */
max-width: 400px;
}</code></pre>
<style>
.ratio {
border: 3px solid;
max-width: 400px;
}
.ratio1 {
aspect-ratio: 16 / 9;
}
.ratio2 {
aspect-ratio: 400 / 225;
}
.ratio3 {
aspect-ratio: 1 / 1;
}
.ratio4 {
aspect-ratio: 1;
}
.ratio5 {
aspect-ratio: 4;
}
.ratio6 {
aspect-ratio: 16 / 9;
width: 250px;
height: 250px;
}
.ratio7 {
aspect-ratio: 4;
max-width: 400px;
overflow: auto;
}
</style>
<div class="ratio ratio1"></div>
<p>Instead of writing <code>aspect-ratio: 16 / 9</code> we can also write <code>aspect-ratio: 400 / 225</code>.</p>
<pre><code class="language-css">div {
aspect-ratio: 400 / 225;
max-width: 400px;
}</code></pre>
<div class="ratio ratio2"></div>
<h2>Squares</h2>
<p>If you want to create a square, you need a ratio of <code>1:1</code>, or <code>42:42</code>, or <code>742617000027:742617000027</code>;</p>
<pre><code class="language-css">div {
aspect-ratio: 1 / 1;
max-width: 400px;
}</code></pre>
<div class="ratio ratio3"></div>
<p>That's the same as writing <code>aspect-ratio: 1</code> because if you omit the second value, the value for the height is <code>1</code> by default.</p>
<pre><code class="language-css">div {
aspect-ratio: 1;
max-width: 400px;
}</code></pre>
<div class="ratio ratio4"></div>
<h2>Single value ratios</h2>
<p>If <code>aspect-ratio: 1</code> equals <code>aspect-ratio: 1 / 1</code>, this means that <code>aspect-ratio: 4</code> equals <code>aspect-ratio: 4 / 1</code>.</p>
<pre><code class="language-css">div {
aspect-ratio: 4;
max-width: 400px;
}</code></pre>
<div class="ratio ratio5"></div>
<h2>width, height, and aspect-ratio</h2>
<p><code>aspect-ratio</code> has no effect if you set the value of both the <code>width</code> and <code>height</code> to something other than <code>auto</code>.</p>
<pre><code class="language-css">div {
aspect-ratio: 16 / 9;
width: 250px;
height: 250px;
}</code></pre>
<div class="ratio ratio6"></div>
<h2>preferred, not enforced ratio</h2>
<p>If the content of your box is larger than the preferred height, content won't be cut-off but the box grows with its content, ignoring the ratio.</p>
<pre><code class="language-css">div {
aspect-ratio: 4;
max-width: 400px;
}</code></pre>
<div class="ratio ratio5">
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
</ul>
</div>
<p>You can change the behaviour by defining a different value for the <code>overflow</code> property.</p>
<pre><code class="language-css">div {
aspect-ratio: 4;
max-width: 400px;
overflow: auto;
}</code></pre>
<div class="ratio ratio7">
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
</ul>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+42%3A+aspect-ratio%E2%80%9D">blog@matuzo.at</a>.</p> Day 43: grouping layers2022-11-23T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day43
<p>Cascade layers can be grouped by nesting layer rules.</p><p>If you work on a large style sheet, you might want to create cascade layers to group different types of declarations. In order to give your layers even more structure and control, you can also group declarations within layers.</p>
<p>Consider the following example. We have a layer for reset styles, base styles, components, and theming.</p>
<pre><code class="language-css">@layer reset {
body {
margin: 0;
}
}
@layer base {
body {
font-size: 1.6rem;
}
}
@layer components {
p {
border: 1px solid;
}
}
@layer theme {
p {
border-color: red;
}
}</code></pre>
<p>There's nothing wrong with that, but it might make sense to group similar layers. For example, you could group reset and base styles and component and theme styles.</p>
<pre><code class="language-css">@layer base {
@layer reset {
body {
margin: 0;
}
}
@layer defaults {
body {
font-size: 1.6rem;
}
}
}
@layer components {
@layer structure {
p {
border: 1px solid;
}
}
@layer theme {
p {
border-color: red;
}
}
}</code></pre>
<p>The same rules in terms or prioritization that apply to root layers also apply to nested layers. This adds more complexity to your style sheets, but it also gives you fine-grained control over specificity.</p>
<p>Nesting layer may seem to be overkill, and it probably is for many sites, but it will make more sense once we talk about ordering layers.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+43%3A+grouping+layers%E2%80%9D">blog@matuzo.at</a>.</p> Day 44: logical floating and clearing2022-11-24T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day44
<p>Thanks to Flexbox and CSS Grid no one seems to talk about <code>float</code> and <code>clear</code> anymore,…except for me now because there's news.</p><p>Floating and clearing has lost its importance for layout, but they are still useful properties. Just like for <code>margin</code> or <code>border</code>, you can now use logical properties for floating and clearing.</p>
<pre><code class="language-css">img {
float: inline-start; /* or inline-end */
}
p {
clear: inline-start; /* or inline-end */
}</code></pre>
<style>
[dir] {
margin-block-end: 2rem;
}
[dir] img {
float: inline-start;
margin-inline-end: 1rem;
}
[dir] p:nth-of-type(3) {
clear: inline-start;
}
</style>
<div dir="ltr" class="sample" data-sample="ltr demo">
<img alt="Gus Polinski, the Polka King of the Midwest talking to a desperate mother." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day44/1c477d7b04-1698950567/gus.jpg" width="350">
<p>Gus Polinski, the "Polka King of the Midwest," was the clarinet player and lead musician of a polka band, the Kenosha Kickers.</p>
<p>Gus first appears in the Scranton Airport when Kate is unable to fly to Chicago on Christmas Eve because all flights are fully booked. Upon hearing about her dilemma, Gus offers her a ride, which Kate gladly accepts.</p>
<p>Source: <a href="https://homealone.fandom.com/wiki/Gus_Polinski">Gus_Polinski</a></p>
</div>
<div dir="rtl" class="sample" data-sample="rtl demo">
<img alt="Gus Polinski, the Polka King of the Midwest talking to a desperate mother." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day44/1c477d7b04-1698950567/gus.jpg" width="350">
<p>Gus Polinski, the "Polka King of the Midwest," was the clarinet player and lead musician of a polka band, the Kenosha Kickers.</p>
<p>Gus first appears in the Scranton Airport when Kate is unable to fly to Chicago on Christmas Eve because all flights are fully booked. Upon hearing about her dilemma, Gus offers her a ride, which Kate gladly accepts.</p>
<p>Source: <a href="https://homealone.fandom.com/wiki/Gus_Polinski">Gus_Polinski</a></p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+44%3A+logical+floating+and+clearing%E2%80%9D">blog@matuzo.at</a>.</p> Day 45: the specificity of ::slotted() content2022-11-25T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day45
<p>When you pass an element to a web component through a <code><slot></code>, you can select that element using the <code>::slotted()</code> pseudo-element and apply additional styles.</p><p>Let's take the following component. There's a paragraph in the shadow DOM and another paragraph coming from the light DOM, passed through a <code><slot></code>, and there's a global style turning the background color of paragraphs aqua.</p>
<pre><code class="language-css">p {
background-color: aqua;
}</code></pre>
<pre><code class="language-js">class SlotComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const content = document.createElement('div')
content.innerHTML = `
<p>not slotted</p>
<slot></slot>
`
this.shadowRoot.appendChild(content)
}
}
customElements.define('slot-component', SlotComponent);</code></pre>
<pre><code class="language-html"><slot-component>
<p>slotted</p>
</slot-component></code></pre>
<style>
.sample p {
background-color: aqua;
}
</style>
<script>
class SlotComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const content = document.createElement('div')
content.innerHTML = `
<p>not slotted</p>
<slot></slot>
`
this.shadowRoot.appendChild(content)
}
}
customElements.define('slot-component', SlotComponent);
class SlotComponent2 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const styles = document.createElement('style')
styles.innerHTML = `
p {
background-color: salmon;
}
`
this.shadowRoot.appendChild(styles)
const content = document.createElement('div')
content.innerHTML = `
<p>not slotted</p>
<slot></slot>
`
this.shadowRoot.appendChild(content)
}
}
customElements.define('slot-component2', SlotComponent2);
class SlotComponent3 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const styles = document.createElement('style')
styles.innerHTML = `
p {
background-color: salmon;
}
::slotted(p) {
background-color: red;
}
`
this.shadowRoot.appendChild(styles)
const content = document.createElement('div')
content.innerHTML = `
<p>not slotted</p>
<slot></slot>
`
this.shadowRoot.appendChild(content)
}
}
customElements.define('slot-component3', SlotComponent3);
class SlotComponent4 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const styles = document.createElement('style')
styles.innerHTML = `
p {
background-color: salmon;
}
::slotted(p) {
background-color: red !important;
}
`
this.shadowRoot.appendChild(styles)
const content = document.createElement('div')
content.innerHTML = `
<p>not slotted</p>
<slot></slot>
`
this.shadowRoot.appendChild(content)
}
}
customElements.define('slot-component4', SlotComponent4);
</script>
<div class="sample">
<slot-component>
<p>slotted</p>
</slot-component>
</div>
<p>The global styles only apply to the slotted paragraph. We've already learned why in <a href="/blog/2022/100daysof-day10/">“Day 10: global styles and web components”</a>.</p>
<p>If you add styles to the shadow DOM, you can see how these styles only apply to the paragraph inside the shadow DOM, but not to the slotted paragraph.</p>
<pre><code class="language-js">const styles = document.createElement('style')
styles.innerHTML = `
p {
background-color: salmon;
}
`
this.shadowRoot.appendChild(styles)</code></pre>
<div class="sample">
<slot-component2>
<p>slotted</p>
</slot-component2>
</div>
<p>If you want to style slotted content from within the component, you can use the <code>::slotted()</code> pseudo-element.</p>
<pre><code class="language-js">const styles = document.createElement('style')
styles.innerHTML = `
p {
background-color: salmon;
}
::slotted(p) {
background-color: red;
}
`
this.shadowRoot.appendChild(styles)</code></pre>
<div class="sample">
<slot-component3>
<p>slotted</p>
</slot-component3>
</div>
<p>As you can see, the background color didn't change. That's because by slotting content you're not moving it from the light DOM to the shadow DOM. The nodes physically stay where they are, they're just reflected inside the web component. This also means that global document styles still apply. By using <code>::slotted()</code> we can add additional styles, but by default these styles have lower specificity than global document styles.<br />
That changes if we add <code>!important</code> to the mix.</p>
<pre><code class="language-js">const styles = document.createElement('style')
styles.innerHTML = `
p {
background-color: salmon;
}
::slotted(p) {
background-color: red !important;
}
`
this.shadowRoot.appendChild(styles)</code></pre>
<div class="sample">
<slot-component4>
<p>slotted</p>
</slot-component4>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+45%3A+the+specificity+of+%3A%3Aslotted%28%29+content%E2%80%9D">blog@matuzo.at</a>.</p> Day 46: ordering layers2022-11-28T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day46
<p>By default, cascade layers are stacked in the order they are defined, but you don’t have to rely on it. You can determine the order in one place.</p><p>In the following example, the border color of the paragraph is first red, then blue, then rebeccapurple, and finally green. </p>
<pre><code class="language-css">@layer base {
p {
border: 10px solid red;
}
}
@layer framework {
p {
border-color: blue;
}
}
@layer components {
p {
border-color: rebeccapurple;
}
}
@layer theme {
p {
border-color: green;
}
}</code></pre>
<style>
@layer base {
.demo1 p {
border: 10px solid red;
}
}
@layer framework {
.demo1 p {
border-color: blue;
}
}
@layer components {
.demo1 p {
border-color: rebeccapurple;
}
}
@layer theme {
.demo1 p {
border-color: green;
}
}
.demo3 p {
border-color: hotpink;
}
@layer base1, components1, theme1, framework1;
@layer base1 {
.demo3 p,
.demo2 p {
border: 10px solid red;
}
}
@layer framework1 {
.demo3 p,
.demo2 p {
border-color: blue;
}
}
@layer components1 {
.demo3 p,
.demo2 p {
border-color: rebeccapurple;
}
}
@layer theme1 {
.demo3 p,
.demo2 p {
border-color: green;
}
}
</style>
<div data-sample="demo" class="sample demo1">
<p>
Bartender, I got me a bet for you. I'm gonna bet you $300 that I can piss into that glass over there and not spill a single, solitary drop.
</p>
</div>
<p>You can change that order by defining layers first in a comma-separated list, starting with <code>@layer</code>.</p>
<pre><code class="language-css">@layer base, components, theme, framework;
@layer base {
p {
border: 10px solid red;
}
}
@layer framework {
p {
border-color: blue;
}
}
@layer components {
p {
border-color: rebeccapurple;
}
}
@layer theme {
p {
border-color: green;
}
}</code></pre>
<div data-sample="demo" class="sample demo2">
<p>
Bartender, I got me a bet for you. I'm gonna bet you $300 that I can piss into that glass over there and not spill a single, solitary drop.
</p>
</div>
<p>The order of appearance of the <code>@layer</code> blocks doesn't matter any more, the order in the <code>@layer</code> list does. The border color of the paragraph is now first red, then rebeccapurple, then green, and finally blue.</p>
<p>Oh, and of course, if you add <a href="/blog/2022/100daysof-day40/">unlayered styles</a>, those still win.</p>
<pre><code class="language-css">p {
border-color: hotpink;
}
@layer base, components, theme, framework;
@layer base {
p {
border: 10px solid red;
}
}
@layer framework {
p {
border-color: blue;
}
}
@layer components {
p {
border-color: rebeccapurple;
}
}
@layer theme {
p {
border-color: green;
}
}</code></pre>
<div data-sample="demo" class="sample demo3">
<p>
Bartender, I got me a bet for you. I'm gonna bet you $300 that I can piss into that glass over there and not spill a single, solitary drop.
</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+46%3A+ordering+layers%E2%80%9D">blog@matuzo.at</a>.</p> Day 47: the overscroll-behavior property2022-11-29T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day47
<p>You can use the <code>overscroll-behavior</code> property to disable scroll-chaining.</p><p>If you scroll the inner box in the following example to the end and you keep scrolling, the outer box starts scrolling, too, and finally the whole page.</p>
<style>
.demo.outer,
.demo .inner {
padding: 1rem;
border: 10px solid;
overflow: auto;
font-size: 1rem;
}
.demo.outer {
border-color: brown;
max-width: 40rem;
max-height: 400px;
}
.demo .inner {
border-color: hotpink;
max-width: 50%;
max-height: 200px;
float: left;
margin: 0 1rem 1rem 0;
}
.demo2 .inner {
overscroll-behavior: none;
}
</style>
<div class="sample" data-sample="demo">
<div class="demo outer">
<div class="inner">
And suckers be thinkin' that they can fake this<br>
But I'ma drop it at a higher level<br>
'Cause I'm inclined to stoop down, hand out some beatdowns<br>
Could run a train on punk fools that think they run the game<br>
But I learned to burn that bridge and delete<br>
Those who compete at a level that's obsolete<br>
Instead, I warm my hands upon the flames of the flag<br>
To recall the downfall and the businesses that burnt us all<br>
See through the news and the views that twist reality<br>
Enough, I call the bluff, fuck Manifest Destiny<br>
Landlords and power whores, on my people, they took turns<br>
Dispute the suits, I ignite and then watch 'em burn
</div>
<p>
With the thoughts from a militant mind<br>
Hardline, hardline after hardline<br>
Landlords and power whores, on my people, they took turns<br>
Dispute the suits, I ignite and then watch 'em burn<br>
</p>
<p>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn (Yes, you're gonna)<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
</p>
<p>
It goes a one, two, three, another funky, radical bombtrack<br>
Started as a sketch in my notebook<br>
And now dope hooks make punks take another look<br>
My thoughts ya hear and ya begin to fear<br>
That ya card will get pulled if ya interfere
</p>
<p>
With the thoughts from a militant mind<br>
Hardline, hardline after hardline<br>
Landlords and power whores, on my people, they took turns<br>
Dispute the suits, I ignite and then watch 'em burn<br>
</p>
<p>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn (Yes, you're gonna)<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
</p>
</div>
</div>
<p>In some cases this behavior is not desirable. You can use <code>overscroll-behavior</code> to prevent scrolls from chaining.</p>
<pre><code class="language-css">.inner {
overscroll-behavior: none;
}</code></pre>
<div class="sample" data-sample="demo">
<div class="demo demo2 outer">
<div class="inner">
And suckers be thinkin' that they can fake this<br>
But I'ma drop it at a higher level<br>
'Cause I'm inclined to stoop down, hand out some beatdowns<br>
Could run a train on punk fools that think they run the game<br>
But I learned to burn that bridge and delete<br>
Those who compete at a level that's obsolete<br>
Instead, I warm my hands upon the flames of the flag<br>
To recall the downfall and the businesses that burnt us all<br>
See through the news and the views that twist reality<br>
Enough, I call the bluff, fuck Manifest Destiny<br>
Landlords and power whores, on my people, they took turns<br>
Dispute the suits, I ignite and then watch 'em burn
</div>
<p>
With the thoughts from a militant mind<br>
Hardline, hardline after hardline<br>
Landlords and power whores, on my people, they took turns<br>
Dispute the suits, I ignite and then watch 'em burn<br>
</p>
<p>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn (Yes, you're gonna)<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
</p>
<p>
It goes a one, two, three, another funky, radical bombtrack<br>
Started as a sketch in my notebook<br>
And now dope hooks make punks take another look<br>
My thoughts ya hear and ya begin to fear<br>
That ya card will get pulled if ya interfere
</p>
<p>
With the thoughts from a militant mind<br>
Hardline, hardline after hardline<br>
Landlords and power whores, on my people, they took turns<br>
Dispute the suits, I ignite and then watch 'em burn<br>
</p>
<p>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn (Yes, you're gonna)<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
Burn, burn, yes, you're gonna burn<br>
</p>
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+47%3A+the+overscroll-behavior+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 48: inset 02022-11-30T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day48
<p>On day 9 I’ve talked about the inset shorthand properties <a href="/blog/2022/100daysof-day9/">inset, inset-inline, and inset-block</a>. I don’t believe that I will need those often, but <code>inset</code> can come in handy when you want one element to fill another element entirely.</p><p>If you have an outer element and an inner element and you want the inner element to fill its parent, you can use absolute positioning and set <code>top</code>, <code>right</code>, <code>bottom</code>, and <code>left</code> to <code>0</code>.</p>
<pre><code class="language-html"><div class="outer">
<div class="inner">
</div>
</div></code></pre>
<style>
.outer {
border: 10px solid hotpink;
position: relative;
width: 7rem;
height: 7rem;
}
.inner {
background: aqua;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.inner2 {
position: absolute;
inset: 0;
background: aqua;
}
</style>
<pre><code class="language-css">.outer {
border: 10px solid hotpink;
position: relative;
width: 7rem;
height: 7rem;
}
.inner {
background: aqua;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}</code></pre>
<div class="outer">
<div class="inner">
</div>
</div>
<p>Instead, you could also just set <code>inset</code> to <code>0</code>.</p>
<pre><code class="language-css">.inner {
position: absolute;
inset: 0;
background: aqua;
}</code></pre>
<div class="outer">
<div class="inner2">
</div>
</div>
<p>Of course, this also applies to a fixed positioned element that you want to fill the viewport with.</p>
<pre><code class="language-css">.inner {
position: fixed;
inset: 0;
background: aqua;
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+48%3A+inset+0%E2%80%9D">blog@matuzo.at</a>.</p> Day 49: layering entire style sheets2022-12-01T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day49
<p>You can use <code>@import</code> to load entire style sheets into a cascade layer.</p><pre><code class="language-css">@import url("path/to/the/styles.css") layer(layername);</code></pre>
<p>For example, you could load something like Bootstrap into a dedicated third-party layer.</p>
<style>
@layer third-party, base, components, utility;
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css") layer(third-party);
@layer base {
body {
/* my custom styles */
}
}
</style>
<pre><code class="language-css">@layer third-party, base, components, utility;
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css") layer(third-party);
@layer base {
body {
/* my custom styles */
}
}</code></pre>
<pre><code class="language-html"><button type="button" class="btn btn-primary">Primary</button></code></pre>
<div class="sample" data-sample="demo: styles coming from the bootstrap CDN">
<button type="button" class="btn btn-primary">Primary</button>
</div>
<p>An important thing to know when importing styles is that it matters where you put the <code>@import</code> rule. <a href="https://www.w3.org/TR/css-cascade-5/#at-import">In the spec</a> it says:</p>
<blockquote>Any @import rules must precede all other valid at-rules and style rules in a style sheet (ignoring @charset and empty @layer definitions) and must not have any other valid at-rules or style rules between it and previous @import rules, or else the @import rule is invalid.</blockquote>
<p>This is invalid:</p>
<pre><code class="language-css">@layer third-party, base, components, utility;
@layer base {
body {
/* my custom styles */
}
}
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css") layer(third-party);</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+49%3A+layering+entire+style+sheets%E2%80%9D">blog@matuzo.at</a>.</p> Day 50: :has(:not()) vs. :not(:has())2022-12-02T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day50
<p>Something I was tripping over when I began learning about <code>:has()</code> was the combination with <code>:not()</code>.</p><p>Let me show you what I got wrong by using an example. Let's say we have two cards, each with a heading and some text. One of them also contains an image. </p>
<pre><code class="language-html"><div class="card">
<h2>Card with image</h2>
<img src="https://assets.codepen.io/144736/skateboard.jpg" alt="" />
<p>text</p>
</div>
<div class="card">
<h2>Card without image</h2>
<p>text</p>
</div></code></pre>
<style>
.democard.card {
border: 4px solid;
max-width: 20rem;
margin-bottom: 2rem;
display: flex;
flex-direction: column;
width: 100%;
}
.democard img {
max-width: 100%;
order: 0;
border: 0;
}
.democard h2 {
order: 1;
margin-top: 1rem;
padding: 0 1rem;
}
.democard p {
order: 2;
padding: 0 1rem 1rem;
}
.democard2.card:has(:not(img)) {
border-style: dotted;
}
.democard2.card:has(:not(img)) h2 {
margin-top: 0;
}
.democard3.card:not(:has(img)) {
border-style: dotted;
}
.democard3.card:not(:has(img)) h2 {
margin-top: 0;
}
.democols {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
</style>
<div class="democols">
<div class="democard card">
<h2>Card with image</h2>
<img src="https://assets.codepen.io/144736/skateboard.jpg" alt="" />
<p>text</p>
</div>
<div class="democard card">
<h2>Card without image</h2>
<p>text</p>
</div>
</div>
<p>Now we want to add additional styling to cards without an image. If a card doesn't contain an image, we want to remove the margin on the heading and change the border-style.</p>
<pre><code class="language-css">.card:has(:not(img)) {
border-style: dotted;
}
.card:has(:not(img)) h2 {
margin-top: 0;
}</code></pre>
<div class="democols">
<div class="democard democard2 card">
<h2>Card with image</h2>
<img src="https://assets.codepen.io/144736/skateboard.jpg" alt="" />
<p>text</p>
</div>
<div class="democard democard2 card">
<h2>Card without image</h2>
<p>text</p>
</div>
</div>
<p>The styles apply to both cards, no matter whether an image is present. That's because <code>.card:has(:not(img))</code> means “select a <code>.card</code> that has any element that is not an image”. This means that the selector only wouldn't apply if the card only contained images.</p>
<pre><code class="language-html"><div class="card">
<img src="https://assets.codepen.io/144736/skateboard.jpg" alt="" />
</div></code></pre>
<div class="democard democard2 card">
<img src="https://assets.codepen.io/144736/skateboard.jpg" alt="" />
</div>
<p>If we switch <code>:has()</code> and <code>:not()</code> we're instructing the browser to do something completely different. <code>.card:not(:has(img))</code> means “select a <code>.card</code> doesn't have (not has) an image”, and that's exactly what we want in this case.</p>
<pre><code class="language-css">.card:not(:has(img)) {
border-style: dotted;
}
.card:not(:has(img)) h2 {
margin-top: 0;
}</code></pre>
<div class="democols">
<div class="democard democard3 card">
<h2>Card with image</h2>
<img src="https://assets.codepen.io/144736/skateboard.jpg" alt="" />
<p>text</p>
</div>
<div class="democard democard3 card">
<h2>Card without image</h2>
<p>text</p>
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+50%3A+%3Ahas%28%3Anot%28%29%29+vs.+%3Anot%28%3Ahas%28%29%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 51: aspect-ratio and replaced elements2022-12-05T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day51
<p>Most elements have no preferred aspect ratio. On <a href="/blog/2022/100daysof-day42/">day 42</a> I’ve explained how you can use the <code>aspect-ratio</code> property to define a ratio for these elements. Replaced elements like <code><iframe></code>, <code><video></code>, <code><embed></code>, or <code><img></code>, on the other hand, have an intrinsic aspect ratio. This means that you don’t have to define one using the <code>aspect-ratio</code> property and they will scale naturally.</p><div data-sample="demo: natural aspect ratio of the image">
<img alt="Natural ratio. The danube river in Vienna." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day51/a7a4de5a3a-1698950578/neue-donau.webp" width="400">
</div>
<p>You can change the aspect ratio of an image by using <code>aspect-ratio</code> and defining either a <code>height</code> or <code>width</code> with a value other than <code>auto</code>.</p>
<pre><code class="language-css">img {
width: 400px;
aspect-ratio: 1;
}</code></pre>
<style>
.square {
width: 400px;
aspect-ratio: 1;
}
.autoAndRatio {
width: 400px;
aspect-ratio: auto 3 / 1;
max-width: 100%;
}
div.autoAndRatio {
border: 3px solid;
}
</style>
<div data-sample="demo">
<img alt="Skewed, square image. The danube river in Vienna."" class="square" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day51/a7a4de5a3a-1698950578/neue-donau.webp">
</div>
<p>The default value of the <code>aspect-ratio</code> property is <code>auto</code> (depending on the element, it’s either no preferred aspect ratio or the natural, intrinsic aspect ratio.). You can change the value to a ratio (<code>1</code>, <code>16/9</code>, <code>666/666</code>, etc.), or you can do both.</p>
<pre><code class="language-css">.autoAndRatio {
width: 400px;
aspect-ratio: auto 3 / 1;
}</code></pre>
<p>If you both specify <code>auto</code> and a ratio together, replaced elements will use their natural aspect ratio (<code>auto</code>) and all other elements the specified ratio (<code>16 / 9</code>).</p>
<pre><code class="language-html"><img alt="The danube river in Vienna.&quot;" class="autoAndRatio" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day51/a7a4de5a3a-1698950578/neue-donau.webp">
<div class="autoAndRatio"></div></code></pre>
<div data-sample="demo: image natural aspect ratio and 3 / 1 for the div">
<img alt="Natural ratio. The danube river in Vienna."" class="autoAndRatio" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day51/a7a4de5a3a-1698950578/neue-donau.webp">
<div class="autoAndRatio"></div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+51%3A+aspect-ratio+and+replaced+elements%E2%80%9D">blog@matuzo.at</a>.</p> Day 52: declaring multiple layer lists2022-12-06T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day52
<p>On <a href="/blog/2022/100daysof-day46/">day 46</a>, I’ve explained how you can order layers by defining them in a comma-separated list first. The first layer in the list has the lowest priority and the last layer the highest.</p><pre><code class="language-css">@layer base, components, theme, framework;</code></pre>
<p>You can create as many lists as so you want, but the important thing to remember is that layers are stacked based on the order in which they first appear. If you define one layer in multiple lists, only the first appearance of that layer matters.</p>
<pre><code class="language-css">@layer base, components, theme;
@layer framework, base, components;
@layer base {
p {
border: 10px solid red;
}
}
@layer framework {
p {
border-color: blue;
}
}
@layer components {
p {
border-color: rebeccapurple;
}
}
@layer theme {
p {
border-color: green;
}
}</code></pre>
<style>
@layer base, components, theme;
@layer framework, components, base;
@layer base {
[data-sample] p {
border: 10px solid red;
}
}
@layer framework {
[data-sample] p {
border-color: blue;
}
}
@layer components {
[data-sample] p {
border-color: rebeccapurple;
}
}
@layer theme {
[data-sample] p {
border-color: green;
}
}
</style>
<p>Although <code>components</code> is the last layer in the last list and therefore should have the highest priority, the color of the border is blue, as defined in the <code>framework</code> layer. That’s because base and components have already been defined earlier. </p>
<p>@layer base, components, theme;<br><br />
@layer framework, <s>base</s>, <s>components</s>;</p>
<div data-sample="demo" class="sample demo1">
<p>
Bartender, I got me a bet for you. I'm gonna bet you $300 that I can piss into that glass over there and not spill a single, solitary drop.
</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+52%3A+declaring+multiple+layer+lists%E2%80%9D">blog@matuzo.at</a>.</p> Day 53: disabling pull-to-refresh2022-12-07T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day53
<p>On day 47, I introduced you to the <a href="/blog/2022/100daysof-day47/">overscroll-behavior property</a>, and I showed you how to use it to disable scroll-chaining. There’s another feature we can disable using this property.</p><p>In some mobile browsers, you can refresh the page by swiping down when the page is scrolled to the very top. That's called pull-to-refresh. This is a great feature, but depending on what the user’s interacting with on the page, this can be undesirable.</p>
<p>You can use <code>overscroll-behavior: none;</code> to disable pull-to-refresh.</p>
<pre><code class="language-css">html, body {
overscroll-behavior: none;
}</code></pre>
<p>You have to put it on <code><html></code> and <code><body></code> because in Chrome it only works on the <code><body></code> and in Safari only on the <code><html></code> element (tested on Android 12 Chrome, FF, Samsung Internet and Safari 16 on iOS).</p>
<p>Please don't disable this feature by default, only when it's beneficial to your users.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+53%3A+disabling+pull-to-refresh%E2%80%9D">blog@matuzo.at</a>.</p> Day 54: testing for the support of a selector2022-12-08T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day54
<p>Support for a CSS property isn’t the only thing you can check with <code>@supports()</code>, you can also check support for a selector.</p><p>I knew you can check whether a property is supported by the current browser and apply styles accordingly. </p>
<style>
@supports (display: grid) {
.grid {
display: block
}
}
<p>@supports selector(:has(a)) {<br />
.has {<br />
display: block<br />
}<br />
}</p>
</style>
<pre><code class="language-html"><div hidden class="grid">
Your browser supports <code>display: grid</code> 🎉
</div></code></pre>
<pre><code class="language-css">@supports (display: grid) {
.grid {
display: block
}
}</code></pre>
<div data-sample="demo">
<div hidden class="grid">
Your browser supports <code>display: grid</code> 🎉
</div>
</div>
<p>What I didn’t know is that you can do the same, but for a selector using the <code>selector()</code> function.</p>
<pre><code class="language-html"><div hidden class="has">
Your browser supports <code>:has()</code> 🎉
</div></code></pre>
<pre><code class="language-css">@supports selector(:has(a)) {
.has {
display: block
}
}</code></pre>
<div data-sample="demo">
<div hidden class="has">
Your browser supports <code>:has()</code> 🎉
</div>
</div>
<p>You can also reverse the query.</p>
<pre><code class="language-css">@supports not selector(:has(a)) {
/* You're Firefox, Opera Mini, etc. fallback */
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+54%3A+testing+for+the+support+of+a+selector%E2%80%9D">blog@matuzo.at</a>.</p> Day 55: anonymous layers2022-12-09T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day55
<p>In all previous posts about cascade layers I’ve used named layers in the demos, but it’s actually not required to name them.</p><p>Using <code>@layer</code> without a name works just as well. The downsides are that you can’t append styles elsewhere in the document to the same layer and you can’t define a custom order since you don’t have a name to reference them.</p>
<pre><code class="language-css">@layer {
p {
border: 10px solid red;
}
}
@layer {
p {
border-color: rebeccapurple;
}
}</code></pre>
<style>
@layer {
[data-sample] p {
border: 10px solid red;
}
}
@layer {
[data-sample] p {
border-color: rebeccapurple;
}
}
</style>
<div data-sample="demo" class="sample demo1">
<p>
Bartender, I got me a bet for you. I'm gonna bet you $300 that I can piss into that glass over there and not spill a single, solitary drop.
</p>
</div>
<p>You can also import style sheets into an anonymous layer by using the <code>layer</code> keyword.</p>
<pre><code class="language-css">@import url('style.css') layer;
</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+55%3A+anonymous+layers%E2%80%9D">blog@matuzo.at</a>.</p> Day 56: container queries2022-12-12T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day56
<p>You can use media queries to style elements based on features of the browser viewport, for example, <code>min-width</code>, <code>max-height</code>, or orientation. With container queries, you can now do the same but with any parent element. Instead of the viewport, you can now listen to properties and features of a containing element.</p><p>You can query all kinds of things, not just the width, height, or orientation, but for example custom properties, as well. There’s an important difference between size container features (<code>width</code>, <code>height</code>, <code>inline-size</code>, <code>block-size</code>, <code>aspect-ratio</code>, <code>orientation</code>) and style container features (computed values). If you want to query size container features, you have to define a size container explicitly. That’s because they require special size containment in order to function. </p>
<p>Miriam Suzanne explains it in <a href="https://www.oddbird.net/2022/08/18/cq-syntax/">Use the Right Container Query Syntax</a> like that:</p>
<blockquote>
<p>Normally, the size of an element would be based on the size of its contents – but if we query that size, and change the contents based on the query, we have an infinite loop. Size containment breaks that loop by ensuring the size of a container is not based on the size of its contents.</p>
</blockquote>
<p>You can create a size container using the <code>container-type</code> property. There are two options: <code>inline-size</code> (establishes size containment on the inline axis) and <code>size</code> (establishes size containment on the inline and block axis). There’s no dedicated <code>block-size</code> option and we should be careful using <code>size</code>, according to Miriam Suzanne. I don’t really understand why yet, but I’ll dig into that in a separate post.</p>
<pre><code class="language-html"><section>
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section></code></pre>
<pre><code class="language-css">/* That's our size container. */
section {
width: 50%;
container-type: inline-size;
}
/* Default styles for the card. */
.card {
background-color: yellow;
border: 5px solid;
}</code></pre>
<p>We can query the container using <code>@container</code>.</p>
<pre><code class="language-css">/* The background-color of the .card
changes to hotpink once the section
has a min-width of 500px */
@container (min-width: 500px) {
.card {
background-color: hotpink;
}
}
/* For comparison: The border-style
of the .card changes to dotted once
the viewport has a min-width of 500px */
@media (min-width: 500px) {
.card {
border-style: dotted;
}
}</code></pre>
<style>
[data-sample] section {
container-type: inline-size;
outline: 10px solid;
overflow: auto;
}
[data-sample] .card {
background-color: yellow;
border: 5px solid;
padding: 1rem;
margin: 1rem;
}
[data-sample] h2 {
margin: 1rem;
}
[data-sample] .card h2 {
background: none;
}
@container (min-width: 500px) {
[data-sample] .card {
background-color: hotpink;
}
}
@media (min-width: 500px) {
[data-sample] .card {
border-style: dotted;
}
}
</style>
<p>You can grab and resize the <code><section></code> by clicking and dragging it in the bottom right corner. The background color of the <code>.card</code> changes as soon as the width of the parent section hits <code>500px</code>.</p>
<div data-sample="demo">
<section>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+56%3A+container+queries%E2%80%9D">blog@matuzo.at</a>.</p> Day 57: media queries: range syntax2022-12-13T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day57
<p>With <a href="https://www.w3.org/TR/mediaqueries-4/">CSS Media Queries Level 4</a>, it's possible to use mathematical comparison operators in media queries.</p><p>Instead of <code>(min-width: 768px)</code> you can now write <code>(width >= 768px)</code>.</p>
<p class="code-label">
<strong>Before</strong>
</p>
<pre><code class="language-css">@media(min-width: 768px) {
body {
background-color: aqua;
}
}</code></pre>
<p class="code-label">
<strong>After</strong>
</p>
<pre><code class="language-css">@media(width >= 768px) {
body {
background-color: aqua;
}
}</code></pre>
<p class="code-label">
<strong>Before</strong>
</p>
<pre><code class="language-css">@media(min-width: 400px) and (max-width: 800px) {
body {
border: 40px dotted yellow;
}
}</code></pre>
<p class="code-label">
<strong>After</strong>
</p>
<pre><code class="language-css">@media(400px <= width <= 800px) {
body {
border: 40px dotted yellow;
}
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+57%3A+media+queries%3A+range+syntax%E2%80%9D">blog@matuzo.at</a>.</p> Restart2022-12-14T00:00:00+00:00https://www.matuzo.at/blog/2022/restart
<p>Like many others, I’ve been thinking a lot about Twitter, social media in general, and what being on a social media platform means for me.</p><p>The other day I logged into Twitter with my HTMHell account and I looked at the feed. It was full of opinions by “tech personalities” (men), funny tweets, and random videos. It felt like being on Reddit or 9gag. That’s not the reason I’m on social media. To be fair, it mostly showed recommended tweets because I only follow 3 accounts (React, Vue, and Angular. Funny, right?), but others told me that their feeds are full of NFT and crypto spam and Musk’s brain farts, too.</p>
<p>Looking at the Twitter timeline again after a month of being on Mastodon made me realize just how fucked up Twitter is. So much superficial, attention seeking and useless crap driven by the urge for more and more engagement fueled by a harmful algorithm. This form of social media is not good for us. And it’s not just Twitter, Instagram and TikTok aren’t any better. The other day I saw a report on TV that talked about the severe psychological consequences social media has on us and our self-perception.</p>
<p>(Too much) social media is not good for us. Yes, I know, that’s not a new insight, but I feel like I’m finally being honest with myself for the first time in terms of my behaviour on social media. Just like others constantly post photos to get attention and engagement, I did the same with other content. I feel like it’s time for me to restart. Social media is great for many reasons, but I believe I wasn’t using it correctly.</p>
<p>By the end of the year, I will delete the HTMHell Twitter account. No, not deactivate, delete! Many say that they still want to keep their accounts in case things might get better again, but I believe they're lying to themselves. The ship has sailed, Twitter is dead.</p>
<p>I will move my personal website to a different stack. A CMS instead of 11ty. When <a href="https://andy-bell.co.uk/the-daunting-prospect-of-writing-again/">I asked Andy</a> how he made comments work on his static site, he replied, “Ah this is a WordPress site! That was the problem—I was messing around so much with tech, rather than just, y’know, writing. This setup makes it super easy for me :)”, and he’s absolutely right. I want to enable comments on my site and maybe even add a login, and I really don’t want to jump through hoops for something as easy as that just because I want to use a certain stack.</p>
<p>I’ll reactivate the <a href="https://www.htmhell.dev/newsletter/">HTMHell Newsletter</a>. I’ve started it about a year ago but quickly got bored or overwhelmed. I don’t know. Maybe I didn’t get as much engagement as on Twitter? :) Anyways, I believe newsletters are I still a nice way of sharing and retrieving information.</p>
<p>I’ll use Mastondon to keep in touch with friends and share ideas, but I won’t promote my work as much on there. If people want to follow my work, they can visit my site or follow via RSS.</p>
<p>And finally, I’ve deleted the Mastodon app on my smartphone. Checking the site whenever I’m in front of a computer should be enough. This means no social media in the evening and on weekends for me. Doesn’t sound too bad.</p>
<p>Let’s see how that goes. :)</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CRestart%E2%80%9D">blog@matuzo.at</a>.</p> Day 58: ordering nested layers2022-12-14T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day58
<p>On day 43, we've learned how to <a href="/blog/2022/100daysof-day43/">group layers</a> and on day 46, how to <a href="/blog/2022/100daysof-day43/">order them</a>. In this post, we’ll look into ordering grouped layers.</p><p>If we take the following layer, the background color of the <code><p></code> will be red because the last defined layer has precedence over previously defined layers.</p>
<pre><code class="language-css">@layer base {
@layer reset {
p {
background-color: #fff;
}
}
@layer theme {
p {
background-color: aqua;
}
}
@layer defaults {
p {
background-color: red;
}
}
}</code></pre>
<style>
@layer base {
@layer reset {
.sample1 p {
background-color: #fff;
}
}
@layer theme {
.sample1 p {
background-color: aqua;
}
}
@layer defaults {
.sample1 p {
background-color: red;
}
}
}
@layer base1 {
@layer reset, defaults, theme;
@layer reset {
.sample2 p {
background-color: #fff;
}
}
@layer theme {
.sample2 p {
background-color: aqua;
}
}
@layer defaults {
.sample2 p {
background-color: red;
}
}
}
@layer reset3, defaults3, theme3;
@layer base3 {
@layer reset3 {
.sample3 p {
background-color: #fff;
}
}
@layer theme3 {
.sample3 p {
background-color: aqua;
}
}
@layer defaults3 {
.sample3 p {
background-color: red;
}
}
}
@layer base4.reset, base4.defaults, base4.theme;
@layer base4 {
@layer reset {
.sample4 p {
background-color: #fff;
}
}
@layer theme {
.sample4 p {
background-color: aqua;
}
}
@layer defaults {
.sample4 p {
background-color: red;
}
}
}
@layer base5.reset, component.first, base5.defaults, base5.theme;
@layer base5 {
@layer reset {
.sample5 p {
background-color: #fff;
}
}
@layer theme {
.sample5 p {
background-color: aqua;
}
}
@layer defaults {
.sample5 p {
background-color: red;
}
}
}
@layer component {
@layer first {
.sample5 p {
background-color: hotpink;
}
}
}
</style>
<div class="sample1" data-sample="demo: the background color is red">
<p>yo!</p>
</div>
<p>We can change the order by defining the layers within the <code>base</code> layer upfront.</p>
<pre><code class="language-css">@layer base {
@layer reset, defaults, theme;
@layer reset {
p {
background-color: #fff;
}
}
@layer theme {
p {
background-color: aqua;
}
}
@layer defaults {
p {
background-color: red;
}
}
}</code></pre>
<p><code>reset</code> has the lowest priority and <code>theme</code> the highest.</p>
<div class="sample2" data-sample="demo: the background color is aqua">
<p>yo!</p>
</div>
<p>If we move the list outside of the base layer, our custom order has no effect because the layers in the list only exist inside the base layer.</p>
<pre><code class="language-css">@layer reset, defaults, theme;
@layer base {
@layer reset {
p {
background-color: #fff;
}
}
@layer theme {
p {
background-color: aqua;
}
}
@layer defaults {
p {
background-color: red;
}
}
}</code></pre>
<div class="sample3" data-sample="demo: the background color is red">
<p>yo!</p>
</div>
<p>If we want to change the order inside a layer from the outside, we can do that by referencing the nested layer on the parent layer, similar to how you would reference a property in a JavaScript object.</p>
<pre><code class="language-css">@layer base.reset, base.defaults, base.theme;
@layer base {
@layer reset {
p {
background-color: #fff;
}
}
@layer theme {
p {
background-color: aqua;
}
}
@layer defaults {
p {
background-color: red;
}
}
}</code></pre>
<div class="sample4" data-sample="demo: the background color is aqua">
<p>yo!</p>
</div>
<p>If we add another parent layer to the mix, you can see how the background color is hotpink even though we’ve added the <code>component.first</code> layer early in the list. That’s because top level layers are sorted first, and then the layers within each layer group. The parent layer <code>component</code> has precedence over the <code>base</code> layer because it comes later in the list.</p>
<pre><code class="language-css">@layer base.reset, component.first, base.defaults, base.theme;
@layer base {
@layer reset {
p {
background-color: #fff;
}
}
@layer theme {
p {
background-color: aqua;
}
}
@layer defaults {
p {
background-color: red;
}
}
}
@layer component {
@layer first {
p {
background-color: hotpink;
}
}
}</code></pre>
<div class="sample5" data-sample="demo: the background color is hotpink">
<p>yo!</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+58%3A+ordering+nested+layers%E2%80%9D">blog@matuzo.at</a>.</p> Day 59: naming containers2022-12-15T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day59
<p>When you add a container query, it will look for the nearest ancestor container, by default. If you have multiple nested containers or if you just want to make sure that your query uses the right container, you can name containers and query them specifically.</p><p>Let's say, we have 2 size containers, <code>.wrapper</code> and <code><section></code>.</p>
<pre><code class="language-html"><div class="wrapper">
<section>
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section>
</div></code></pre>
<pre><code class="language-css">/* That's our outer size container. */
.wrapper {
container-type: inline-size;
}
/* That's our inner size container. */
section {
width: 50%;
container-type: inline-size;
}
/* Default styles for the card. */
.card {
background-color: yellow;
border: 5px solid;
}
/* Container query */
@container (min-width: 500px) {
.card {
background-color: hotpink;
}
}</code></pre>
<style>
[data-sample] .wrapper {
container-type: inline-size;
outline: 10px dashed;
padding: 20px;
resize: horizontal;
overflow: auto;
}
[data-sample].sample2 .wrapper {
container-name: wrapper;
}
[data-sample] section {
width: 50%;
container-type: inline-size;
outline: 10px solid;
resize: horizontal;
overflow: auto;
}
[data-sample] .card {
background-color: yellow;
border: 5px solid;
padding: 1rem;
margin: 1rem;
}
[data-sample] h2 {
margin: 1rem;
}
[data-sample] .card h2 {
background: none;
}
@container (min-width: 500px) {
[data-sample] .card {
background-color: hotpink;
}
}
@media (min-width: 500px) {
[data-sample] .card {
border-style: dotted;
}
}
@container wrapper (min-width: 500px) {
.sample2 .card {
background-color: hotpink;
}
}
</style>
<p>By default, the container query watches the width of the closest size container, <code><section></code>. You can grab and resize the <code><section></code> by clicking and dragging it in the bottom right corner. The background color of the <code>.card</code> changes as soon as the width of the parent section hits <code>500px</code>.</p>
<div data-sample="demo">
<div class="wrapper">
<section>
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section>
</div>
</div>
<p>By naming the container and using that name in the query, you can target a specific container.</p>
<pre><code class="language-css">/* That's our outer size container. */
.wrapper {
container-type: inline-size;
container-name: wrapper;
}
/* That's our inner size container. */
section {
container-type: inline-size;
}
/* The query watches the width of the outer size container (.wrapper) */
@container wrapper (min-width: 500px) {
.card {
background-color: hotpink;
}
}</code></pre>
<p>You can grab and resize the <code>.wrapper</code> by clicking and dragging it in the bottom right corner. The background color of the <code>.card</code> changes as soon as the width of the <code>.wrapper</code> is lower than <code>500px</code>.</p>
<div data-sample="demo" class="sample2">
<div class="wrapper">
<section>
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section>
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+59%3A+naming+containers%E2%80%9D">blog@matuzo.at</a>.</p> Day 60: the ::part() pseudo-element2022-12-16T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day60
<p>You can use the <code>::part</code> CSS pseudo-element to style an element within a shadow tree.</p><p>Let's have a look at the following web component.<br><br />
There's a heading and a paragraph in the shadow DOM and we can pass content via light DOM.</p>
<pre><code class="language-js">class NewsTeasers extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<h3>Latest news</h3>
<p>Here's a selection of our latest blog posts.</p>
<slot></slot>
`
}
}
customElements.define('news-teasers', NewsTeasers);</code></pre>
<pre><code class="language-html"><news-teasers>
<ol>
<li><a href="#1">Blog post 1</a></li>
<li><a href="#2">Blog post 2</a></li>
<li><a href="#3">Blog post 3</a></li>
<li><a href="#4">Blog post 4</a></li>
</ol>
</news-teasers></code></pre>
<script>
class NewsTeasers extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<h3>Latest news</h3>
<p>Here's a selection of our latest blog posts.</p>
<slot></slot>
`
}
}
customElements.define('news-teasers', NewsTeasers);
class NewsTeasers2 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<h3 part="heading">Latest news</h3>
<p part="intro">Here's a selection of our latest blog posts.</p>
<slot></slot>
`
}
}
customElements.define('news-teasers2', NewsTeasers2);
</script>
<style>
.demo2 news-teasers {
border: 8px dashed;
display: block;
padding: 20px;
}
/* inheritable properties */
.demo2 news-teasers {
color: green;
}
.demo2 news-teasers ol {
border: 4px dotted;
}
.demo3 news-teasers h3 {
font-size: 2rem;
}
/* light DOM */
.demo3 news-teasers p {
background-color: #000;
color: #fff;
}
</style>
<div data-sample="demo">
<news-teasers>
<ol>
<li><a href="#1">Blog post 1</a></li>
<li><a href="#2">Blog post 2</a></li>
<li><a href="#3">Blog post 3</a></li>
<li><a href="#4">Blog post 4</a></li>
</ol>
</news-teasers>
</div>
<p>Sometimes it's desirable to allow authors to style web components from the outside. I've written about our options on <a href="/blog/2022/100daysof-day10">day 10</a>, <a href="/blog/2022/100daysof-day18">day 18</a>, and <a href="/blog/2022/100daysof-day28">day 28</a>. In summary, we can style the element itself, we can change inheritable properties, and we can style elements in light DOM.</p>
<pre><code class="language-css">/* the element itself */
news-teasers {
border: 8px dashed;
}
/* inheritable properties */
news-teasers {
color: green;
}
/* light DOM */
news-teasers ol {
border: 4px dotted;
}</code></pre>
<div data-sample="demo" class="demo2">
<news-teasers>
<ol>
<li><a href="#1">Blog post 1</a></li>
<li><a href="#2">Blog post 2</a></li>
<li><a href="#3">Blog post 3</a></li>
<li><a href="#4">Blog post 4</a></li>
</ol>
</news-teasers>
</div>
<p>If we try to style elements in the shadow DOM, we're out of luck. We don't have access to them from the outside.</p>
<pre><code class="language-css">/* shadow DOM */
news-teasers h3 {
font-size: 2rem;
}
news-teasers p {
background-color: #000;
color: #fff;
}</code></pre>
<div data-sample="demo" class="demo3">
<news-teasers>
<ol>
<li><a href="#1">Blog post 1</a></li>
<li><a href="#2">Blog post 2</a></li>
<li><a href="#3">Blog post 3</a></li>
<li><a href="#4">Blog post 4</a></li>
</ol>
</news-teasers>
</div>
<p>This is where <code>[part]</code> and <code>::part</code> come into play. The <code>part</code> attribute exposes an element outside of the shadow tree and the <code>::part()</code> pseudo-element allows you to select the exposed element.</p>
<pre><code class="language-js">class NewsTeasers extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<h3 part="heading">Latest news</h3>
<p part="intro">Here's a selection of our latest blog posts.</p>
<slot></slot>
`
}
}
customElements.define('news-teasers', NewsTeasers);</code></pre>
<style>
news-teasers2::part(heading) {
font-size: 2rem;
}
news-teasers2::part(intro) {
background-color: #000;
color: #fff;
}
</style>
<pre><code class="language-css">/* exposed parts from the shadow DOM */
news-teasers::part(heading) {
font-size: 2rem;
}
news-teasers::part(intro) {
background-color: #000;
color: #fff;
}</code></pre>
<div data-sample="demo" class="demo4">
<news-teasers2>
<ol>
<li><a href="#1">Blog post 1</a></li>
<li><a href="#2">Blog post 2</a></li>
<li><a href="#3">Blog post 3</a></li>
<li><a href="#4">Blog post 4</a></li>
</ol>
</news-teasers2>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+60%3A+the+%3A%3Apart%28%29+pseudo-element%E2%80%9D">blog@matuzo.at</a>.</p> Day 61: color-scheme2022-12-19T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day61
<p>The <code>color-scheme</code> property allows you to indicate which color schemes an element can be rendered in.</p><p><em>This post is inspired by Sara Wallén’s article <a href="https://www.htmhell.dev/adventcalendar/2022/19/">“Do you know color-scheme?”</a> in the <a href="https://www.htmhell.dev/adventcalendar/">HTMHell advent calendar 2022</a>. Read it to learn more about the feature and other ways of using it.</em></p>
<p>If you create an HTML document, it comes with default styles that are more or less the same in most browsers. A serif font, transparent (white) background, black text color, etc. You could say that the default theme for any HTML document is a light theme because it uses a light background color. Now here comes the big revelation (at least to me) Sara writes about: There's also a dark theme.</p>
<p>The thing is, if you change the color mode in your operating system from dark to light, the colors in the browser’s viewport stay the same (If you know of an operating system (OS)/browser where that's different, please tell me). You have to instruct the browser first about what to do. You can do that by using the <code>color-scheme</code> property.</p>
<pre><code class="language-css">html {
/* Indicates that the element can be rendered using the
operating system dark color scheme. */
color-scheme: dark;
}</code></pre>
<figure style="margin-bottom: 2.4rem"><img src="https://www.htmhell.dev/images/advent2022/19/bare-html.png" alt="Screenshot of two bare-HTML mini-sites, one light, one dark" width="873" height="838" loading="lazy"><figcaption>Comparison of two mock plain HTML sites, one with color-scheme set to light, one set to dark. (source: HTMHell)</figcaption></figure>
<p><em>Note: the following results are only based on tests on macOS 13.0.1 and Android 12.</em></p>
<p>If you set <code>color-scheme: dark</code>, the whole document will be rendered in a dark scheme, even if the color mode of the OS is light. The same goes for using <code>color-scheme: light</code> in dark mode, the document will be rendered in a light scheme.</p>
<p>If you want to take advantage of this feature, but you still want to respect user preference, you have to provide two values.</p>
<pre><code class="language-css">html {
color-scheme: dark light;
}</code></pre>
<p>If the color mode in the OS is light, the color scheme of the document will be light. If the color mode is dark, the scheme will be dark. If you don't have a preference, <code>dark</code> might be used first because it comes first in the list, but I can't confirm that. On macOS at least, the document is always rendered in light mode when the color mode is “auto” in the OS, no matter the order of the values. It only changes when the operating system changes the mode automatically, as well.</p>
<p>You can try it yourself on this page by changing the value from “normal” to <code>light</code>, <code>dark</code>, <code>light dark</code>, or <code>dark light</code> in this editable style element and adjusting the preference in your OS or in your Dev Tools settings.</p>
<style class="editable-style" contenteditable>html {
color-scheme: normal;
}</style>
<p>The property is not limited to the root element, you can also apply it to parts of your page. </p>
<h2>light default</h2>
<pre><code class="language-html"><fieldset>
<legend>Pick a language</legend>
<input type="checkbox" name="lang" id="css">
<label for="css">CSS</label><br>
<input type="checkbox" name="lang" id="html">
<label for="html">HTML</label><br>
<input type="checkbox" name="lang" id="js">
<label for="js">JS</label><br>
</fieldset></code></pre>
<div data-sample="light default">
<fieldset>
<legend>Pick a language</legend>
<input type="checkbox" name="lang" id="css">
<label for="css">CSS</label><br>
<input type="checkbox" name="lang" id="html">
<label for="html">HTML</label><br>
<input type="checkbox" name="lang" id="js">
<label for="js">JS</label><br>
</fieldset>
</div>
<h2>light default with dark background</h2>
<pre><code class="language-css">fieldset {
background-color: #000;
color: #fff;
}</code></pre>
<div data-sample="light default" style="background-color: #000;">
<fieldset style="background-color: #000; color: #fff;">
<legend>Pick a language</legend>
<input type="checkbox" name="lang2" id="css2">
<label for="css2">CSS</label><br>
<input type="checkbox" name="lang2" id="html2">
<label for="html2">HTML</label><br>
<input type="checkbox" name="lang2" id="js2">
<label for="js2">JS</label><br>
</fieldset>
</div>
<h2>dark with dark background</h2>
<pre><code class="language-css">fieldset {
color-scheme: dark;
background-color: #000;
color: #fff;
}</code></pre>
<div data-sample="dark with color-scheme: dark" style="background-color: #000;">
<fieldset style="color-scheme: dark; background-color: #000; color: #fff;">
<legend>Pick a language</legend>
<input type="checkbox" name="lang3" id="css3">
<label for="css3">CSS</label><br>
<input type="checkbox" name="lang3" id="html3">
<label for="html3">HTML</label><br>
<input type="checkbox" name="lang3" id="js3">
<label for="js3">JS</label><br>
</fieldset>
</div>
<p>I had to apply the background color manually, but you can see how the form elements look differently, optimized for dark mode.</p>
<h2>dark on a form element</h2>
<p>You can also apply the property to a form element directly.</p>
<pre><code class="language-css">select {
color-scheme: dark;
}</code></pre>
<pre><code class="language-html"><label for="color2">Favorite color</label>
<select id="color2">
<option>Aqua</option>
<option>Fuchsia</option>
</select></code></pre>
<p>
<label for="color2">Favorite color</label><br>
<select id="color2" style="color-scheme: dark">
<option>Aqua</option>
<option>Fuchsia</option>
</select>
</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+61%3A+color-scheme%E2%80%9D">blog@matuzo.at</a>.</p> Day 62: the container shorthand2022-12-20T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day62
<p>On <a href="/blog/2022/100daysof-day56/">day 56</a> you've learned that you have to define a <code>container-type</code> when working with size containers and on <a href="/blog/2022/100daysof-day59/">day 59</a> you've learned that you can name containers using the <code>container-name</code> property.</p><p>The <code>container</code> shorthand allows you to define both properties in a single property, <code>[name]</code> / <code>[type]</code>.</p>
<pre><code class="language-css">section {
container: wrapper / inline-size;
/*
Same as:
container-name: wrapper;
container-type: inline-size;
*/
}</code></pre>
<p>If you only define a single value (the name), the type is <code>normal</code> by default.</p>
<pre><code class="language-css">section {
container: wrapper;
/*
Same as:
container-name: wrapper;
container-type: normal;
*/
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+62%3A+the+container+shorthand%E2%80%9D">blog@matuzo.at</a>.</p> Day 63: explicit defaulting with inherit, initial, unset, and revert2022-12-21T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day63
<p>There are several CSS-wide property values you can use to specify a particular defaulting behavior for a property explicitly.</p><p>Okay, okay, I know, these keywords aren't new at all, except for <code>revert</code> maybe which is newish, but if I want to write about <code>revert-layer</code>, which is brand new, I need a basic understanding of all keywords. Also, I had the feeling that most of you, like me, don't know what all of these keywords do, and I was right. At least, if you want to trust this <a href="https://front-end.social/@matuzo/109547583525926042">poll on Mastodon</a>.</p>
<h2>Our baseline</h2>
<p>We'll work with the following example. We have a <code><div></code> with a <code>1px solid green</code> border, a <code>red</code> text color, and a <code>10px</code> margin. Nested in the div is a <code><h2></code> with a <code>blue</code> text color.</p>
<style>
[data-sample] div {
border: 1px solid green;
color: red;
margin: 10px;
}
[data-sample] h2 {
color: blue;
}
[data-sample] .inherit {
border: inherit;
color: inherit;
margin: inherit;
}
[data-sample] .initial {
border: initial;
color: initial;
margin: initial;
}
[data-sample] .unset {
border: unset;
color: unset;
margin: unset;
}
[data-sample] .revert {
border: revert;
color: revert;
margin: revert;
}
</style>
<pre><code class="language-html"><div>
<h2>standard</h2>
</div></code></pre>
<pre><code class="language-css">div {
color: red;
border: 1px solid green;
margin: 10px;
}
h2 {
color: blue;
}</code></pre>
<div data-sample="demo">
<div>
<h2>Feliz Navidad</h2>
</div>
</div>
<p>Now, let's apply keywords to the <code>border</code>, <code>color</code>, and <code>margin</code> property on the <code><h2></code> and see what happens. </p>
<h2>inherit</h2>
<pre><code class="language-css">h2 {
border: inherit;
color: inherit;
margin: inherit;
}</code></pre>
<p><code>inherit</code> is pretty straight-forward. The <code><h2></code> inherits all defined properties from its parent element.</p>
<div data-sample="demo">
<div>
<h2 class="inherit">Feliz Navidad</h2>
</div>
</div>
<table>
<caption>h2 inherit keyword: property values</caption>
<thead>
<tr>
<th>property</th>
<th>value</th>
<th>origin</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border-color</code></td>
<td>green</td>
<td>parent</td>
</tr>
<tr>
<td><code>border-style</code></td>
<td>solid</td>
<td>parent</td>
</tr>
<tr>
<td><code>border-width</code></td>
<td>1px</td>
<td>parent</td>
</tr>
<tr>
<td><code>color</code></td>
<td>red</td>
<td>parent</td>
</tr>
<tr>
<td><code>margin</code></td>
<td>10px</td>
<td>parent</td>
</tr>
</thead>
</table>
<h2>initial</h2>
<pre><code class="language-css">h2 {
border: initial;
color: initial;
margin: initial;
}</code></pre>
<p><code>initial</code> sets the value of the property to its <em>initial value</em>. Each property has an <em>initial value</em>, defined in the property’s definition table. For example, if you look at the <a href="https://w3c.github.io/csswg-drafts/css-color/#the-color-property">color property in the specification</a>, you see that the defined initial value in the definition table is <a href="https://w3c.github.io/csswg-drafts/css-color/#css-system-colors">CanvasText</a>.<br />
<strong>The initial value is not the default value of the property</strong> defined in the user agent (browser). For example, the default margin of the body in most (all?) browsers is <code>8px</code>, but the initial value of the margin property is <code>0</code>.</p>
<div data-sample="demo">
<div>
<h2 class="initial">Feliz Navidad</h2>
</div>
</div>
<table>
<caption>h2 initial keyword: property values</caption>
<thead>
<tr>
<th>property</th>
<th>value</th>
<th>origin</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border-color</code></td>
<td>currentColor</td>
<td>initial value</td>
</tr>
<tr>
<td><code>border-style</code></td>
<td>none</td>
<td>initial value</td>
</tr>
<tr>
<td><code>border-width</code></td>
<td>medium</td>
<td>initial value</td>
</tr>
<tr>
<td><code>color</code></td>
<td>canvasText</td>
<td>initial value</td>
</tr>
<tr>
<td><code>margin</code></td>
<td>0</td>
<td>initial value</td>
</tr>
</thead>
</table>
<h2>unset</h2>
<pre><code class="language-css">h2 {
border: unset;
color: unset;
margin: unset;
}</code></pre>
<p><code>unset</code> resets a property to its inherited value if the property naturally inherits from its parent, and to its initial value if not. In our example, it inherits the <code>color</code> from the parent <code><div></code> and sets the <code>border</code> and <code>margin</code> to its initial value.</p>
<div data-sample="demo">
<div>
<h2 class="unset">Feliz Navidad</h2>
</div>
</div>
<table>
<caption>h2 unset keyword: property values</caption>
<thead>
<tr>
<th>property</th>
<th>value</th>
<th>origin</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border-color</code></td>
<td>currentColor</td>
<td>initial value</td>
</tr>
<tr>
<td><code>border-style</code></td>
<td>none</td>
<td>initial value</td>
</tr>
<tr>
<td><code>border-width</code></td>
<td>medium</td>
<td>initial value</td>
</tr>
<tr>
<td><code>color</code></td>
<td>red</td>
<td>parent</td>
</tr>
<tr>
<td><code>margin</code></td>
<td>0</td>
<td>initial value</td>
</tr>
</thead>
</table>
<h2>revert</h2>
<pre><code class="language-css">h2 {
border: revert;
color: revert;
margin: revert;
}</code></pre>
<p><code>revert</code> resets a property to its inherited value if the property naturally inherits from its parent. In our example, it inherits the <code>color</code> from the parent <code><div></code>. If the property is not an inheritable property, <code>revert</code> rolls back the cascaded value to a previous level. If there are user-agent or user default styles, it sets the property to the default value. In our example, it sets the <code>margin</code> of the <code><h2></code> to its user-agent default value of <code>0.83em</code>. If there are no default styles, it sets the value to its initial value. This applies to the <code>border</code> properties in our example.</p>
<div data-sample="demo">
<div>
<h2 class="revert">Feliz Navidad</h2>
</div>
</div>
<table>
<caption>h2 revert keyword: property values</caption>
<thead>
<tr>
<th>property</th>
<th>value</th>
<th>origin</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border-color</code></td>
<td>currentColor</td>
<td>initial value</td>
</tr>
<tr>
<td><code>border-style</code></td>
<td>none</td>
<td>initial value</td>
</tr>
<tr>
<td><code>border-width</code></td>
<td>medium</td>
<td>initial value</td>
</tr>
<tr>
<td><code>color</code></td>
<td>red</td>
<td>parent</td>
</tr>
<tr>
<td><code>margin</code></td>
<td>0.83em</td>
<td>user-agent default</td>
</tr>
</thead>
</table><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+63%3A+explicit+defaulting+with+inherit%2C+initial%2C+unset%2C+and+revert%E2%80%9D">blog@matuzo.at</a>.</p> Day 64: the revert-layer keyword2022-12-22T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day64
<p>With cascade layers comes a new CSS-wide property value, <code>revert-layer</code>.</p><p>You can use the <code>revert-layer</code> keyword to roll back the cascade to a value defined in a previous layer.</p>
<p>In the following example, the <code>base</code> layer defines a black color for the border. The theme layer sets the border color to <code>fuchsia</code>. In a <code>print</code> media query within the <code>theme</code> layer we revert the style back to the color in the <code>base</code> layer.</p>
<style>
@layer base {
[data-sample] h2 {
--border-color: #000;
border: 4px solid var(--border-color);
}
}
@layer theme {
[data-sample] h2 {
--border-color: fuchsia;
}
@media print {
[data-sample] h2 {
--border-color: revert-layer;
}
}
}
</style>
<pre><code class="language-css">@layer base {
h2 {
--border-color: #000;
border: 4px solid var(--border-color);
}
}
@layer theme {
h2 {
--border-color: fuchsia;
}
@media print {
h2 {
--border-color: revert-layer;
}
}
}</code></pre>
<pre><code class="language-html"><h2>Sretan Božić!</h2></code></pre>
<div data-sample="demo">
<h2>Sretan Božić!</h2>
</div>
<p>If you try to print this page, you will see that the <code>border-color</code> of the <code><h2></code> is <code>#000</code>.</p>
<p><button onclick="print()">Preview print stylesheet</button></p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+64%3A+the+revert-layer+keyword%E2%80%9D">blog@matuzo.at</a>.</p> Day 65: using the em unit in container queries2022-12-23T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day65
<p>Relative units used in container queries work differently than relative units in media queries.</p><p>If you use <code>em</code> in a media query, the font-size of the <code><body></code>, <code><html></code>, or any other element on the page doesn't matter. That's because relative length units in media queries are based on the <em>initial value</em>, which means that units are never based on results of declarations. <code>em</code> in a media query is relative to the font-size, defined by the user agent or the user’s preferences.</p>
<pre><code class="language-css">/*
The font size of both <body> and <html> is 40px,
the media query doesn't fire at 2560px (40 * 64),
but at 1024px (16 * 64) (assuming that the base
font size in the browser is 16px).
*/
html, body {
font-size: 40px;
}
@media (min-width: 64em) {
body {
background: aqua;
}
}</code></pre>
<p>With container queries that's different. Relative length units are evaluated based on the <em>computed values</em> of the query container.</p>
<p>The container query in the following example fires at <code>512px</code> (32 * 16 = 512) because the font size of the container is <code>16px</code>.</p>
<pre><code class="language-css">section {
font-size: 16px;
container: wrapper / inline-size;
}
.card {
background-color: yellow;
}
@container wrapper (min-width: 32em) {
.card {
background-color: hotpink;
}
}</code></pre>
<style>
[data-sample] section {
width: 50%;
container-type: inline-size;
outline: 10px solid;
resize: horizontal;
overflow: auto;
font-size: 16px;
}
[data-sample] .large {
font-size: 26px;
}
[data-sample] .card {
background-color: yellow;
border: 5px solid;
padding: 1rem;
margin: 1rem;
}
[data-sample] h2 {
margin: 1rem;
}
[data-sample] .card h2 {
background: none;
}
@container (min-width: 32em) {
[data-sample] .card {
background-color: hotpink;
}
}
</style>
<pre><code class="language-html"><section>
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section></code></pre>
<p>You can grab and resize the <code><section></code> by clicking and dragging it in the bottom right corner.</p>
<div data-sample="demo">
<section>
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section>
</div>
<p>If you change the font size of the container to <code>26px</code>, the media query fires at <code>832px</code> (32 * 26 = 832)</p>
<pre><code class="language-css">.large {
font-size: 26px;
}</code></pre>
<pre><code class="language-html"><section class="large">
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section></code></pre>
<div data-sample="demo">
<section class="large">
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+65%3A+using+the+em+unit+in+container+queries%E2%80%9D">blog@matuzo.at</a>.</p> Day 66: individual transform properties2022-12-26T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day66
<p>From now on you can transform elements with the <code>translate</code>, <code>rotate</code>, and <code>scale</code> properties.</p><p>Let’s say you apply several transforms to an element, and on <code>:hover</code> and <code>:focus</code> you only want to change one of them, for example, <code>scale</code>.</p>
<pre><code class="language-html"><button>Transform</button></code></pre>
<pre><code class="language-css">button {
transform: translateX(20px) rotate(15deg) scale(1);
}
button:is(:hover, :focus) {
transform: scale(2);
}</code></pre>
<style>
.button1 {
transform: translateX(20px) rotate(15deg) scale(1);
}
.button1:is(:hover, :focus) {
transform: scale(2);
}
.button2 {
transform: translateX(20px) rotate(15deg) scale(1);
}
.button2:is(:hover, :focus) {
transform: translateX(20px) rotate(15deg) scale(2);
}
.button3 {
translate: 20px 0;
rotate: 15deg;
scale: 1;
}
.button3:is(:hover, :focus) {
scale: 2;
}
</style>
<div data-sample="demo">
<button class="button1">Transform</button>
</div>
<p>That doesn't work as expected because by setting <code>transform: scale(2)</code> you're overwriting all the previously defined transforms. To fix that, you have to repeat the other transforms.</p>
<pre><code class="language-css">button {
transform: translateX(20px) rotate(15deg) scale(1);
}
button:is(:hover, :focus) {
transform: translateX(20px) rotate(15deg) scale(2);
}</code></pre>
<div data-sample="demo">
<button class="button2">Transform</button>
</div>
<p>That can cause a lot of repetition in your code.</p>
<p>Individual transform properties fix that issue because now you can use <code>translate</code>, <code>rotate</code>, and <code>scale</code> separately.</p>
<pre><code class="language-css">button {
translate: 20px 0;
rotate: 15deg;
scale: 1;
}
button:is(:hover, :focus) {
scale: 2;
}</code></pre>
<div data-sample="demo">
<button class="button3">Transform</button>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+66%3A+individual+transform+properties%E2%80%9D">blog@matuzo.at</a>.</p> Day 67: counting children2022-12-27T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day67
<p>There are a lot of interesting things you can do with the <code>:has()</code> pseudo-class. I’ve already covered some of them on <a href="/blog/2022/100daysof-day26/">day 26</a>.</p><p>If you want to style an element based on the number of direct children it has, you can do that with just CSS.<br />
Let's say you want to style a list in a certain way when it contains at least three items. You use the <code>:has()</code> pseudo-class with the condition that it <em>has</em> a direct child item that is a third child.</p>
<p class="highlight">Note: Firefox <a href="https://caniuse.com/css-has">doesn't support</a> <code>:has()</code> yet.</p>
<pre><code class="language-css">ul:has(>:nth-child(3)) {
border: 10px solid red;
}</code></pre>
<style>
.sample1 ul:has(>:nth-child(3)) {
border: 10px solid red;
}
</style>
<div class="sample1" data-sample="demo: list with 2 items">
<ul>
<li>A</li>
<li>B</li>
</ul>
</div>
<div class="sample1" data-sample="demo: list with 3 items">
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
</div>
<div class="sample1" data-sample="demo: list with 4 items">
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
</ul>
</div>
<p>If you want to limit the rule to only apply styles if the list contains exactly three items, you extend the condition and only select the <code><ul></code> if it contains a direct child item that is the third and last item.</p>
<pre><code class="language-css">ul:has(>:nth-child(3):last-child) {
border: 10px solid red;
}</code></pre>
<style>
.sample2 ul:has(>:nth-child(3):last-child) {
border: 10px solid red;
}
</style>
<div class="sample2" data-sample="demo: list with 2 items">
<ul>
<li>A</li>
<li>B</li>
</ul>
</div>
<div class="sample2" data-sample="demo: list with 3 items">
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
</div>
<div class="sample2" data-sample="demo: list with 4 items">
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
</ul>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+67%3A+counting+children%E2%80%9D">blog@matuzo.at</a>.</p> Day 68: cascade layers and browser support2022-12-28T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day68
<p>Cascade layers are one of the most interesting and useful additions to CSS recently. It will change the way we write CSS, how we use selectors, naming conventions, and probably also more things that I can’t think of right now.</p><p>If you’re as excited about using cascade layers as I am, you have to consider browser support before you get started.</p>
<h2>Browser support</h2>
<p>All major desktop browsers started supporting cascade layers between February and April 2022, but your users might use an older version of Safari, <a href="https://adrianroselli.com/2022/06/internet-explorer-still-does-not-go-away-today.html">IE11</a>, or one of the mobile browsers that doesn’t support it yet.</p>
<figure>
<a href="https://caniuse.com/css-cascade-layers">
<img alt="cascade layers on caniuse.com" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2022/100daysof-day68/955518a914-1698950580/caniuse_cascade-layers.jpg">
</a>
<figcaption>
Browser support: Chrome, Edge since version 99, Safari iOS/macOS 15.4, Firefox 97, Opera 86, Chrome for Android/Android Browser 108, Samsung Internet 18, Firefox for Android 107, Opera Mobile 72.
</figcaption>
</figure>
<h2>Feature queries</h2>
<p>When you use cascade layers in a browser that doesn’t support them, styles in your layers will be ignored entirely. Only unlayered styles will be applied. I was trying to figure out a way to serve styles only to browsers that don’t support layers in order to provide some kind of basic fallback styling for them, but I had no luck.</p>
<p>There is a <a href="https://css-tricks.com/css-cascade-layers/#aa-query-feature-support-using-supports">@supports feature in CSS</a> that will allow us to test for support of <code>@layer</code> and other at-rules, but the feature itself has no support in any browser yet.</p>
<pre><code class="language-css">@supports at-rule(@layer) {
/* code applied for browsers with layer support */
}
@supports not at-rule(@layer) {
/* fallback applied for browsers without layer support */
}</code></pre>
<h2>Polyfill</h2>
<p>If you want to use cascade layers today, your best option is to use this <a href="https://www.npmjs.com/package/@csstools/postcss-cascade-layers">PostCSS polyfill</a>. Miriam Suzanne explains how to use it and how it works in <a href="https://www.oddbird.net/2022/06/21/cascade-layers-polyfill/">Cascade Layers – There's a Polyfill for That!</a></p>
<pre><code class="language-bash">npm install postcss @csstools/postcss-cascade-layers --save-dev</code></pre>
<pre><code class="language-js">const postcss = require('postcss');
const postcssCascadeLayers = require('@csstools/postcss-cascade-layers');
postcss([
postcssCascadeLayers(/* pluginOptions */)
]).process(YOUR_CSS /*, processOptions */);</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+68%3A+cascade+layers+and+browser+support%E2%80%9D">blog@matuzo.at</a>.</p> Day 69: width in container queries2022-12-29T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day69
<p>In a media query, it’s obvious what <em>width</em> means. It always refers to the width of the viewport. With size container queries it’s not that obvious.</p><pre><code class="language-css">/* Kicks in when the viewport has a minimum width of 500px */
@media (min-width: 500px) {
body {
background-color: hotpink;
}
}</code></pre>
<p><code>width</code> in a size container query queries the width of the container's content box. Let me illustrate what that means with an example.</p>
<p>The <code><section></code> is the container and the <code>.card</code> changes background color when the container has a minimum width of 500px.</p>
<pre><code class="language-html"><section>
<h2>Latest news</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section></code></pre>
<p>The <code><section></code> has an explicit width, padding, and a border. Its <code>box-sizing</code> property is set to the default value <code>content-box</code>.</p>
<pre><code class="language-css">section {
box-sizing: content-box;
container-type: inline-size;
width: 500px;
padding: 20px;
border: 10px solid;
}
@container (min-width: 500px) {
.card {
background-color: hotpink;
}
}</code></pre>
<p>The total width of the container (<code><section></code>) is 560px (500px <code>width</code> + 40px <code>padding</code> + 20px <code>border</code>). The container query fires when the width without padding and border (<code>content-box</code>) is at least 500px, not when the total width is 500px. </p>
<div class="highlight">
<p>You can grab and resize the <code><section></code> by clicking and dragging it in the bottom right corner. The background color of the <code>.card</code> changes as soon as the width of the parent section hits <code>500px</code>.</p>
</div>
<style>
[data-sample] section {
width: 500px;
container-type: inline-size;
border: 10px solid;
padding: 20px;
resize: horizontal;
overflow: auto;
box-sizing: content-box;
max-width: calc(100% - 60px);
}
.sample2 section {
box-sizing: border-box;
max-width: 100%;
}
[data-sample] .card {
background-color: yellow;
border: 5px solid;
padding: 1rem;
margin: 1rem;
}
[data-sample] h2 {
margin: 1rem;
}
[data-sample] .card h2 {
background: none;
}
@container (min-width: 500px) {
[data-sample] .card {
background-color: hotpink;
}
}
</style>
<div data-sample="demo">
<section>
<h2>total width: 560px,<br>content-box: 500px</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section>
</div>
<p>If you change <code>box-sizing: content-box;</code> to <code>box-sizing: border-box;</code>, the total width of the container is 500px (440px <code>width</code> + 40px <code>padding</code> + 20px <code>border</code>). The container query still only fires when the content-box is at least 500px.</p>
<pre><code class="language-css">section {
box-sizing: border-box;
container-type: inline-size;
width: 500px;
padding: 20px;
border: 10px solid;
}
@container (min-width: 500px) {
.card {
background-color: hotpink;
}
}</code></pre>
<div data-sample="demo" class="sample2">
<section>
<h2>total width: 500px,<br>content-box: 460px</h2>
<div class="card">
<h2>Hey, I'm a card!</h2>
</div>
</section>
</div>
<script>
const sections = document.querySelectorAll('[data-sample] section');
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
entry.target.querySelector('h2').innerHTML =
`total width: ${Math.round(entry.target.getBoundingClientRect().width)}px,<br>
content-box: ${Math.round(entry.contentRect.width)}px`
}
console.log('Size changed');
});
resizeObserver.observe(sections[0]);
resizeObserver.observe(sections[1]);
</script>
<p>So, when we talk about width in a size container query, it always refers to the size of the <a href="https://w3c.github.io/csswg-drafts/css-sizing/#valdef-box-sizing-content-box">content-box</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+69%3A+width+in+container+queries%E2%80%9D">blog@matuzo.at</a>.</p> Day 70: the defined pseudo-class2022-12-30T00:00:00+00:00https://www.matuzo.at/blog/2022/100daysof-day70
<p><code>:defined</code> represents any element that has been <em>defined</em>. This includes standard elements and custom elements that have been successfully defined.</p><style>
[data-sample] :defined {
background-color: #000;
color: #fff;
}
</style>
<script>
class BasicComponent extends HTMLElement {
constructor() {
super();
}
}
customElements.define('de-fined', BasicComponent);
</script>
<pre><code class="language-css">:defined {
background-color: #000;
color: #fff;
}</code></pre>
<h2>Standard elements</h2>
<pre><code class="language-html"><p>I'm defined</p>
<small>I'm defined</small></code></pre>
<div data-sample="demo">
<p>I'm defined</p>
<small>I'm defined</small>
</div>
<h2>Undefined custom elements</h2>
<pre><code class="language-html"><unde-fined>I'm not defined</unde-fined></code></pre>
<div data-sample="demo">
<unde-fined>I'm not defined</unde-fined>
</div>
<h2>Defined custom elements</h2>
<pre><code class="language-html"><de-fined>I'm defined</de-fined></code></pre>
<pre><code class="language-js">class BasicComponent extends HTMLElement {
constructor() {
super();
}
}
customElements.define('de-fined', BasicComponent);</code></pre>
<div data-sample="demo">
<de-fined>I'm defined</de-fined>
</div>
<h2>Deprecated elements</h2>
<pre><code class="language-html"> <center>test</center>
<font>test</font>
<blink>test</blink></code></pre>
<div data-sample="demo">
<center>test</center>
<font>test</font>
<blink>test</blink>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+70%3A+the+defined+pseudo-class%E2%80%9D">blog@matuzo.at</a>.</p> Day 71: the masonry keyword2023-01-02T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day71
<p>The <code>masonry</code> keyword allows you to create masonry grid layouts.</p><p>If you want to create a masonry layout, all you have to do is turn one of the grid axes into a masonry axis using the <code>masonry</code> keyword.</p>
<pre><code class="language-html"><ol>
<li>
<a href="/blog/2023/100daysof-day70/">
Day 70: the defined pseudo-class
</a>
</li>
<li>
<a href="/blog/2022/100daysof-day69/">
Day 69: width in container queries
</a>
</li>
<li>
<a href="/blog/2022/100daysof-day68/">
Day 68: cascade layers and browser support
</a>
</li>
…
</ol></code></pre>
<pre><code class="language-css">ol {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
grid-template-rows: masonry;
}</code></pre>
<p>This simple layout turns into…</p>
<p><a style="display: block" href="/images/100days-71-1.jpg"></p>
<img alt="A 7-column grid. All items in a row have the same height. They're as large as the largest item in the row." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day71/24294ba855-1698950552/100days-71-1.jpg">
</a>
<p>…this <em>“so much better”</em> layout.</p>
<p><a style="display: block" href="/images/100days-71-2.jpg"></p>
<img alt="A 7-column grid. All items have a different height. They're only as large as they have to be." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day71/8a2f427d5d-1698950553/100days-71-2.jpg">
</a>
<p>You can also turn the other axis into the masonry axis.</p>
<pre><code class="language-css">ol {
display: grid;
gap: 1rem;
grid-template-columns: masonry;
grid-template-rows: repeat(auto-fill, 6rem);
}</code></pre>
<p><a style="display: block" href="/images/100days-71-3.jpg"></p>
<img alt="A 4-rows grid. Each item in each row is as wide as its content." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day71/2516ed4fe4-1698950553/100days-71-3.jpg">
</a>
<h3>Accessibility</h3>
<p>By default, the packing algorithm puts items into the column with the most space. This may cause a disconnect between <abbr title="document object model">DOM</abbr> and tabbing order, which can be an issue for keyboard and screen reader users. Test your masonry grids sufficiently.</p>
<h3>Browser support</h3>
<p>Why am I showing screenshots instead of rendering the result directly? Well, because Firefox is the only browser that supports the keyword at the moment and the feature is still behind a flag. You can enable it by visiting <code>about:config</code> and setting <code>layout.css.grid-template-masonry-value.enabled</code> to <code>true</code>.</p>
<p>Can you use it today? Yeah, why not?! Most users will still get a grid, it's just not a masonry layout.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+71%3A+the+masonry+keyword%E2%80%9D">blog@matuzo.at</a>.</p> A year in review: 20222023-01-03T00:00:00+00:00https://www.matuzo.at/blog/2023/a-year-in-review-2022
<p>I started my last year in review post with the words “2021 was wild!”. If 2021 was wild, then I don’t know what 2022 was because so much stuff was going on last year.</p><h2>Personal</h2>
<p>Our daughter was born in 2021, and suddenly she’s 19 months old. It’s killing me how fast time flies. It’s still amazing to watch her grow and learn new things. Looking at photos from early last year baffles me because she looks so different. It’s almost like I’m looking at another or multiple other persons. I’m really glad that I managed to find enough time for her. I still don’t work on weekends and evenings and I try to be home early every day, which allows me to spend a lot of time with her.</p>
<p>Some of my personal highlights this year were a weekend trip to Italy (Venice and Chioggia), our 4 week trip to Greece (Peloponnese and Athens), our road trip to Germany and The Netherlands (Freiburg, Utrecht, Rotterdam, Amsterdam), and of course moving to Graz. </p>
<div class="image-grid">
<img alt="My daughter and fiance playing with toys on a beach in Italy in October." height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/2cc5808516-1698950544/IMG_20220326_161141_thumb.jpg" width="436">
<img alt="Our fully packed van for our trip to Greece" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/43f486a84b-1698950544/IMG_20220520_163216_thumb.jpg" width="436">
<img alt="Me lying clothed on a beach and my daughter lying on top of me looking at me" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/7773062873-1698950544/IMG_20220910_171323_thumb.jpg" width="436">
<img alt="A beautiful beach in Greece with crystal clear water" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/9373da86cf-1698950544/IMG_20220613_111318_thumb.jpg" width="436">
</div>
<p>In early 2022, we were looking for a house or flat in Graz, the second largest city in Austria. In June we found a nice place, said “Yes” with no hesitation, and moved in in August. Everything went so quick and now we’re already 5 months here. I like it here. It’s much smaller and calmer than Vienna, but nice.</p>
<div class="image-grid">
<img alt="Again our fully packed van but this time with furniture" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/04d6c9ce03-1698950543/IMG_20220729_094633_thumb.jpg" width="436">
<img alt="First selfie of me, my fiance and our daughter in our new home" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/7ee1118706-1698950544/IMG_20220728_155617_thumb.jpg" width="436">
</div>
<p>This year I ran the first part of the relay marathon at the Vienna City Marathon. I did 5:14/km at a distance of 15.5km and I added another 5 kilometers for fun, which I ran in 6:43/km. I’m pretty happy with the result and I hope I can beat it this year.</p>
<p>What didn’t go well? I still didn’t have enough time and energy left for the rest of my family and my friends. I really want to fix that this year. Also, I have to make room for reading. I didn’t read a single book last year, and newspapers not as much as I would like to. </p>
<p>I was able to maintain my body weight more or less, but I actually wanted to lose some weight. Today I have ~90kg. I’d love to get down to 85kg.</p>
<p>The pandemic didn't hit me as much as other people, but I can feel how the general situation (war in Ukraine, covid, supression in Iran, increasing worldwide poverty, the rise of far right movements, climate crisis, etc.) effects me emotionally. I'm in no position to complain, though. I understand how priviledged I am.</p>
<h2>Work</h2>
<p>This year working for the City of Vienna was really busy. We had so much to do and a lot of internal discussions on how to do things. It was stressful, but I enjoyed it. The most notable event was our decision to switch to web components. I’ve spent a great deal of 2022 writing web components and I’ve learned a lot about JavaScript, reusability, and scalability.</p>
<p>December 31st was the last day for me working for the City of Vienna as a regular employee. I’ve worked for almost 4 years for Wien Holding Media and the Presse- and Informationsdienst (MA 53). My original plan was to give it a shot and quit after 2 years if it gets too boring, but it never did. I had amazing colleagues, interesting tasks, a lot of freedom in many aspects, and a boss you can only wish for. Moving to Graz didn’t just mark a new personal, but also a new professional chapter in my life. I’ll keep working with the City now and then, but starting 2023 I want to focus on new tasks (by the way, if you want to work with me, <a href="/about-me/">get in touch</a>. :))</p>
<p>Besides my work for the city, I did a couple of accessibility audits, for example, for the Burgtheater in Vienna. I’ve helped an agency consult the Vienna University of Technology, and I did some workshops. Two online and one in-person workshops for Smashing Magazine and an online workshop with Shopify.</p>
<p>Objectively speaking, you can say that I worked too much last year, but doing less makes me unhappy. It sounds silly, but I need this sometimes stressful, face-paced, always busy environment in order to feel good. After making bad experiences with doing too much of the wrong kind of work a couple of years ago, I feel pretty confident that I can now judge when it becomes too much for me.</p>
<p>This year I want to focus more on teaching (through my blog, HTMHell and workshops) and consulting. If there’s one thing I can do to make the web a better place, it’s teaching others how to make inclusive websites.</p>
<h2>Speaking</h2>
<p>In 2022 I could check off so my things from my bucket list. I wrote a new talk called “<a href="https://noti.st/matuzo/hyO5e3/lost-in-translation">Lost in Translation</a>” and I presented it at Beyond Tellerrand, Smashing Conf in Freiburg, Fronteers, and Paris Web. </p>
<p>Here's a recording of my talk:</p>
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/Wno1IhEBTxc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
<p>After 2 years without conferences, it felt so great to meet some of my friends again in person and other friends I made online for the first time.</p>
<p>In Düsseldorf, I met Stephanie Eckles, Marc Thiele, Joschi Kuphal, Matthias Ott, Niels Binder, Marco Hengstenberg, Oliver Schöndorfer, Gunnar Bittersmann, Marc Hinse, Maik, Robert Weber, Daniel Wentsch, Thomas Semmler, Reinhard Steudel, Vasilis van Gemert, and many more. In Freiburg, I met Andy Bell, Michelle Barker, Charis and Jarijn, Vitaly, Ari, Amanda, Iris, Samuel Snopko, Nathan Curtis, Harry Roberts, Barry Pollard, and many more. In Utrecht, I met Bramus van Damme, Schepp, Jewwy Qadri, Michael Hastrich, Niels Leenheer, Roudy Rabouw, Floor Drees, Erik Kroes, and many more. In Paris, I met Ahmad Shadeed, Gaël Poupard, Elie Sloïm, Cassie Evans, Luc Poupard, Nicolas Hoizey, and many more.</p>
<p>I know that listing names is a stupid idea because I’m sure that I forgot someone, but it just makes me happy to visualize how many amazing people I’ve met.</p>
<div class="image-grid">
<img alt="Me standing and smilie in front of a poster made for my talk" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/915dd93f6c-1698950544/IMG_20220502_160603_thumb.jpg" width="436">
<img alt="Selfie of Daniel Wentsch, me and Oliver Schöndorfer in front of a bar that uses the Lobster font in their logo" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/0e87f1622c-1698950544/IMG_20220503_133915_thumb.jpg" width="436">
<img alt="Breakfast with Bramus, Schepp, my fiance, our daughter, and Niels" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/15a458fdba-1698950544/IMG_20220910_100951_thumb.jpg" width="436">
<img alt="Selfie of Ahmad Shadeed and me" height="245" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/a-year-in-review-2022/584044e154-1698950544/paris-web-ahmad-and-manuel_thumb.jpg" width="436">
</div>
<p>On top of that, I’ve taught at FH Joanneum in Graz for the first time (thanks for the opportunity Eric) and I gave talks at UX Graz online and at the Accessibility Club in Düsseldorf.</p>
<h2>Side projects</h2>
<p>My two most notable side projects in 2023 were the <a href="https://www.htmhell.dev/adventcalendar/">HTMHell advent calendar</a> and my <a href="https://www.matuzo.at/blog/2022/100-days-of-more-or-less-modern-css/">#100DaysOfMoreOrLessModernCSS</a>. I’m really proud of how both have turned out.</p>
<p>In 2021 I’ve started the <a href="https://www.htmhell.dev/newsletter/">HTMHell newsletter</a>, as well as <a href="https://www.youtube.com/@HTMHell/videos">recording videos</a> for HTMHell. I've worked on both things in 2022, but not as much as I would’ve liked to. I hope I can change that in 2023. </p>
<h2>Articles</h2>
<p>I've written 90 blog posts this year, which means on average 7.5 per month.</p>
<p>If I had to pick 3 favorite articles, I'd go with:</p>
<ul>
<li><a href="/blog/2022/specificity/">CSS Specificity Demo</a> on my blog</li>
<li><a href="https://web.dev/website-navigation/">Building the main navigation for a website</a> on web.dev</li>
<li><a href="https://www.smashingmagazine.com/2022/04/accessible-filterable-paginated-list-11ty-alpinejs/">How To Build A Progressively Enhanced, Accessible, Filterable And Paginated List</a> on Smashing Magazine</li>
</ul>
<h2>Misc</h2>
<p>My two favorite albums in 2022 were Joy as an Act of Resistance by IDLES and Sometimes I Might be Introvert by Little Simz (both not released in 2022).</p>
<p>My top 5 songs:</p>
<ul>
<li><a href="https://youtu.be/QkF_G-RF66M">IDLES - Danny Nedelko</a></li>
<li><a href="https://youtu.be/tvY31eN3gtE">Little Simz - Point and Kill</a></li>
<li><a href="https://youtu.be/mr9nfTqfQeA">Joyner Lucas - Zim Zimma</a></li>
<li><a href="https://youtu.be/hrLYmieiyMo">Retrogott - Show Time</a></li>
<li><a href="https://youtu.be/TyA-oz7lSrc">Manu Chao - Clandestino</a></li>
</ul>
<p>My favorite podcasts were <a href="https://darknetdiaries.com/">Darknet Diaries</a> and <a href="https://www.iheart.com/podcast/105-behind-the-bastards-29236323/">Behind the Bastards</a>.</p>
<h2>Plans for 2023</h2>
<p>Looking at my goals from 2022, I can say that I was really bad with achieving my goals last year.</p>
<ul>
<li>🆗 Work less and enjoy life with my family and friends</li>
<li>❌ Spend more time on writing for HTMHell</li>
<li>❌ Record videos for HTMHell</li>
<li>❌ Redesign my personal website</li>
<li>✅ Finally update my profile pic</li>
</ul>
<p>For 2023 I want to add:</p>
<ul>
<li>Read at least 2 books</li>
<li>Lose 5 kgs of body weight.</li>
<li>Stopping eating breakfast and lunch in front of the computer, ffs.</li>
</ul>
<p>Thanks to everyone who helped make my year 2022 as amazing and exciting as it was. I'm looking forward to 2023! 🤗🥳</p>
<h2>Reviews by other people</h2>
<p>If you enjoy reading review posts as much as I do, you might want to checkout reviews by other people in the web dev community.</p>
<ul class="list">
<li>
<a href="https://hidde.blog/2022-review/">
2022 in review by Hidde de Vries
</a>
</li>
<li>
<a href="https://meiert.com/en/blog/re-2022/">2022 by Jens Oliver Meiert</a>
</li>
<li>
<a href="https://andy-bell.co.uk/wrapping-up-2022/">Wrapping up 2022 by Andy Bell</a>
</li>
<li>
<a href="https://ishadeed.com/journal/2022/">2022 year in review by Ahmad Shadeed</a>
</li>
<li>
<a href="https://yatil.net/blog/2022">2022 It happened. by Eric Eggert</a>
</li>
<li>
<a href="https://dnikub.dev/blog/year-review-2022/">My year in review – 2022 by Daniela Kubesch</a>
</li>
<li>
<a href="https://thinkdobecreate.com/articles/my-2022-in-review/">My 2022 In Review by Stephanie Eckles</a>
</li>
<li>
<a href="https://alistairshepherd.uk/writing/2022-roundup/">
My 2022 round-up by Alistair Shepherd
</a>
</li>
<li>
<a href="https://robbowen.digital/wrote-about/so-long-2022/">
So long, 2022. by Robb Owen
</a>
</li>
<li>
<a href="https://melanie-richards.com/blog/2022-review/">
2022 in Review by Melanie Richards
</a>
</li>
<li>
<a href="https://pawelgrzybek.com/a-look-back-at-2022/">
A look back at 2022 by Pawel Grzybek
</a>
</li>
<li>
<a href="https://www.oidaisdes.org/looking-back-at-2022.en">
Looking Back at 2022 — My First Year of Blogging by Alexander Lehner
</a>
</li>
<li>
<a href="https://rachelandrew.co.uk/archives/2023/01/01/2022-in-review/">
2022 in review by Rachel Andrew
</a>
</li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CA+year+in+review%3A+2022%E2%80%9D">blog@matuzo.at</a>.</p> Day 72: the masonry-auto-flow property2023-01-03T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day72
<p>If you’re creating a masonry layout, the packing algorithm puts items into the column with the most space by default. This can cause accessibility issues. The <code>masonry-auto-flow</code> property gives us control over the automatic placement of items in a masonry layout.</p><p>In the following layout, you can see how the tile for “Day 63” is placed in the middle column. Since day 64 is in the last column of the first row, you’d expect day 63 to be in the first column of the second row. If you compare the heights of the items in the first column, you can see that the middle column is the one with the most available space. That’s why the algorithm placed day 63 there. </p>
<pre><code class="language-css">ol {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
grid-template-rows: masonry;
}</code></pre>
<p><a style="display: block" href="/images/100days-72-1.jpg"></p>
<img alt="A 7-column grid. Only the placement for items in the first row matches DOM order. The rest is random." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day72/cd434fdc40-1698950538/100days-72-1.jpg">
</a>
<p>It’s hard to orientate and understand how the layout is structured, if the placement is completely random. By setting <code>masonry-auto-flow</code> to <code>next</code>, we can instruct the browser to place items one after the other on the grid axis.</p>
<pre><code class="language-css">ol {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
grid-template-rows: masonry;
masonry-auto-flow: next;
}</code></pre>
<p><a style="display: block" href="/images/100days-72-2.jpg"></p>
<img alt="A 7-column grid. Only the placement for items in the first row matches DOM order. The rest is random." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day72/e52346a5df-1698950538/100days-72-2.jpg">
</a>
<p class="highlight">
<strong>Note:</strong> Firefox is the only browser that supports the keyword at the moment and the feature is still behind a flag. You can enable it by visiting `about:config` and setting `layout.css.grid-template-masonry-value.enabled` to `true`. </p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+72%3A+the+masonry-auto-flow+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 73: size container features2023-01-04T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day73
<p>In my previous posts about size container features I’ve only used the <code>min-width</code> feature, but there’s actually more you can query.</p><p><code>container-type: inline-size</code> establishes size containment only on the inline axis. There is no <code>block-size</code> option because it wasn’t possible for browsers to implement, but there is a <code>size</code> option, which establishes size containment on both dimensions of the container. According to Miriam Suzanne, you should be careful using this option because I may cause side effects, but it allows you to query more than just the width/inline-size.</p>
<h2>orientation</h2>
<p>You can query the orientation of the container. If the height is larger than the width, the <code>orientation</code> is <code>portrait</code>. If the width is larger than the height, it's <code>landscape</code>.</p>
<style>
[data-sample] {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: start;
}
[data-sample] .container {
border: 8px solid aqua;
container-type: size;
width: 10rem;
height: 15rem;
}
[data-sample] .container2 {
width: 15rem;
height: 10rem;
}
[data-sample] .child {
aspect-ratio: 1;
width: 5rem;
border: 4px solid;
color: red;
}
@container (orientation: portrait) {
[data-sample] .child {
background: currentColor;
}
}
[data-sample] .container3 {
width: 10rem;
height: 10rem;
}
[data-sample] .container4 {
width: 10rem;
height: 5.625rem;
box-sizing: content-box;
}
@container (aspect-ratio: 1 / 1) {
.sample2 .child {
background: blue;
}
}
@container (aspect-ratio: 16 / 9) {
.sample2 .child {
background: green;
}
}
@container (min-height: 14rem) {
.sample3 .child {
background: fuchsia;
}
}
@container (min-block-size: 14rem) {
.sample4 .child {
background: aqua;
}
}
</style>
<pre><code class="language-css">.container {
border: 8px solid aqua;
container-type: size;
width: 10rem;
height: 15rem;
}
.container2 {
width: 15rem;
height: 10rem;
}
.child {
aspect-ratio: 1;
width: 5rem;
border: 4px solid;
color: red;
}
@container (orientation: portrait) {
.child {
background: currentColor;
}
}</code></pre>
<pre><code class="language-html"><div class="container">
<div class="child"></div>
</div>
<div class="container container2">
<div class="child"></div>
</div></code></pre>
<div data-sample="demo: orientation" class="sample1">
<div class="container">
<div class="child">
</div>
</div>
<div class="container container2">
<div class="child">
</div>
</div>
</div>
<h1>aspect-ratio</h1>
<pre><code class="language-css">.container3 {
width: 10rem;
height: 10rem;
}
.container4 {
width: 10rem;
height: 5.625rem;
box-sizing: content-box;
}
@container (aspect-ratio: 1 / 1) {
.child {
background: blue;
}
}
@container (aspect-ratio: 16 / 9) {
.child {
background: green;
}
}</code></pre>
<div data-sample="demo: aspect-ratio" class="sample2">
<div class="container container3">
<div class="child">
</div>
</div>
<div class="container container4">
<div class="child">
</div>
</div>
</div>
<h2>height</h2>
<p>You can also query the height.</p>
<pre><code class="language-css">@container (min-height: 14rem) {
.child {
background: fuchsia;
}
}</code></pre>
<div data-sample="demo: aspect-ratio" class="sample3">
<div class="container container1">
<div class="child">
</div>
</div>
<div class="container container2">
<div class="child">
</div>
</div>
</div>
<h2>logical properties</h2>
<p>Instead of <code>width</code> you can also use <code>inline-size</code> in your queries and instead of <code>height</code> you can use <code>block-size</code>.</p>
<pre><code class="language-css">@container (min-block-size: 14rem) {
.sample4 .child {
background: aqua;
}
}</code></pre>
<div data-sample="demo: aspect-ratio" class="sample4">
<div class="container container1">
<div class="child">
</div>
</div>
<div class="container container2">
<div class="child">
</div>
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+73%3A+size+container+features%E2%80%9D">blog@matuzo.at</a>.</p> Day 74: using !important in cascade layers2023-01-05T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day74
<p>In order to understand how <code>!important</code> works in cascade layers, you have to understand how <code>!important</code> works generally. The conclusion of this post might not be what you expect.</p><style>
.sample1 h1 {
color: green;
}
.sample1 h1 {
color: red;
}
.sample2 h1 {
color: green !important;
}
.sample2 h1 {
color: red;
}
@layer base {
.sample3 h1 {
color: green;
}
}
@layer components {
.sample3 h1 {
color: red;
}
}
.sample4 h1 {
color: blue;
}
@layer base {
.sample4 h1 {
color: green;
}
}
@layer components {
.sample4 h1 {
color: red;
}
}
.sample5 h1 {
color: blue;
}
@layer base {
.sample5 h1 {
color: green !important;
}
}
@layer components {
.sample5 h1 {
color: red;
}
}
.sample6 h1 {
color: blue;
}
@layer base {
.sample6 h1 {
color: green !important;
}
}
@layer components {
.sample6 h1 {
color: red !important;
}
}
.sample7 h1 {
color: blue !important;
}
@layer base {
.sample7 h1 {
color: green !important;
}
}
@layer components {
.sample7 h1 {
color: red !important;
}
}
</style>
<h2>The basics</h2>
<p>Let's start nice and easy. We have two declarations with the same specificity. The second one wins because it comes later in the document.</p>
<pre><code class="language-css">h1 {
color: green;
}
h1 {
color: red;
}</code></pre>
<div data-sample="demo 1" class="sample1">
<h1>I'm red</h1>
</div>
<p>Adding <code>!important</code> to the first declaration increases its specificity, turning the color green.</p>
<pre><code class="language-css">h1 {
color: green !important;
}
h1 {
color: red;
}</code></pre>
<div data-sample="demo 2" class="sample2">
<h1>I'm green</h1>
</div>
<p>So far, so good. I assume that most of you already knew that. Now let’s have a look at cascade layers and what happens if we use <code>!important</code> in layers.</p>
<h2>Layers</h2>
<p>We have two layers, each with a declaration with the same specificity. The second declaration wins because it’s in a layer defined later in the document.</p>
<pre><code class="language-css">@layer base {
h1 {
color: green;
}
}
@layer components {
h1 {
color: red;
}
}</code></pre>
<div data-sample="demo 3" class="sample3">
<h1>I'm red</h1>
</div>
<p>If we add un-layered styles, the color turns blue because un-layered styles have precedence over layered styles.</p>
<pre><code class="language-css">h1 {
color: blue;
}
@layer base {
h1 {
color: green;
}
}
@layer components {
h1 {
color: red;
}
}</code></pre>
<div data-sample="demo 4" class="sample4">
<h1>I'm blue</h1>
</div>
<p>If we add <code>!important</code> to the declaration in the base layer, the color turns green.</p>
<pre><code class="language-css">h1 {
color: blue;
}
@layer base {
h1 {
color: green !important;
}
}
@layer components {
h1 {
color: red;
}
}</code></pre>
<div data-sample="demo 5" class="sample5">
<h1>I'm green</h1>
</div>
<p>Okay, I have to stop here for a moment. All this makes sense to me. Here’s what I thought happens before I wrote this blog post:</p>
<p>By default, our order of precedence is like this:</p>
<ol>
<li>Un-layered styles (most important)</li>
<li>components layer</li>
<li>base layer (least important)</li>
</ol>
<p>Demo 3 and 4 confirm that.</p>
<p>If we add <code>!important</code> to the declaration in the base layer, our order looks like this:</p>
<ol>
<li>!important base layer (most important)</li>
<li>Un-layered styles</li>
<li>components layer</li>
<li>base layer (least important)</li>
</ol>
<p>Demo 5 confirms that.</p>
<p>If I keep extending my logic, this would mean that if we add <code>!important</code>to the components layer, the order looks like this:</p>
<ol>
<li>!important components layer (most important)</li>
<li>!important base layer</li>
<li>Un-layered styles</li>
<li>components layer </li>
<li>base layer(least important)</li>
</ol>
<p>Let’s try!</p>
<pre><code class="language-css">h1 {
color: blue;
}
@layer base {
h1 {
color: green !important;
}
}
@layer components {
h1 {
color: red !important;
}
}</code></pre>
<div data-sample="demo 6" class="sample6">
<h1>I'm green</h1>
</div>
<p>Nooope, not red, still green. To explain why, let's have a look at the spec.</p>
<blockquote>“CSS attempts to create a balance of power between author and user style sheets. By default, rules in an author’s style sheet override those in a user’s style sheet, which override those in the user-agent’s default style sheet. To balance this, a declaration can be marked important, which increases its weight in the cascade and <strong>inverts the order of precedence</strong>” <em>(emphasis mine)</em>.</blockquote>
<p><a href="https://www.w3.org/TR/css-cascade-3/#importance">CSS Cascading and Inheritance Level 3</a></p>
<p>This means that <code>!important</code> doesn't just increase the weight of a declaration in the cascade, but it inverts the order of precedence. So, in our first basic example <code>!important</code> doesn't just make the first declaration more important, no, it inverts the order of precedence!</p>
<pre><code class="language-css">h1 {
color: green !important;
}
h1 {
color: red;
}</code></pre>
<p>If we make the first declaration important, this…</p>
<ol>
<li>h1 color red (most important)</li>
<li>h1 color green (least important)</li>
</ol>
<p>…becomes this:</p>
<ol>
<li>h1 color green (most important)</li>
<li>h1 color red (least important)</li>
</ol>
<p>Before cascade layers, this didn’t really matter, but now, with multiple layers on the author level, understanding this concept is critical. If we apply this logic to our last layer example, we get this:</p>
<ol>
<li>!important base layer (most important)</li>
<li>!important components layer</li>
<li>!important un-layered styles</li>
<li>Un-layered styles</li>
<li>components layer </li>
<li>base layer(least important)</li>
</ol>
<p>So, even if we add <code>!important</code> to the un-layered styles, the declaration in the base layer still wins.</p>
<pre><code class="language-css">h1 {
color: blue !important;
}
@layer base {
h1 {
color: green !important;
}
}
@layer components {
h1 {
color: red !important;
}
}</code></pre>
<div data-sample="demo 7" class="sample7">
<h1>I'm still green</h1>
</div>
<p>Got it? No? Don't worry. It took me more than an hour to understand it. This video by Una Kravets helped a lot:</p>
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/dS123IXPcJ0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+74%3A+using+%21important+in+cascade+layers%E2%80%9D">blog@matuzo.at</a>.</p> Day 75: font palettes2023-01-06T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day75
<p>Apparently, multicolored typefaces on the web are a thing. You can use and modify them in CSS.</p><style>
@font-face {
font-family: 'Rocher';
src: url('/blog/2023/100daysof-day76/RocherColorGX.woff2');
}
@font-palette-values --pink {
font-family: 'Rocher';
base-palette: 1;
}
@font-palette-values --green {
font-family: 'Rocher';
base-palette: 2;
}
@font-palette-values --gray {
font-family: 'Rocher';
base-palette: 9;
}
[data-sample] h1 {
font-family: 'Rocher';
font-size: 6rem;
margin: 0;
}
.sample2 h1 {
font-palette: --pink;
}
.sample3 h1 {
font-palette: --green;
}
.sample4 h1 {
font-palette: --gray;
}
</style>
<pre><code class="language-css">@font-face {
font-family: 'Rocher';
src: url('/fonts/RocherColorGX.woff2');
}
h1 {
font-family: "Rocher";
}</code></pre>
<div data-sample="demo">
<h1>woah!</h1>
</div>
<p>Pretty cool, right? What’s even cooler is that color fonts come with a default color palette and optionally with a set of alternative palettes that you can access via CSS.</p>
<figure>
<img alt="11 palette each with 4 colors" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day75/4a755c25dd-1698950553/100days-75-1.jpg">
<figcaption>The font “Rocher” comes with 11 palettes</figcaption>
</figure>
<p>In order to use a different palette, you have to reference and associate it with a font using the <code>@font-palette-value</code> rule. Within the rule, you assign a palette using the <code>base-palette</code> property. The value is an index, starting at 0 (default palette). Rocher comes with 11 palettes, which means that you can assign values between 0 and 10.</p>
<pre><code class="language-css">@font-palette-values --pink {
font-family: 'Rocher';
base-palette: 1;
}
@font-palette-values --green {
font-family: 'Rocher';
base-palette: 2;
}
@font-palette-values --gray {
font-family: 'Rocher';
base-palette: 9;
}</code></pre>
<p>To use a palette, you use the <code>font-palette</code> property and reference the name you’ve defined in the <code>@font-palette-values</code> rule (You pick the name, it's not predefined).</p>
<pre><code class="language-css">h1 {
font-palette: --pink;
}</code></pre>
<div data-sample="demo" class="sample2">
<h1>woah!</h1>
</div>
<pre><code class="language-css">h1 {
font-palette: --green;
}</code></pre>
<div data-sample="demo" class="sample3">
<h1>woah!</h1>
</div>
<pre><code class="language-css">h1 {
font-palette: --gray;
}</code></pre>
<div data-sample="demo" class="sample4">
<h1>woah!</h1>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+75%3A+font+palettes%E2%80%9D">blog@matuzo.at</a>.</p> Day 76: overwriting colors in font palettes2023-01-09T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day76
<p>You can use the <code>override-colors</code> property to override colors in a font palette.</p><p>Color fonts come with one or more predefined color palettes. You can select them by using the <code>font-palette</code> property. You can also define your own color palettes or change specific colors in a palette using the <code>override-colors</code> property.</p>
<style>
@font-face {
font-family: 'Rocher';
src: url('/blog/2023/100daysof-day76/RocherColorGX.woff2');
}
[data-sample] h1 {
font-family: 'Rocher';
font-size: 6rem;
margin: 0;
}
@font-palette-values --custom {
font-family: 'Rocher';
override-colors: 0 #a13908;
}
@font-palette-values --custom-single-2 {
font-family: 'Rocher';
override-colors:
2 #a13908;
}
@font-palette-values --custom-all {
font-family: 'Rocher';
override-colors:
0 rgb(21, 58, 81),
1 rgb(255 215 0),
2 rgb(84 159 167),
3 rgb(128, 210, 219);
}
@font-palette-values --custom-base {
font-family: 'Rocher';
base-palette: 1;
override-colors: 0 #9e4356;
}
.custom {
font-palette: --custom;
}
.custom-single-2 {
font-palette: --custom-single-2;
}
.custom-all {
font-palette: --custom-all;
}
.custom-base {
font-palette: --custom-base;
}
</style>
<pre><code class="language-css">@font-face {
font-family: 'Rocher';
src: url('/fonts/RocherColorGX.woff2');
}
h1 {
font-family: "Rocher";
}</code></pre>
<p>That's the default font palette of the “Rocher” font.</p>
<div data-sample="demo - default">
<h1>Jurassic 5</h1>
</div>
<p>Using the <code>@font-palette-value</code> rule you can create a new font palette. You reference the typeface you want to create the palette for using the <code>font-family</code> property. Color palettes of the “Rocher” font consist of 4 colors. We can override colors by defining the index (starting with 0) of the color and a valid color value.</p>
<pre><code class="language-css">@font-palette-values --custom {
font-family: 'Rocher';
override-colors: 0 #a13908;
}
h1 {
font-palette: --custom;
}</code></pre>
<div data-sample="demo">
<h1 class="custom">The Pharcyde</h1>
</div>
<p>You don't have to start at 0, you can override any color.</p>
<pre><code class="language-css">@font-palette-values --custom {
font-family: 'Rocher';
override-colors: 2 #a13908;
}</code></pre>
<div data-sample="demo">
<h1 class="custom-single-2">Del the Funky Homosapien</h1>
</div>
<p>Here's a custom palette based on the colors of my code syntax highlighter.</p>
<pre><code class="language-css">@font-palette-values --custom {
font-family: 'Rocher';
override-colors:
0 rgb(21, 58, 81),
1 rgb(255 215 0),
2 rgb(84 159 167),
3 rgb(128, 210, 219);
}</code></pre>
<div data-sample="demo">
<h1 class="custom-all">A Tribe Called Quest</h1>
</div>
<p>You can also use another base palette and override colors.</p>
<pre><code class="language-css">@font-palette-values --custom {
font-family: 'Rocher';
base-palette: 1;
override-colors: 0 #9e4356;
}</code></pre>
<div data-sample="demo">
<h1 class="custom-base">MF DOOM</h1>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+76%3A+overwriting+colors+in+font+palettes%E2%80%9D">blog@matuzo.at</a>.</p> Day 77: block-size, inline-size, vi, and vb2023-01-10T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day77
<p>There are logical properties for width and height values.</p><h2>width and height</h2>
<p>The logical alternative for <code>width</code> is <code>inline-size</code> and the alternative for <code>height</code> is <code>block-size</code>. Here’s an example of how using <code>inline-size</code> over <code>width</code> makes a difference.</p>
<style>
[data-sample] h1 {
border: 1rem solid;
padding: 1rem;
}
[data-sample] h1:not(:last-child) {
margin-bottom: 2rem;
}
[data-sample] .vertical {
writing-mode: vertical-rl;
}
[data-sample] {
overflow: auto;
}
.sample1 h1 {
width: 16rem;
}
.sample2 h1 {
inline-size: 16rem;
}
.sample3 h1, .sample4 h1 {
padding: 0.5rem;
font-size: 1.4rem;
}
.sample3 h1 {
width: 20vw;
height: 40vh;
}
.sample4 h1 {
width: 20vi;
height: 40vb;
}
</style>
<p>If you use <code>width</code>, the property sets the physical width, regardless of the writing mode.</p>
<pre><code class="language-css">h1 {
border: 1rem solid;
padding: 1rem;
width: 16rem;
}
.vertical {
writing-mode: vertical-rl;
}</code></pre>
<pre><code class="language-html"><h1>width horizontal</h1>
<h1 class="vertical">width vertical</h1></code></pre>
<div data-sample="demo" class="sample1">
<h1>width horizontal</h1>
<h1 class="vertical">width vertical</h1>
</div>
<p>If you use <code>inline-size</code>, the property sets the logical width with respect to the writing mode.</p>
<pre><code class="language-css">h1 {
border: 1rem solid;
padding: 1rem;
inline-size: 16rem;
}
.vertical {
writing-mode: vertical-rl;
}</code></pre>
<div data-sample="demo" class="sample2">
<h1>inline-size horizontal</h1>
<h1 class="vertical">inline-size vertical</h1>
</div>
<h2>vi and vb</h2>
<p>There are also logical unit alternatives for <code>vw</code> and <code>vh</code>.</p>
<p>The width and height of the <code><div></code> is always the width and height of the viewport, regardless of the writing mode.</p>
<pre><code class="language-css">div {
border: 1rem solid;
width: 20vw;
height: 40vh;
}
.vertical {
writing-mode: vertical-rl;
}</code></pre>
<div data-sample="demo" class="sample3">
<h1>vw + vh</h1>
<h1 class="vertical">vw + vh</h1>
</div>
<p>The width and height of the <code><div></code> matches either the vertical or horizontal dimensions of the viewport, depending on the writing mode.</p>
<pre><code class="language-css">div {
border: 1rem solid;
width: 20vi;
height: 40vb;
}</code></pre>
<div data-sample="demo" class="sample4">
<h1>vi + vb</h1>
<h1 class="vertical">vi + vb</h1>
</div>
<ul>
<li><code>50vw</code> alternative: <code>50vi</code></li>
<li><code>50vh</code> alternative: <code>50vb</code></li>
<li><code>50dvh</code> alternative: <code>50dvb</code></li>
<li><code>50svh</code> alternative: <code>50svb</code></li>
<li><code>50lvh</code> alternative: <code>50lvb</code></li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+77%3A+block-size%2C+inline-size%2C+vi%2C+and+vb%E2%80%9D">blog@matuzo.at</a>.</p> Day 78: container query units2023-01-11T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day78
<p>Container queries come with their own units.</p><style>
[data-sample] div {
outline: 10px solid;
overflow: auto;
margin: 1rem;
}
.sample2 div {
container-type: inline-size;
}
[data-sample] h2 {
background-color: yellow;
border: 5px solid;
padding: 1rem;
margin: 1rem;
inline-size: 80cqi;
}
</style>
<p>Container query units work the same as viewport units. <code>80cqi</code> equals <code>80svi</code>.</p>
<pre><code class="language-css"> div {
outline: 10px solid;
}
h2 {
background-color: yellow;
border: 5px solid;
inline-size: 80cqi;
}</code></pre>
<div data-sample="demo">
<div>
<h2>It's me, Mike D!</h2>
</div>
</div>
<p>The big difference is that if they're inside a size container, container query units aren't relative to the viewport anymore, but to the container.</p>
<pre><code class="language-css"> div {
outline: 10px solid;
container-type: inline-size;
}
h2 {
background-color: yellow;
border: 5px solid;
inline-size: 80cqi;
}</code></pre>
<div data-sample="demo" class="sample2">
<div>
<h2>It's me, Mike D!</h2>
</div>
</div>
<table class="data">
<caption>Container Units</caption>
<thead>
<tr>
<th>unit
</th><th>relative to
</th></tr></thead><tbody>
<tr>
<td>cqw
</td><td>1% of a query container’s width
</td></tr><tr>
<td>cqh
</td><td>1% of a query container’s height
</td></tr><tr>
<td>cqi
</td><td> 1% of a query container’s inline size
</td></tr><tr>
<td>cqb
</td><td> 1% of a query container’s block size
</td></tr><tr>
<td>cqmin
</td><td>The smaller value of cqi or cqb
</td></tr><tr>
<td>cqmax
</td><td>The larger value of cqi or cqb
</td></tr></tbody></table><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+78%3A+container+query+units%E2%80%9D">blog@matuzo.at</a>.</p> CSS color functions and custom properties2023-01-12T00:00:00+00:00https://www.matuzo.at/blog/2023/hsl-custom-properties
<p>I know I’m really late to the party, but I finally understood why people find color functions like <code>hsl()</code>, <code>hwb()</code>, or <code>lab()</code> so appealing.</p><p>There are many reasons, but one of them is that in combination with custom properties, working with color functions is so much easier, cleaner, and understandable compared to working with hex colors or <code>rbg()</code>.</p>
<p>Here's an example. Let's say we have a simple status component.</p>
<style>
:is(.sample1, .sample2) [role="status"] {
--background: rgb(211 232 248);
background-color: var(--background);
padding: 1rem;
margin-block-end: 1em;
}
.sample2 [role="status"] {
--border-color: rgb(122 186 235);
border: 2px solid var(--border-color);
}
.sample3 [role="status"] {
--background-color: hsl(206deg 74% 90%);
--border-color: hsl(206deg 74% 70%);
background-color: var(--background-color);
border: 2px solid var(--border-color);
padding: 1rem;
margin-block-end: 1em;
}
.sample4 [role="status"] {
--h: 206deg;
--s: 74%;
--background-color: hsl(var(--h) var(--s) 90%);
--border-color: hsl(var(--h) var(--s) 70%);
background-color: var(--background-color);
border: 2px solid var(--border-color);
padding: 1rem;
margin-block-end: 1em;
}
.sample4 .warning {
--h: 40deg;
}
.sample4 .error {
--h: 0deg;
}
</style>
<pre><code class="language-css">[role="status"] {
--background-color: rgb(211 232 248);
background-color: var(--background-color);
padding: 1rem;
margin-block-end: 1em;
}</code></pre>
<pre><code class="language-html"><div role="status">
<strong>Information:</strong> You're logged in as “Tyler Durden”.
</div></code></pre>
<div data-role="demo" class="sample1">
<div role="status">
<strong>Information:</strong> You're logged in as “Tyler Durden”.
</div>
</div>
<p>If I wanted to add a border to the component with a slightly darker variation of the background color, I would usually take the color picker in dev tools and just pick a random darker color.</p>
<pre><code class="language-css">[role="status"] {
--border-color: rgb(122 186 235);
border: 2px solid var(--border-color);
}</code></pre>
<div data-role="demo" class="sample2">
<div role="status">
<strong>Information:</strong> You're logged in as “Tyler Durden”.
</div>
</div>
<p>That's fine, but with <code>hsl()</code> it's way more intuitive. Instead of picking a random color, I use the background color and I just reduced the <code>l</code> value (lightness) from 90% to 70%.</p>
<pre><code class="language-css">[role="status"] {
--background-color: hsl(206deg 74% 90%);
--border-color: hsl(206deg 74% 70%);
background-color: var(--background-color);
border: 2px solid var(--border-color);
padding: 1rem;
margin-block-end: 1em;
}</code></pre>
<div data-role="demo" class="sample3">
<div role="status">
<strong>Information:</strong> You're logged in as “Tyler Durden”.
</div>
</div>
<p>To avoid repetition, I store the <code>h</code> (hue) and <code>s</code> (saturation) value in custom properties. </p>
<pre><code class="language-css">[role="status"] {
--h: 206deg;
--s: 74%;
--background-color: hsl(var(--h) var(--s) 90%);
--border-color: hsl(var(--h) var(--s) 70%);
}</code></pre>
<p>What’s great about that is that creating variations of this component in different colors is now super easy. All I have to do is change the <code>h</code> value.</p>
<pre><code class="language-css">.warning {
--h: 40deg;
}
.error {
--h: 0deg;
}</code></pre>
<pre><code class="language-html"><div role="status" class="warning">
<strong>Warning:</strong> Your free trial is ending in 4 days.
</div>
<div role="status" class="error">
<strong>Error:</strong> Your username and password don't match.
</div></code></pre>
<div data-role="demo" class="sample4">
<div role="status" class="warning">
<strong>Warning:</strong> Your free trial is ending in 4 days.
</div>
<div role="status" class="error">
<strong>Error:</strong> Your username and password don't match.
</div>
</div>
<p>Theoretically, you don’t even need the classes.</p>
<pre><code class="language-html"><div role="status" style="--h: 40deg;">
<strong>Warning:</strong> Your free trial is ending in 4 days.
</div>
<div role="status" style="--h: 0deg;">
<strong>Error:</strong> Your username and password don't match.
</div></code></pre>
<p>Here's the final CSS code.</p>
<pre><code class="language-css">[role="status"] {
--h: 206deg;
--s: 74%;
--background-color: hsl(var(--h) var(--s) 90%);
--border-color: hsl(var(--h) var(--s) 70%);
background-color: var(--background-color);
border: 2px solid var(--border-color);
padding: 1rem;
margin-block-end: 1em;
}
.warning {
--h: 40deg;
}
.error {
--h: 0deg;
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CCSS+color+functions+and+custom+properties%E2%80%9D">blog@matuzo.at</a>.</p> Day 79: font-tech() and font-format()2023-01-12T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day79
<p>You can use the <code>@supports</code> rule to check whether a browser supports a specified font technology or font format.</p><h2>font-tech()</h2>
<style>
@supports font-tech(palettes) {
.palette {
display: block;
}
}
@supports not font-tech(incremental) {
.incremental {
display: block;
}
}
@supports font-format(woff2) {
.woff {
display: block;
}
}
[data-sample] {
font-size: 1.6rem;
font-weight: bold;
}
</style>
<p>The <code>font-tech()</code> function checks whether a browser supports the specified font technology. For example, you can apply styles only if the browser supports <a href="/blog/2023/100daysof-day75/">font-palettes</a> or if it doesn't support incremental font loading (I have no idea what that is).</p>
<pre><code class="language-html"><div class="palette" hidden>
Your browser supports font palettes 🎉
</div>
<div class="incremental" hidden>
Your browser doesn't support incremental font loading 😭
</div></code></pre>
<pre><code class="language-css">@supports font-tech(palettes) {
.palette {
display: block;
}
}
@supports not font-tech(incremental) {
.incremental {
display: block;
}
}</code></pre>
<div data-sample="demo">
<div class="palette" hidden>Your browser supports font palettes 🎉</div>
<div class="incremental" hidden>Your browser doesn't support incremental font loading 😭</div>
</div>
<p>You can find a list of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@supports#font-tech">font technologies on MDN</a>.</p>
<h2>font-format()</h2>
<p>The <code>font-format()</code> function checks whether a browser supports the specified font format. For example, you can apply styles only if the browser supports<br />
<code>.woff2</code>.</p>
<pre><code class="language-html"><div hidden>
Your browser supports woff2 🎉
</div></code></pre>
<pre><code class="language-css">@supports font-format(woff2) {
div {
display: block;
}
}</code></pre>
<div data-sample="demo">
<div hidden class="woff">
Your browser supports woff2 🎉
</div>
</div>
<p>You can find a list of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@supports#font-format">font formats on MDN</a>.</p>
<div class="highlight">
<strong>Browser Support:</strong> I haven't tested in thoroughly but both functions work on latest Chrome and Firefox on macOS, and Chrome and Firefox on Android. They don't work on Samsung Internet and Safari.
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+79%3A+font-tech%28%29+and+font-format%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 80: container style queries2023-01-13T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day80
<p>Container style queries allow querying the computed values of a query container.</p><p>No browser supports it yet, but we should be able to do something like this:</p>
<pre><code class="language-css">.card {
aspect-ratio: 1 / 1;
}
.card--wide {
aspect-ratio: 16 / 9
}
@container style(aspect-ratio: 16 / 9) {
img {
object-fit: cover;
}
}</code></pre>
<p>We can check whether a container has a specific property and value assigned and apply additional styles based on this condition. Chrome support container style queries behind a flag, but only with custom properties, not <em>ordinary</em> properties.</p>
<p>Here’s an example based on a component I've built a while ago where this can be useful. We have a card component that can be used in different ways. It can contain just text, text and an image, text and a video, and also text with a background color. Most background colors in our design system work well with black as the text color, but there’s one exception.</p>
<style>
:root {
--nebelgrau: #d6d1ca;
--flieder: #aaaafa;
--abendstimmung: #49274b;
}
.card {
--bg: var(--nebelgrau);
background-color: var(--bg);
}
@container style(--bg: var(--abendstimmung)) {
.sample2 * {
color: #fff;
}
}
</style>
<pre><code class="language-css">:root {
--nebelgrau: #d6d1ca;
--flieder: #aaaafa;
--abendstimmung: #49274b;
}
.card {
--bg: var(--nebelgrau);
background-color: var(--bg);
}</code></pre>
<pre><code class="language-html"><div class="card">
<h2>Chapter 1</h2>
</div>
<div class="card" style="--bg: var(--flieder)">
<h2>Chapter 2</h2>
</div>
<div class="card" style="--bg: var(--abendstimmung)">
<h2>Chapter 3</h2>
</div></code></pre>
<div data-sample="simplified demo">
<div class="card">
<h2>Chapter 1</h2>
</div>
<div class="card" style="--bg: var(--flieder)">
<h2>Chapter 2</h2>
</div>
<div class="card" style="--bg: var(--abendstimmung)">
<h2>Chapter 3</h2>
</div>
</div>
<p>I could assign a class to this specific card variation and just change the text color. That’s actually what we’re doing at the moment, but with style queries I can create a general rule that changes the text color whenever the container has a specific background color.</p>
<pre><code class="language-css">@container style(--bg: var(--abendstimmung)) {
* {
color: #fff;
}
}</code></pre>
<div class="highlight"><strong>Note:</strong> The color is probably still black for you, but it's white in supported browsers (Only Chrome behind a flag at the moment.)</div>
<div data-sample="simplified demo" class="sample2">
<div class="card" style="--bg: var(--abendstimmung)">
<h2>Chapter 2</h2>
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+80%3A+container+style+queries%E2%80%9D">blog@matuzo.at</a>.</p> Day 81: the order of individual transform properties2023-01-16T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day81
<p><a href="/blog/2023/100daysof-day76/">On day 66</a>, I’ve introduced you to individual transform properties. An interesting detail about these properties is the order in which transforms are applied compared to the <code>transform</code> property.</p><p>If you use the <code>transform</code> property, transformation functions are applied in the order of appearance, from left to right.</p>
<style>
.button1 {
transform: translateX(50px) scale(1.5);
}
.button2 {
transform: scale(1.5) translateX(50px);
}
.button3 {
translate: 50px 0;
scale: 1.5;
}
.button4 {
scale: 1.5;
translate: 50px 0;
}
.button5 {
transform: translateX(50px);
scale: 1.5;
}
.button6 {
translate: 50px 0;
transform: scale(1.5);
}
</style>
<pre><code class="language-html"><button class="button1">Button 1</button>
<button class="button2">Button 2</button></code></pre>
<pre><code class="language-css">.button1 {
transform: translateX(50px) scale(1.5);
}
.button2 {
transform: scale(1.5) translateX(50px);
}</code></pre>
<div data-sample="demo">
<button class="button1">Button 1</button>
<br><br>
<button class="button2">Button 2</button>
</div>
<p>With individual transform properties, the order of appearance of the declarations doesn’t matter. The order is always the same: <code>translate</code> –> <code>rotate</code> –> <code>scale</code>.</p>
<pre><code class="language-css">.button3 {
translate: 50px 0;
scale: 1.5;
}
.button4 {
scale: 1.5;
translate: 50px 0;
}</code></pre>
<div data-sample="demo">
<button class="button3">Button 3</button>
<br><br>
<button class="button4">Button 4</button>
</div>
<p>If you mix <code>transform</code> and individual properties, individual transforms get applied first.</p>
<pre><code class="language-css">.button5 {
transform: translateX(50px);
scale: 1.5;
}
.button6 {
transform: scale(1.5);
translate: 50px 0;
}</code></pre>
<div data-sample="demo">
<button class="button5">Button 5</button>
<br><br>
<button class="button6">Button 6</button>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+81%3A+the+order+of+individual+transform+properties%E2%80%9D">blog@matuzo.at</a>.</p> Day 82: value processing2023-01-17T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day82
<p>This post differs from most of the other posts because it’s not about modern CSS, but about CSS fundamentals. When I was writing about <a href="/blog/2022/100daysof-day1/">custom properties</a> and especially about <a href="/blog/2023/100daysof-day80/">container style queries</a>, I realized that I had to understand some of the basics of the language first before I could comprehend how certain properties and rules worked.</p><p>The final value of a property in CSS is the result of a multi-step calculation. In this process, the actual value of a property can come from different sources, take on different forms, and undergo adjustments.</p>
<h2>Declared Values</h2>
<p>A property may have multiple declared values. Each property declaration applied to an element contributes a declared value.</p>
<pre><code class="language-css">h1 {
color: aqua;
}
#heading {
color: rebeccapurple;
}
.heading {
color: fuchsia;
}
</code></pre>
<p>The <code>color</code> property has 3 declared values. The cascade takes theses values and chooses a single “winning” value, the cascaded value.</p>
<h2>Cascaded Value</h2>
<p>The cascaded value represents the result of the cascade. It’s the declared value with the highest precedence.</p>
<pre><code class="language-css">#heading {
color: rebeccapurple;
}</code></pre>
<h2>Specified Value</h2>
<p>If the cascade results in a value, the specified value equals the cascaded value. If not, the property must take their value from somewhere else. Inherited properties draw their value from their parent element. All other properties use their initial value.</p>
<p>If we take the <code><h1></code> as an example, we get the following values and origins for the <code>border-bottom-color</code>, <code>border-bottom-style</code>, <code>color</code>, <code>font-family</code>, and <code>width</code> properties. (These properties are just examples).</p>
<table>
<caption>h1 specified values</caption>
<thead>
<tr>
<th>property</th>
<th>value</th>
<th>origin</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border-bottom-color</code></td>
<td>currentColor</td>
<td>initial</td>
</tr>
<tr>
<td><code>border-bottom-style</code></td>
<td>none</td>
<td>initial</td>
</tr>
<tr>
<td><code>color</code></td>
<td>rebeccapurple</td>
<td>cascaded</td>
</tr>
<tr>
<td><code>font-family</code></td>
<td>Depends on the user agent</td>
<td>inherited</td>
</tr>
<tr>
<td><code>width</code></td>
<td>auto</td>
<td>initial</td>
</tr>
</thead>
</table>
<h2>Computed value</h2>
<p>A specified value can either be relative or absolute. Relative means relative to another value, like <code>50%</code>, <code>2em</code>, or <code>lighter</code>. Absolute is the opposite, for example, <code>200px</code>, <code>2mm</code>, or <code>bold</code>.</p>
<p>The computed value results from resolving value dependencies, which generally means absolutizing relative values. You can find how a property value is resolved in the property definition table of the property in the specification.</p>
<table>
<caption>h1 computed values</caption>
<thead>
<tr>
<th>property</th>
<th>specified value</th>
<th>computed value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border-bottom-color</code></td>
<td>currentColor</td>
<td>rgb(102, 51, 153)</td>
</tr>
<tr>
<td><code>border-bottom-style</code></td>
<td>none</td>
<td>none</td>
</tr>
<tr>
<td><code>color</code></td>
<td>rebeccapurple</td>
<td>rgb(102, 51, 153)</td>
</tr>
<tr>
<td><code>font-family</code></td>
<td>Depends on the user agent</td>
<td>Times (for example)</td>
</tr>
<tr>
<td><code>width</code></td>
<td>auto</td>
<td>auto</td>
</tr>
</thead>
</table>
<h2>Used value</h2>
<p>In the previous chapter, I said that computing values <em>generally</em> means absolutizing relative values. For example, <code>font-size: 1rem;</code> becomes <code>font-size: 16px;</code>, but that’s not true for every property. </p>
<p><code>width: 80%;</code> stays <code>width: 80%</code>. Per definition, the computed value of the <code>width</code> property is <em>“as specified”</em>. That’s because <code>width: 80%;</code> can’t be resolved into a length without knowing the layout of the element’s ancestors.</p>
<p>The used value takes the computed value and completes any remaining calculations to make it the absolute theoretical value used in the formatting of the document. For example, <code>width: 80%;</code> becomes <code>width: 420px</code>.</p>
<table>
<caption>h1 used values</caption>
<thead>
<tr>
<th>property</th>
<th>computed value</th>
<th>used value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border-bottom-color</code></td>
<td>rgb(102, 51, 153)</td>
<td>rgb(102, 51, 153)</td>
</tr>
<tr>
<td><code>border-bottom-style</code></td>
<td>none</td>
<td>none</td>
</tr>
<tr>
<td><code>color</code></td>
<td>rgb(102, 51, 153)</td>
<td>rgb(102, 51, 153)</td>
</tr>
<tr>
<td><code>font-family</code></td>
<td>Times</td>
<td>Times</td>
</tr>
<tr>
<td><code>width</code></td>
<td>auto</td>
<td>1062.27px (for example)</td>
</tr>
</thead>
</table>
<h2>Actual value</h2>
<p>In principle, the <em>“used value”</em> is ready, but the user agent may need to adjust the value in order to make use of it. For example, the font size of an element may need adjustment based on the presence of the <code>font-size-adjust</code> property, or floats may need to be converted to integers. </p>
<table>
<caption>h1 actual values</caption>
<thead>
<tr>
<th>property</th>
<th>used value</th>
<th>actual value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border-bottom-color</code></td>
<td>rgb(102, 51, 153)</td>
<td>rgb(102, 51, 153)</td>
</tr>
<tr>
<td><code>border-bottom-style</code></td>
<td>none</td>
<td>none</td>
</tr>
<tr>
<td><code>color</code></td>
<td>rgb(102, 51, 153)</td>
<td>rgb(102, 51, 153)</td>
</tr>
<tr>
<td><code>font-family</code></td>
<td>Times</td>
<td>Times</td>
</tr>
<tr>
<td><code>width</code></td>
<td>1062.27px</td>
<td>1062px</td>
</tr>
</thead>
</table><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+82%3A+value+processing%E2%80%9D">blog@matuzo.at</a>.</p> Day 83: computed values in container style queries2023-01-18T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day83
<p>On <a href="/blog/2023/100daysof-day80/">day 80</a>, I’ve explained that we can check whether a container has a specific property and value assigned and apply additional styles based on this condition. On <a href="/blog/2023/100daysof-day82/">day 82</a>, I’ve explained that the value of a property can come from different sources, undergo adjustments before it becomes the actual value, and take on different forms along the way. To use container style queries, it’s important to understand which value's being used in queries.</p><p>Let’s start nice and easy. We have a <code>.card</code> and the value of the <code>--bg</code> property is <code>red</code>. In a style query, we check if that’s actually the case and apply conditional style to the <code><h1></code> nested in the card.</p>
<pre><code class="language-html"><div class="card">
<h1>heyho</h1>
</div></code></pre>
<pre><code class="language-css">.card {
--bg: red;
}
@container style(--bg: red) {
h1 {
border: 10px dotted aqua;
}
}</code></pre>
<p>Result: the <code><h1></code> gets a beautiful dotted border.</p>
<p>If we put the color value in its own property and query the assignment of the <code>var()</code> function to the <code>--bg</code> property, the styles will be applied, as well.</p>
<pre><code class="language-css">html {
--color-red: red;
}
.card {
--bg: var(--color-red);
}
@container style(--bg: var(--color-red)) {
h1 {
border: 10px dotted aqua;
}
}</code></pre>
<p>Here's where it gets really interesting: If we change the query back to <code>style(--bg: red)</code>, the styles still apply.</p>
<pre><code class="language-css">html {
--color-red: red;
}
.card {
--bg: var(--color-red);
}
@container style(--bg: red) {
h1 {
border: 10px dotted aqua;
}
}</code></pre>
<p>Even if we never assign <code>--color-red</code> to <code>--bg</code>, but they have the same value, the styles still apply.</p>
<pre><code class="language-css">html {
--color-red: red;
}
.card {
--bg: red;
}
@container style(--bg: var(--color-red)) {
h1 {
border: 10px dotted aqua;
}
}</code></pre>
<p>The <em>“simple”</em> explanation is that style queries compare the equality of <a href="http://localhost:8080/blog/2023/100daysof-day82/#computed-value">computed values</a> and not assignments. The computed value results from resolving value dependencies, which generally means absolutizing relative values. Both sides of the query are evaluated and resolved before the comparison.</p>
<p>So, in each example the actual comparison is something like: </p>
<pre><code class="language-css">/* Note: this is not valid syntax, it's just an
illustration of the underlying comparison. */
@container style("red": "red") {
h1 {
border: 10px dotted aqua;
}
}</code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+83%3A+computed+values+in+container+style+queries%E2%80%9D">blog@matuzo.at</a>.</p> Day 84: the @property at-rule2023-01-19T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day84
<p>The <code>@property</code> rule allows you to register custom properties.</p><p>You can already define custom properties, but the difference between defining and registering is that the at-rule allows you to specify the type and other attributes.</p>
<pre><code class="language-css">@property --hue {
/* The type. */
syntax: '<angle>';
/* Is it an inhertiable property? */
inherits: false;
/* The initial value. */
initial-value: 0deg;
}</code></pre>
<h2>syntax</h2>
<p>The <code>syntax</code> descriptor specifies the syntax (or type) of the property. You can find a list of <a href="https://drafts.css-houdini.org/css-properties-values-api/#supported-names">supported syntax component names</a> in the spec.</p>
<pre><code class="language-css">@property --milliseconds {
syntax: '<integer>';
inherits: false;
}</code></pre>
<h2>inherits</h2>
<p>The <code>inherits</code> descriptor specifies whether the property inherits from its parent or not.</p>
<pre><code class="language-css">@property --color-primary {
syntax: '<color>';
inherits: true;
}</code></pre>
<h2>initial-value</h2>
<p>The <code>initial-value</code> descriptor specifies the initial value of the custom property.</p>
<pre><code class="language-css">@property --color-primary {
syntax: '<color>';
inherits: true;
initial-value: rgb(0 0 0);
}</code></pre>
<h2>An example</h2>
<p>Let's say we have a <code><button></code> and we want to transition the background color on <code>:hover</code> and <code>:focus-visible</code>.</p>
<style>
@property --h3 {
initial-value: 0;
inherits: true;
syntax: '<number>';
}
button {
--h: 176;
--h3: 176;
--s: 74%;
--l: 60%;
--bg: hsl(var(--h) var(--s) var(--l));
background-color: var(--bg);
font-size: 2rem;
width: 10rem;
height: 10rem;
border-radius: 50%;
}
.button1 {
transition: background-color 1s;
}
.button2 {
transition: --h 1s;
}
.button3 {
transition: --h3 1.6s;
--bg: hsl(var(--h3) var(--s) var(--l));
}
.button3:hover {
--h3: 40;
}
button:is(:hover, :focus-visible) {
--h: 40;
}
</style>
<pre><code class="language-css">button {
--h: 176;
--s: 74%;
--l: 60%;
--bg: hsl(var(--h) var(--s) var(--l));
background-color: var(--bg);
transition: background-color 1s;
}
button:is(:hover, :focus-visible) {
--h: 20;
}</code></pre>
<div data-sample="demo">
<header>
<button type="button" class="button1">Send</button>
</header>
</div>
<p>That works well, we get a nice transition from the first color to the second color, but if we want to animate just the hue to get a more interesting effect, we have bad luck, because the value of <code>--h</code> is a string, which you can't animate.</p>
<pre><code class="language-css">button {
--h: 176;
--s: 74%;
--l: 60%;
--bg: hsl(var(--h) var(--s) var(--l));
background-color: var(--bg);
transition: --h 1s;
}</code></pre>
<div data-sample="demo">
<header>
<button type="button" class="button2">Send</button>
</header>
</div>
<p>With <code>@property</code> we can turn the string into number and animate it.</p>
<pre><code class="language-css">@property --h {
initial-value: 0;
inherits: true;
syntax: '<number>';
}
button {
--h: 176;
--s: 74%;
--l: 60%;
--bg: hsl(var(--h) var(--s) var(--l));
background-color: var(--bg);
transition: --h 1.6s;
}</code></pre>
<div data-sample="demo">
<header>
<button type="button" class="button3">Send</button>
</header>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+84%3A+the+%40property+at-rule%E2%80%9D">blog@matuzo.at</a>.</p> Day 85: typed custom properties in container style queries2023-01-20T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day85
<p>Registering <a href="/blog/2023/100daysof-day84/">typed custom properties</a> can be useful in container style queries.</p><p>The following container style query matches because the computed value of both <code>--bg</code> and <code>--color</code> is “red”.</p>
<pre><code class="language-css">html {
--color: red;
}
.card {
--bg: red;
}
/* Condition is true, styles applied */
@container style(--bg: var(--keyword)) {
h1 {
border: 10px dotted fuchsia;
}
}</code></pre>
<p>What's important to understand is that we're comparing two strings, not colors. If we change <code>--color</code> to <code>rgb(255 0 0)</code>, the query doesn't match anymore.</p>
<pre><code class="language-css">html {
--color: rgb(255 0 0);
}
.card {
--bg: red;
}
/* Condition is false, styles not applied */
@container style(--bg: var(--color)) {
h1 {
border: 10px dotted fuchsia;
}
}</code></pre>
<p>That's where the <a href="/blog/2023/100daysof-day84/"><code>@property</code> rule</a> comes into play. It allows us to add a type to a custom property and turn <code>--bg</code> into a proper color value.</p>
<pre><code class="language-css">@property --bg {
syntax: '<color>';
inherits: true;
initial-value: rgb(0 0 0);
}
html {
--color: rgb(255 0 0);
}
.card {
--bg: red;
}
/* Condition is true, styles applied */
@container style(--bg: var(--color)) {
h1 {
border: 10px dotted fuchsia;
}
}</code></pre>
<p>As you can see, you only have to add a type to <code>--bg</code> and not <code>--color</code>, as well. That's because <code>--bg</code> is <a href="/blog/2023/100daysof-day83/">resolved as a color</a> in both the declaration in <code>.card</code> and in the query.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+85%3A+typed+custom+properties+in+container+style+queries%E2%80%9D">blog@matuzo.at</a>.</p> Day 86: the initial-letter property2023-01-23T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day86
<p>The <code>initial-letter</code> property specifies size and sink for initial letters.</p><pre><code class="language-css">@supports (-webkit-initial-letter: 1) or (initial-letter: 1) {
p::first-letter {
-webkit-initial-letter: 3;
initial-letter: 3;
}
}</code></pre>
<p>The property takes two arguments. The first one defines the size of the initial letter in terms of how many lines it occupies. The optional second argument defines the number of lines the initial letter should sink. If it's omitted, it equals the initial letter size.</p>
<pre><code class="language-css"> p::first-letter {
initial-letter: 2;
}</code></pre>
<img alt="The first letter in a paragraph spans 2 lines starting at the first line and ending at the second line."" height="328" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day86/d102c290b2-1698950549/100days-86-1.png" width="928">
<p>1 as the second argument indicates a raised initial.</p>
<pre><code class="language-css"> p::first-letter {
initial-letter: 2 1;
}</code></pre>
<img alt="The first letter in a paragraph spans 2 lines starting at the first line and going up ending one line above the first line."" height="384" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day86/23d588f56a-1698950549/100days-86-2.png" width="828">
<p>Values greater than 1 indicate a sunken initial letter. </p>
<pre><code class="language-css"> p::first-letter {
initial-letter: 2 5;
}</code></pre>
<img alt="The first letter in a paragraph spans 2 lines starting at the fourth line and ending at the fifth line."" height="334" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day86/1ddf7eb8e9-1698950549/100days-86-3.png" width="916">
<div class="highlight">
<p><strong>Note:</strong> This property is currently only supported in Safari with a prefix, but it's <a href="https://caniuse.com/css-initial-letter">coming to Chromium browsers soon</a>.</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+86%3A+the+initial-letter+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 87: mask properties2023-01-24T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day87
<p>You can use <code>mask</code> properties to apply a mask to an element.</p><style>
.post img {
border: none;
}
.mask {
-webkit-mask-image: url(/blog/2023/100daysof-day87/htmhell_logo.svg);
mask-image: url(/blog/2023/100daysof-day87/htmhell_logo.svg);
}
.mask-size {
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
}
.element {
max-width: 400px;
aspect-ratio: 1;
background-color: red;
-webkit-mask-image: url(/blog/2023/100daysof-day87/htmhell_logo.svg);
mask-image: url(/blog/2023/100daysof-day87/htmhell_logo.svg);
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
}
</style>
<p>Let's say you have an image and a logo. You can use the logo to mask the image.</p>
<img alt="a huge fire. multiple large palettes burning." height="300" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day87/07fb18b5dd-1698950550/fire.jpg" width="400">
<img alt="HTMHell logo" height="370" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day87/dbab2491b0-1698950550/htmhell_logo.svg" width="400">
<pre><code class="language-css">img {
mask-image: url(/images/htmhell_logo.svg);
}</code></pre>
<img alt="" class="mask" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day87/07fb18b5dd-1698950550/fire.jpg" width="400">
<p>There are a bunch of properties you can use to adjust the styling of the mask.</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-clip">mask-clip (MDN)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite">mask-composite (MDN)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image">mask-image (MDN)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-mode">mask-mode (MDN)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-origin">mask-origin (MDN)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-position">mask-position (MDN)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-repeat">mask-repeat (MDN)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-size">mask-size (MDN)</a></li>
</ul>
<pre><code class="language-css">img {
mask-image: url(/images/htmhell_logo.svg);
mask-size: cover;
mask-repeat: no-repeat;
mask-position: center;
}</code></pre>
<img alt="" class="mask mask-size" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day87/07fb18b5dd-1698950550/fire.jpg" width="400">
<p>You can also apply a mask to an element.</p>
<div class="element">
</div>
<pre><code class="language-css">div {
max-width: 400px;
aspect-ratio: 1;
background-color: red;
mask-image: url(/images/htmhell_logo.svg);
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
}</code></pre>
<div class="highlight">
<strong>Note:</strong> You need the <code>-webkit-</code> prefix for some browsers.</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+87%3A+mask+properties%E2%80%9D">blog@matuzo.at</a>.</p> Day 88: CSS Motion Path2023-01-25T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day88
<p>CSS Motion path allows you to position any graphical object and animate it along a specified path.</p><style>
.square {
background: hsl(93deg 75% 49%);
height: 2em;
width: 2em;
position: absolute;
inset-inline-start: 0;
inset-block-start: 0;
}
.sample2 .square {
offset-path: path("m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019");
}
.sample3 .square {
offset-distance: 30%;
}
.sample4 .square {
offset-rotate: 13deg;
}
.sample5 .square {
animation: move 2s infinite;
animation-play-state: paused;
}
[aria-pressed="true"] ~ div .square {
animation-play-state: running;
}
[aria-pressed="true"] {
outline: 4px solid green !important;
}
@keyframes move {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
.sample6 .square {
position: static;
}
.sample6 .demo-wrapper {
height: 150px;
}
.demo-wrapper {
position: relative;
}
</style>
<p>Let's you have a path, and you want to animate an element along that path. </p>
<div class="highlight">
<strong>Note:</strong> You don't need the <code><svg></code> to achieve that, but for the sake of understanding, I'm using it in this demo to visualize the path. I've placed the square on top of the svg using absolute positioning.
</div>
<pre><code class="language-html"><svg width="305" height="144">
<path stroke="#000" fill="none" stroke-width="4" d="m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019">
</svg>
<div class="square"></div></code></pre>
<pre><code class="language-css">.square {
background: hsl(93deg 75% 49%);
height: 2em;
width: 2em;
position: absolute;
inset-inline-start: 0;
inset-block-start: 0;
}</code></pre>
<div data-sample="demo">
<div class="demo-wrapper">
<svg width="305" height="144" class="svg">
<path stroke="#000" fill="none" stroke-width="4" d="m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019"/>
</svg>
<div class="square"></div>
</div>
</div>
<p>Because of the absolute positioning, the <code>.square</code> is at the top left corner of its parent element. If you want to put the <code>.square</code> on a path (<strong>Note</strong>: not the actual path of the svg, but its own path), you can use the <code>offset-path</code> property. Just copy the value of the <code><path></code>s <code>d</code> attribute and put it in a <code>path()</code> function.</p>
<pre><code class="language-css">.square {
offset-path: path("m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019");
}</code></pre>
<div data-sample="demo" class="sample2">
<div class="demo-wrapper">
<svg width="305" height="144" class="svg">
<path stroke="#000" fill="none" stroke-width="4" d="m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019"/>
</svg>
<div class="square"></div>
</div>
</div>
<p>The <code>.square</code> is now positioned on the path and can be moved, using <code>offset-distance</code>.</p>
<pre><code class="language-css">.square {
offset-path: path("m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019");
offset-distance: 30%;
}</code></pre>
<div data-sample="demo" class="sample2 sample3">
<div class="demo-wrapper">
<svg width="305" height="144" class="svg">
<path stroke="#000" fill="none" stroke-width="4" d="m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019"/>
</svg>
<div class="square"></div>
</div>
</div>
<p>You can also rotate it, using <code>offset-rotate</code>.</p>
<pre><code class="language-css">.square {
offset-path: path("m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019");
offset-distance: 30%;
offset-rotate: 13deg;
}</code></pre>
<div data-sample="demo" class="sample2 sample3 sample4">
<div class="demo-wrapper">
<svg width="305" height="144" class="svg">
<path stroke="#000" fill="none" stroke-width="4" d="m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019"/>
</svg>
<div class="square"></div>
</div>
</div>
<p>Of course, you can also animate these properties.</p>
<pre><code class="language-css">.square {
offset-path: path("m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019");
animation: move 2s infinite;
}
@keyframes move {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}</code></pre>
<div data-sample="demo" class="sample2 sample5">
<button aria-pressed="false" class="play">Play animation</button>
<div class="demo-wrapper">
<svg width="305" height="144" class="svg">
<path stroke="#000" fill="none" stroke-width="4" d="m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019"/>
</svg>
<div class="square"></div>
</div>
</div>
<script>
const button = document.querySelector('.play');
button.addEventListener('click', e => {
const playing = e.target.getAttribute('aria-pressed') !== "false"
e.target.setAttribute('aria-pressed', !playing)
})
</script>
<p>For the sake of completeness, the same demo without the svg.</p>
<pre><code class="language-html"><div class="parent">
<div class="square"></div>
</div></code></pre>
<pre><code class="language-css">.parent {
height: 150px;
}
.square {
offset-path: path("m4,139c0,-1.31731 7.78207,-137 121.62162,-137c113.83955,0 85.71428,133.04808 178.37837,127.12019");
animation: move 2s infinite;
position: static;
}</code></pre>
<div data-sample="demo" class="sample2 sample5 sample6">
<button aria-pressed="false" class="play2">Play animation</button>
<div class="demo-wrapper">
<div class="square"></div>
</div>
</div>
<script>
const button2 = document.querySelector('.play2');
button2.addEventListener('click', e => {
const playing = e.target.getAttribute('aria-pressed') !== "false"
e.target.setAttribute('aria-pressed', !playing)
})
</script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+88%3A+CSS+Motion+Path%E2%80%9D">blog@matuzo.at</a>.</p> Day 89: higher-order custom properties2023-01-26T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day89
<p>Style queries may change the way we write CSS significantly.</p><div class="highlight">
<p><strong>Caution:</strong> If you’re a fan of Tailwind or similar utility frameworks, you might find this post offensive because it suggests using fewer classes instead of more.</p>
</div>
<p>On <a href="/blog/2023/100daysof-day80/">day 80</a> I’ve introduced you to container style queries. I’ve showed you a practical example from a project I was working on where style queries would’ve been really useful: When the following component has a dark background color, I set a light text color on all children.</p>
<pre><code class="language-html"><div class="card">
<h2>light</h2>
</div>
<div class="card" style="--bg: var(--dark)">
<h2>dark</h2>
</div></code></pre>
<pre><code class="language-css">:root {
--dark: #000;
--light: aqua;
}
.card {
--bg: var(--light);
background-color: var(--bg);
color: #000;
}
@container style(--bg: var(--dark)) {
* {
color: #fff;
}
}</code></pre>
<p>Yeah, I know, not the best example in the world, but you get the point.</p>
<p>What’s even more interesting than querying custom properties, we’ve applied to a property of a container, is querying custom properties whose sole purpose it is to tell us something about the container. Doesn’t make sense? Okay, here’s an example.</p>
<p>Let's say we have a basic card component. </p>
<style>
.card-wrapper {
--width: 20rem;
--direction: column;
border: 2px solid #000;
display: flex;
flex-direction: var(--direction);
max-width: var(--width);
width: 100%;
}
.card-image {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.card-image img {
height: 100%;
min-width: 0;
order: 1;
object-fit: cover;
width: 100%;
border: none;
}
.card-content {
order: 2;
padding: 1rem;
}
@container style(--card-size: large) {
.card-wrapper {
--width: 30rem;
}
h2 {
font-size: 2rem;
}
}
@container style(--card-style: vertical) {
.card-wrapper {
--width: 40rem;
--direction: row;
gap: 1rem;
}
h2 {
margin-top: 0.5em;
}
.card-image {
aspect-ratio: 1;
}
}
</style>
<div class="card" style="display: block">
<div class="card-wrapper">
<div class="card-content">
<h2>My title</h2>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Non id sint pariatur excepturi delectus, quas saepe, adipisci nemo, beatae quo minima molestiae mollitia expedita assumenda doloremque.</p>
</div>
<div class="card-image">
<img src="https://assets.codepen.io/144736/neue-donau+%281%29.webp" alt="" />
</div>
</div>
</div>
<p>If I want a larger variation of this component, I do this:</p>
<pre><code class="language-html"><div class="card" style="--card-size: large">
…
</div></code></pre>
<p>You can also create a separate class, if you're not a fan of inline styles.</p>
<pre><code class="language-css">.card-large {
--card-size: large;
}</code></pre>
<pre><code class="language-html"><div class="card card-large">
…
</div></code></pre>
<style>
.card {
display: none;
}
@container style(--supports: style-queries) {
.card {
display: block;
}
.card-screenshot {
display: none;
}
}
</style>
<div style="--supports: style-queries">
<div class="card" style="--card-size: large">
<div class="card-wrapper">
<div class="card-content">
<h2>My title</h2>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Non id sint pariatur excepturi delectus, quas saepe, adipisci nemo, beatae quo minima molestiae mollitia expedita assumenda doloremque.</p>
</div>
<div class="card-image">
<img src="https://assets.codepen.io/144736/neue-donau+%281%29.webp" alt="" />
</div>
</div>
</div>
<img alt="Same component but larger and larger text" class="card-screenshot" height="499" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day89/63a15b970c-1698950547/100days-89-1.jpg" width="480">
</div>
<div class="highlight">
<p><strong>Note:</strong> Container style queries are still only supported in Chrome behind a flag.</p>
</div>
<p>Or if I want a vertical layout for the large card, I do this:</p>
<pre><code class="language-html"><div class="card" style="--card-size: large; --card-style: vertical">
…
</div></code></pre>
<div style="--supports: style-queries">
<div class="card" style="--card-size: large; --card-style: vertical">
<div class="card-wrapper">
<div class="card-content">
<h2>My title</h2>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Non id sint pariatur excepturi delectus, quas saepe, adipisci nemo, beatae quo minima molestiae mollitia expedita assumenda doloremque. Magni nesciunt animi recusandae.</p>
</div>
<div class="card-image">
<img src="https://assets.codepen.io/144736/neue-donau+%281%29.webp" alt="" />
</div>
</div>
</div>
<img alt="Same component but larger, larger text, and a vertical layout." class="card-screenshot" height="296" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day89/eea55a857a-1698950547/100days-89-2.jpg" width="640">
</div>
<p>Here are the style queries that make this possible: </p>
<pre><code class="language-css">
@container style(--card-size: large) {
.card-wrapper {
--width: 30rem;
}
h2 {
font-size: 2rem;
}
}
@container style(--card-style: vertical) {
.card-wrapper {
--width: 40rem;
--direction: row;
gap: 1rem;
}
h2 {
margin-top: 0.5em;
}
.card-image {
aspect-ratio: 1;
}
}</code></pre>
<p>Okay, cool, but can't we just use a class for that? Yes, but…</p>
<ul>
<li>
<p>Instead of applying conditional styling using a class, we can now do something we've been used to doing for a while already: adding conditions through queries. It's just style queries instead of media queries.</p>
</li>
<li>
<p>We don't need modifier classes anymore. The style query already scopes the styles in that block to a specific condition.</p>
</li>
<li>
<p>We can create variations of elements that don't have or don't need classes.</p>
<pre><code class="language-html"><blockquote style="--type: pull-quote">
</blockquote></code></pre>
</li>
<li>
<p>custom properties are inhertiable, which means that we can control the styling of all cards within an element by setting the property on the parent element.</p>
<pre><code class="language-html"><section style="--card-size: large">
<div class="card">…</div>
<div class="card">…</div>
<div class="card">…</div>
</section></code></pre>
<p>or even </p>
<pre><code class="language-html"><body style="--card-size: large">
<div class="card">…</div>
<div class="card">…</div>
<div class="card">…</div>
</body></code></pre>
</li>
</ul>
<p>Container style queries are so brand new they aren’t even there yet. I can’t wait for browsers to support them to see if and how they will change the way we write CSS.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+89%3A+higher-order+custom+properties%E2%80%9D">blog@matuzo.at</a>.</p> Day 90: scoped styles in container queries2023-01-27T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day90
<p>Rules within a container query only apply to descendants of that container.</p><p>If you write a media query and you put rules in the media block, the rules apply to the entire document.</p>
<p><button class="style1" aria-pressed="false">Apply rules</button></p>
<pre><code class="language-css">@media (min-width: 1024px) {
* {
outline: 4px solid
}
}</code></pre>
<style>
@media (min-width: 1024px) {
.demo1, .demo1 * {
outline: 4px solid
}
}
.demo4 main,
.demo3 main,
.demo4 [data-sample],
.demo3 [data-sample],
.demo2 [data-sample] {
container-type: inline-size;
}
@container (min-inline-size: 240px) {
.demo2 *,
.demo3 * {
outline: 4px dotted fuchsia;
}
}
.demo4 [data-sample] {
container-name: demo;
}
@container demo (min-inline-size: 240px) {
* {
outline: 4px dotted fuchsia;
}
}
/* body {
container-type: inline-size;
}
.card {
--style: color;
container-type: inline-size;
}
@container style(--style: color) {
* {
background-color: aqua;
}
}
@media (min-width: 1024px) {
* {
outline: 4px solid
}
}
*/
</style>
<p>If you write a container query and you put rules in the container block, the rules only apply to descendants of the container.</p>
<pre><code class="language-html"><div data-sample="demo">
<h2>A quote</h2>
<blockquote>“What came first – the music or the misery? Did I listen to the music because I was miserable? Or was I miserable because I listened to the music? Do all those records turn you into a melancholy person?”</blockquote>
</div></code></pre>
<pre><code class="language-css">[data-sample] {
container-type: inline-size;
}
@container (min-inline-size: 240px) {
* {
border: 8px dotted fuchsia;
}
}</code></pre>
<p><button class="style2" aria-pressed="false">Apply rules</button></p>
<div data-sample="demo">
<h2>A quote</h2>
<blockquote>“What came first – the music or the misery? Did I listen to the music because I was miserable? Or was I miserable because I listened to the music? Do all those records turn you into a melancholy person?”</blockquote>
</div>
<p>If you have nested containers, the styles apply to all applicable containers.</p>
<p><button class="style3" aria-pressed="false">Apply rules</button></p>
<pre><code class="language-css">main,
[data-sample] {
container-type: inline-size;
}
@container (min-inline-size: 240px) {
* {
border: 8px dotted fuchsia;
}
}</code></pre>
<p>This can cause a lot of confusion. I guess, that's one reason why it's advised to name containers.</p>
<pre><code class="language-css">main,
[data-sample] {
container-type: inline-size;
}
[data-sample] {
container-name: demo;
}
@container demo(min-inline-size: 240px) {
* {
border: 8px dotted fuchsia;
}
}</code></pre>
<p><button class="style4" aria-pressed="false">Apply rules</button></p>
<div data-sample="demo">
<h2>A quote</h2>
<blockquote>“What came first – the music or the misery? Did I listen to the music because I was miserable? Or was I miserable because I listened to the music? Do all those records turn you into a melancholy person?”</blockquote>
</div>
<script>
const add = function(btn, cls) {
document.querySelector(btn).addEventListener('click', e => {
const playing = e.target.getAttribute('aria-pressed') !== "false"
e.target.setAttribute('aria-pressed', !playing)
document.documentElement.classList.toggle(cls)
})
}
add('.style1', 'demo1')
add('.style2', 'demo2')
add('.style3', 'demo3')
add('.style4', 'demo4')
</script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+90%3A+scoped+styles+in+container+queries%E2%80%9D">blog@matuzo.at</a>.</p> Day 91: a previous sibling selector with :has()2023-01-30T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day91
<p>I’ve already shown much appreciation for the <code>:has()</code> pseudo-class in this series, but that we can use it as a previous sibling selector tops it all of.</p><p>Since this is not an official selector, but more something like a hack, it can be hard to read and interpret. So, let’s start nice and easy.</p>
<p>We have three buttons. If we hover/focus one button and we want to highlight it and the next adjacent button in the DOM at the same time, we can use the adjacent sibling selector.</p>
<style>
[data-sample] {
display: flex;
gap: 3rem;
padding: 2rem;
}
[data-sample] button {
font-size: 2rem;
outline-width: 8px;
outline-offset: 4px;
outline-color: hotpink;
}
[data-sample] button:is(:hover, :focus-visible) {
outline-style: solid;
}
[data-sample] button:is(:hover, :focus-visible) + button {
outline-style: dashed;
}
</style>
<pre><code class="language-css"> button {
outline-width: 8px;
outline-offset: 4px;
outline-color: hotpink;
}
button:is(:hover, :focus-visible) {
outline-style: solid;
}
button:is(:hover, :focus-visible) + button {
outline-style: dashed;
}</code></pre>
<div data-sample="demo" class="sample1">
<button>previous</button>
<button>middle</button>
<button>next</button>
</div>
<p>There's no previous item selector, but using <code>:has()</code> we can select an item that comes before another item. I've written about next-sibling combinators and <code>:has()</code> on <a href="/blog/2022/100daysof-day26/">day 26</a>. Here’s an example from that post: The following code sets the block-end margin of all <code><h2></code> to 0 if they're followed by a <code><time></code> element.</p>
<pre><code class="language-css">h2 {
margin-block-end: 0.7em;
}
h2:has(+ time) {
margin-block-end: 0;
}</code></pre>
<pre><code class="language-html"><h2>Heading</h2>
<p>Teaser text</p>
<h2>Heading</h2>
<time>31.10.2022</time>
<p>Teaser text</p></code></pre>
<style>
h2:where(.demo) {
line-height: 1;
margin-block-end: 0.7em;
}
h2:has(+ time) {
margin-block-end: 0;
}
</style>
<div data-sample="demo - h2 followed by p">
<div>
<h2 class="demo">Heading</h2>
<p>Teaser text</p>
</div>
</div>
<div data-sample="demo - h2 followed by time">
<div>
<h2 class="demo">Heading</h2>
<time>31.10.2022</time>
<p>Teaser text</p>
</div>
</div>
<p>If we want to use this in our button example, we have to select a <code><button></code> followed by a <code><button></code> in the <code>:hover</code> or <code>:focus-visible</code> state.</p>
<pre><code class="language-css"> button:is(:hover, :focus-visible) {
outline-style: solid;
}
button:is(:hover, :focus-visible) + button {
outline-style: dashed;
}
button:has(+ button:is(:hover, :focus-visible)) {
outline-style: dotted;
}</code></pre>
<style>
.sample2 button:has(+ button:is(:hover, :focus-visible)) {
outline-style: dotted;
}
</style>
<div data-sample="demo" class="sample2">
<button>previous</button>
<button>middle</button>
<button>next</button>
</div>
<p>If we have more buttons and we want to select the button that comes before the previous button, we can extend our selector.</p>
<pre><code class="language-css"> button:has(+ button + button:is(:hover, :focus-visible)) {
/* styles */
}</code></pre>
<p>What a beauty!</p>
<p>Here are a couple of other demos:</p>
<p class="codepen" data-height="473.48046875" data-default-tab="html,result" data-slug-hash="OJwwmpw" data-user="matuzo" style="height: 473.48046875px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/matuzo/pen/OJwwmpw">
Day 91: previous sibling selector with :has() #100DaysOfMoreOrLessModernCSS</a> by Manuel Matuzovic (<a href="https://codepen.io/matuzo">@matuzo</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="yLvdwQW" data-user="pouriversal" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/pouriversal/pen/yLvdwQW">
selecting previous item using CSS</a> by pourya (<a href="https://codepen.io/pouriversal">@pouriversal</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="qBoogaX" data-user="chriscoyier" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/chriscoyier/pen/qBoogaX">
BUBBLE (previous siblings!)</a> by Chris Coyier (<a href="https://codepen.io/chriscoyier">@chriscoyier</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+91%3A+a+previous+sibling+selector+with+%3Ahas%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 92: relative color syntax2023-01-31T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day92
<p>With the relative color syntax we can modify existing colors using color functions. If an origin color is specified, each color channel can either be directly specified, or taken from the origin color and modified.</p><p>For example, we can take a HEX color and add opacity using the <code>rgb()</code> color function and the <code>from</code> keyword.</p>
<style>
[data-sample] div {
height: 6rem;
width: 6rem;
}
.sample1 div {
background-color: rgb(255 0 0 / 50%);
}
.sample2 div {
background-color: rgb(255 0 150);
}
.sample3 div {
background-color: hsl(0deg 100% 40%);
}
.sample4 div {
background-color: rgb(30% 30% 30%);
}
</style>
<pre><code class="language-css">div {
--color: #FF0000;
}
div {
background-color: rgb(from var(--color) r g b / 50%);
}</code></pre>
<div data-sample="demo" class="sample1">
<div><span class="u-vh">rgb(255 0 0 / 50%)</span></div>
</div>
<p>Or we can take the color and replace a specific channel.</p>
<pre><code class="language-css">div {
background-color: rgb(from var(--color) r g 150);
}</code></pre>
<div data-sample="demo" class="sample2">
<div><span class="u-vh">rgb(255 0 0 / 50%)</span></div>
</div>
<p>We can even use the <code>calc()</code> function.</p>
<pre><code class="language-css">div {
background-color: hsl(from var(--color) h s calc(l - 10%));
}</code></pre>
<div data-sample="demo" class="sample3">
<div><span class="u-vh">hsl(0deg 100% 40%)</span></div>
</div>
<p>We can use channel keywords in their corresponding argument, but we don't have to. We can use them in any position.</p>
<pre><code class="language-css">div {
background-color: rgb(from var(--color)
calc(r * .3 + g * .59 + b * .11)
calc(r * .3 + g * .59 + b * .11)
calc(r * .3 + g * .59 + b * .11));
}</code></pre>
<div data-sample="demo" class="sample4">
<div><span class="u-vh">rgb(30% 30% 30%)</span></div>
</div>
<p>How cool is that!? Is there a catch? Well, yeah, it's currently only supported in Safari behind a flag.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+92%3A+relative+color+syntax%E2%80%9D">blog@matuzo.at</a>.</p> Day 93: the lch() color function2023-02-01T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day93
<p>The <code>lch()</code> color function allows you to pick colors from the <a href="https://en.wikipedia.org/wiki/CIELAB_color_space">CIELAB color space</a>, which is device-independant and covers the entire gamut (range) of human color perception.</p><p>Currently, the CSS colors we can define are in the sRGB color space. For the longest time, professional monitors weren’t able to display all possible colors in this range. So, using sRGB colors was absolutely sufficient, but that’s not true anymore. Nowadays, monitors can display much more colors than exist in the sRGB color space. With <code>lch()</code> we get access to these colors (currently <a href="https://caniuse.com/css-lch-lab">Safari 15+ only</a>).</p>
<p>The function takes 3 space-separated values.</p>
<pre><code class="language-css">div {
background-color: lch(78% 172.33 248.2);
}</code></pre>
<img alt="bright light blue color" height="250" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day93/3d020680cd-1698950552/lch_1.png" width="250">
<h2>l - lightness</h2>
<p>The first value defines the lightness. It's typically a number between 0% (representing black) and 100% (representing white). It's <em>typically</em> a number between 0% and 100% because the value can exceed 100% up to 400% representing extra-bright whites on some systems. It's the same lightness as in the <code>lab()</code> color function. </p>
<pre><code class="language-css">div {
background-color: lch(0% 0 0);
}</code></pre>
<img alt="black" height="250" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day93/edcdeb0064-1698950552/lch_2.png" width="250">
<h2>c - chroma</h2>
<p>The second argument is the chroma (roughly representing the “amount of color”). Its a value between 0 and 230.</p>
<pre><code class="language-css">div {
background-color: lch(100% 180 0)
}</code></pre>
<img alt="dark green color" height="250" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day93/b5956baaa1-1698950551/lch_3.png" width="250">
<h2>h - hue</h2>
<p>The third argument is the hue angle. 0deg points along the positive “a” axis (toward purplish red). 90deg points along the positive “b” axis (toward mustard yellow), 180deg points along the negative “a” axis (toward greenish cyan), and 270deg points along the negative “b” axis (toward sky blue).</p>
<pre><code class="language-css">div {
background-color: lch(100% 180 90);
}</code></pre>
<img alt="dark green color" height="250" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day93/f1fe23bb80-1698950551/lch_4.png" width="250"><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+93%3A+the+lch%28%29+color+function%E2%80%9D">blog@matuzo.at</a>.</p> Day 94: the accent-color property2023-02-02T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day94
<p>The <code>accent-color</code> CSS property allows us to specify the accent color for user-interface controls generated by an element.</p><p>Yeah, I know, yet another property that isn’t new at all, but Safari added support only recently (in 15.4) and I’ve never used it. Reason enough for me to include it in the series.</p>
<p>We can use the property to change the accent color of radio buttons, check boxes, progress elements and range sliders. Here's how the look like by default. You can see how the color automatically varies in a dark color scheme to work better with dark background colors.</p>
<style>
[data-sample] {
display: flex;
flex-wrap: wrap;
}
[data-sample] > * {
flex: 1 0 0;
min-width: 16rem;
padding: 1rem;
}
.dark-scheme {
background-color: hsl(0 0% 0%);
color: #FFF;
color-scheme: dark;
}
.accent {
--l: 40%;
accent-color: hsl(270deg 50% var(--l));
}
.sample3 .dark-scheme {
--l: 75%;
}
</style>
<pre><code class="language-css">.dark-scheme {
background-color: hsl(0 0% 0%);
color: #FFF;
color-scheme: dark;
}</code></pre>
<div data-sample="demo">
<div class="default">
<h2>Default</h2>
<input type="checkbox" id="check" checked><label for="check">Checkbox</label><br>
<input type="radio" id="radio" checked><label for="radio">Radio</label><br>
<progress max="100" value="50"></progress><br>
<label for="range">Range</label>
<input type="range" id="range">
</div>
<div class="default dark-scheme">
<h2>Default dark scheme</h2>
<input type="checkbox" id="check1" checked><label for="check1">Checkbox</label><br>
<input type="radio" id="radio1" checked><label for="radio1">Radio</label><br>
<progress max="100" value="50"></progress><br>
<label for="range1">Range</label>
<input type="range" id="range1">
</div>
</div>
<p>We can change that color by setting the <code>accent-color</code> property on an element directly or on a parent element.</p>
<pre><code class="language-css">body {
--l: 40%;
accent-color: hsl(270deg 50% var(--l));
}</code></pre>
<div data-sample="demo">
<div class="accent">
<h2>Accent</h2>
<input type="checkbox" id="check2" checked><label for="check2">Checkbox</label><br>
<input type="radio" id="radio2" checked><label for="radio2">Radio</label><br>
<progress max="100" value="50"></progress><br>
<label for="range2">Range</label>
<input type="range" id="range2">
</div>
<div class="accent dark-scheme">
<h2>Accent</h2>
<input type="checkbox" id="check3" checked><label for="check3">Checkbox</label><br>
<input type="radio" id="radio3" checked><label for="radio3">Radio</label><br>
<progress max="100" value="50"></progress><br>
<label for="range3">Range</label>
<input type="range" id="range3">
</div>
</div>
<p>That's great, but now the color doesn't change anymore, when you use a dark <code>color-scheme</code> or if the color scheme of the operating system is dark. We have to adjust the color manually.</p>
<pre><code class="language-css">@media (prefers-color-scheme: dark) {
:root {
--l: 75%;
}
}
.dark-scheme {
--l: 75%;
}</code></pre>
<div data-sample="demo" class="sample3">
<div class="accent">
<h2>Accent</h2>
<input type="checkbox" id="check4" checked><label for="check4">Checkbox</label><br>
<input type="radio" id="radio4" checked><label for="radio4">Radio</label><br>
<progress max="100" value="50"></progress><br>
<label for="range4">Range</label>
<input type="range" id="range4">
</div>
<div class="accent dark-scheme">
<h2>Accent</h2>
<input type="checkbox" id="check5" checked><label for="check5">Checkbox</label><br>
<input type="radio" id="radio5" checked><label for="radio5">Radio</label><br>
<progress max="100" value="50"></progress><br>
<label for="range5">Range</label>
<input type="range" id="range5">
</div>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+94%3A+the+accent-color+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 95: the color-mix() function2023-02-03T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day95
<p>The <code>color-mix()</code> function takes two colors and returns the result of mixing them, in a given color space, by a specified amount.</p><p>To mix colors, pass the <code>in</code> keyword, followed by the color space, and 2 colors.</p>
<pre><code class="language-css">body {
background-color: color-mix(in srgb, blue, white);
}</code></pre>
<p>The syntax is pretty straightforward, but the result is not so much. At least for someone like me who doesn’t understand color and color on the web very well. What surprised me specifically is that mixing colors in different color spaces can yield very different results.</p>
<pre><code class="language-css">:root {
--color1: blue;
--color2: white;
}
.a { --bg: color-mix(in srgb, var(--color1), var(--color2)); }
.b { --bg: color-mix(in srgb-linear, var(--color1), var(--color2)); }
.c { --bg: color-mix(in hsl, var(--color1), var(--color2)); }
.d { --bg: color-mix(in hwb, var(--color1), var(--color2)); }
.e { --bg: color-mix(in lch, var(--color1), var(--color2)); }
.f { --bg: color-mix(in oklch, var(--color1), var(--color2)); }
.g { --bg: color-mix(in lab, var(--color1), var(--color2)); }
.h { --bg: color-mix(in oklab, var(--color1), var(--color2)); }</code></pre>
<pre><code class="language-html"><div class="a">srgb</div>
<div class="b">srgb-linear</div>
<div class="c">hsl</div>
<div class="d">hwb</div>
<div class="e">lch</div>
<div class="f">oklch</div>
<div class="g">lab</div>
<div class="h">oklab</div></code></pre>
<img alt="every resulting color is different. it's either a light or dark lilac color, blueish, pinkish or even green." height="564" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day95/b13a27840a-1698950539/100days-95-1.png" width="250">
<p>Each color will be mixed in equally. The resulting color will have 50% blue and 50% white. We can adjust that ratio.</p>
<pre><code class="language-css">body {
background-color: color-mix(in srgb, 30% blue, white);
/* Same as:
background-color: color-mix(in srgb, 30% blue, 70% white);
background-color: color-mix(in srgb, blue 30%, white);
background-color: color-mix(in srgb, blue, white 70%);
*/
}</code></pre>
<img alt="The same colors, just a little lighter." height="559" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day95/d578c44fcc-1698950539/100days-95-2.png" width="250">
<p>You can learn more about the function in Adam Argyle's fantastic article “<a href="https://developer.chrome.com/blog/css-color-mix/">CSS color-mix()</a>”.</p>
<div class="highlight">
<p><code>color-mix()</code> is currently only supported behind a flag in Safari, but it will be supported in Chrome starting with version 111.</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+95%3A+the+color-mix%28%29+function%E2%80%9D">blog@matuzo.at</a>.</p> Day 96: the margin-trim property2023-02-06T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day96
<p>The <code>margin-trim</code> property allows a container element to trim the margins of its children where they adjoin the container’s edges.</p><p>Let’s say we have a parent element and 4 children, and we use <code>margin-block-end</code> to add some spacing between these elements.</p>
<pre><code class="language-html"><ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
</ul></code></pre>
<pre><code class="language-css">li {
margin-block-end: 1rem;
}</code></pre>
<style>
[data-sample] ul {
list-style: none;
padding: 0;
margin: 0;
border: 4px solid hotpink;
}
[data-sample] li {
margin-block-start: 0 !important;
margin-block-end: 2rem;
border: 4px solid;
}
</style>
<div data-sample="demo">
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
</ul>
</div>
<p>That’s great, but to avoid the extra space at the end of the list, we want to make sure that the last item doesn’t get any margin. To achieve that, I’ve used at least 3 different solutions in the past.</p>
<ol>
<li>
<p><strong>Apply a margin on all elements and remove it from the last.</strong><br />
Okay, why not.</p>
<pre><code class="language-css"> li {
margin-block-end: 1rem;
}
li:last-child {
margin-block-end: 0;
}</code></pre>
</li>
<li>
<p><strong>Apply a margin on all elements but the last.</strong><br />
Looks clever, less lines, but harder to read.</p>
<pre><code class="language-css"> li:not(:last-child) {
margin-block-end: 1rem;
}
</code></pre>
</li>
<li>
<p><strong>Use the <a href="https://alistapart.com/article/axiomatic-css-and-lobotomized-owls/">lobotimized owl selector</a></strong><br />
My favorite for the longest time.</p>
<pre><code class="language-css"> ul > * + * {
margin-block-start: 1rem;
}</code></pre>
</li>
</ol>
<p>There are pros and cons to all solutions. Anyway, eventually we might not have to use any of them when we have this specific problem because <code>margin-trim</code> solves it more elegantly. We can define the property on the parent element and tell it where it should trim margins. Allowed values are <code>none</code>, <code>block</code>, <code>block-start</code>, <code>block-end</code>, <code>inline</code>, <code>inline-start</code>, and <code>inline-end</code>.</p>
<pre><code class="language-css">
ul {
margin-trim: block-end;
}
li {
margin-block-end: 1rem;
}</code></pre>
<div class="highlight">
<p><strong>Note:</strong> Safari Technology Preview is currently the only browser that supports <code>margin-trim</code>.</p>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+96%3A+the+margin-trim+property%E2%80%9D">blog@matuzo.at</a>.</p> Day 97: animating grids2023-02-07T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day97
<p>It’s possible to animate <code>gap</code>, <code>grid-template-columns</code>, and <code>grid-template-rows</code>.</p><p>Almost 6 years ago I wrote a blog post on CodePen titled “<a href="https://codepen.io/matuzo/post/animating-css-grid-layout-properties">Animating CSS Grid Layout properties</a>”. A lot has changed since then, especially recently, and I wanted to update the post, but the blogging feature on CodePen has been sunset and I can’t edit the post anymore. Since animating grids is topical again, I added it to the series. </p>
<p>According to the <a href="https://www.w3.org/TR/css-grid-1/#propdef-grid-column-gap">CSS Grid Layout Module Level 1 specification</a> there are 5 animatable grid properties:</p>
<ul>
<li><code>grid-gap</code>, <code>grid-row-gap</code>, <code>grid-column-gap</code><br>
as length, percentage, or calc.</li>
<li><code>grid-template-columns</code>, <code>grid-template-rows</code><br>
as a simple list of length, percentage, or calc, provided the only differences are the values of the length, percentage, or calc components in the list.</li>
</ul>
<p>Animating these properties will most likely affect larger areas of the screen. That's why it's important to only show animation to those who have no preference for reduced motion.</p>
<pre><code class="language-css">.grid {
--col: 9.5rem;
--row: 8rem;
--gap: 2rem;
display: grid;
grid-template-columns: repeat(3, var(--col));
grid-template-rows: repeat(4, var(--row));
grid-gap: var(--gap);
}
.grid--full {
--col: 30%;
--row: 4rem;
--gap: 1rem;
}
@media (prefers-reduced-motion: no-preference) {
.grid {
transition: all 1s;
}
}</code></pre>
<p class="codepen" data-height="600" data-default-tab="result" data-slug-hash="rmQvMG" data-user="matuzo" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/matuzo/pen/rmQvMG">
CSS Grid Layout: Animation</a> by Manuel Matuzovic (<a href="https://codepen.io/matuzo">@matuzo</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<table>
<caption>Browser support for animatable grid properties</caption>
<thead>
<tr><th>Browser</th>
<th>(grid-)gap, (grid-)row-gap, (grid-)column-gap</th>
<th>grid-template-columns</th>
<th>grid-template-rows</th>
</tr></thead>
<tbody>
<tr>
<td>
Firefox 66
</td>
<td>
<span aria-label="supported">✅</span> (since FF 55, FFM 53)
</td>
<td>
<span aria-label="supported">✅</span>
</td>
<td>
<span aria-label="supported">✅</span>
</td>
</tr>
<tr>
<td>
Safari 16
</td>
<td>
<span aria-label="supported">✅</span>
</td>
<td>
<span aria-label="supported">✅</span>
</td>
<td>
<span aria-label="supported">✅</span>
</td>
</tr>
<tr>
<td>
Chrome 107
</td>
<td>
<span aria-label="supported">✅</span> (since Chrome 68)
</td>
<td>
<span aria-label="supported">✅</span>
</td>
<td>
<span aria-label="supported">✅</span>
</td>
</tr>
<tr>
<td>
Edge 107
</td>
<td>
<span aria-label="supported">✅</span> (since Edge 79)
</td>
<td>
<span aria-label="supported">✅</span>
</td>
<td>
<span aria-label="supported">✅</span>
</td>
</tr>
</tbody>
</table>
<h2>Other demos</h2>
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="jOxRdzw" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/jOxRdzw">
Animated Mondrian (CSS grid-template-columns|rows interpolation)</a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="XWqVowx" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/XWqVowx">
Animated CSS Grid</a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+97%3A+animating+grids%E2%80%9D">blog@matuzo.at</a>.</p> Day 98: oklab() and oklch()2023-02-08T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day98
<p><code>oklab()</code> and <code>oklch()</code> are okay versions of <code>lab()</code> and <code>lch()</code> because <code>lab()</code> and <code>lch()</code> are not okay.</p><p>I will not pretend that I really understand this whole color on the web thing, how it works or why one color function offers many more options to developers than the other, but I did learn several things during this experiment. I understand why a color function like <a href="/blog/2023/hsl-custom-properties/"><code>hsl()</code> offers better DX than <code>rgb()</code></a>. I’ve learned that <a href="/blog/2022/100daysof-day30/">rgb(), hsl(), and hwb()</a> use colors from the sRGB color space, and <a href="/blog/2022/100daysof-day23/">lab()</a> and <a href="/blog/2023/100daysof-day93/">lch()</a> colors from the CIELAB color space. These color functions are relevant now because they support more colors and modern monitors can display them. It kinda also makes sense to me why some people say that using <a href="https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl#oklch-vs-oklab--lch-vs-lab">lch() is more intuitive than lab()</a>.</p>
<p>This doesn’t sound too bad, but as soon as I dig deeper into colors, I soon reach a point where I’m out. It’s usually a graph or complicated formula that stops my enthusiasm. Why am I saying this? I don’t know, maybe self-defense, maybe to make you feel better and assure you it’s fine to not understand everything.</p>
<p>Anyway, now I’ve added <code>hsl()</code>, <code>hwb()</code>, <code>lab()</code>, and <code>lch()</code> to my tool belt, and along comes <s>mary</s> <code>oklab()</code> and <code>oklch()</code>. lab and lch are great, but not perfect. The main issue with lab and lch is that there's a bug with blue colors which turns blue purple. </p>
<img alt="3 shades of blue compared in lch and oklch. oklch colors look blue while lch colors turn purple." height="738" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day98/761d72ba19-1698950539/100days-98.png" width="462">
<p><code>oklab()</code> and <code>oklch()</code> fix that.</p>
<figure>
<img alt="Two triangles that are constant-hue slice of LCH and OKLCH spaces with the same hue. The LCH slice, the leftmost triangular shape, is blue on one side and purple on the other. The right shape, OKLCH, keeps a constant hue, as expected." height="364" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day98/f3754465c2-1698950539/lch-vs-oklch.avif" width="912">
<figcaption>Source: <a href="https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl">evilmartians.com</a></figcaption>
</figure>
<p>The okay versions of lab and lch come with additional improvements. You can learn more about it Chris Lilley's presentation “<a href="https://www.youtube.com/watch?v=dOsp6u4bIwI">Better than Lab? Gamut reduction CIE Lab & OKLab</a>”.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+98%3A+oklab%28%29+and+oklch%28%29%E2%80%9D">blog@matuzo.at</a>.</p> Day 99: native nesting2023-02-09T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day99
<p>Nesting in CSS is coming soon! For me personally not <em>the</em> killer feature, at least compared to <a href="/blog/2022/100daysof-day37/">cascade layers</a> or <a href="/blog/2022/100daysof-day56/">container queries</a>, but still exciting. Let’s see how it works.</p><p>The most important thing to know about native nesting in CSS is that the nested selector always must start with a symbol (. # : [ * + > ~) because of limitations in browser parsing engines.</p>
<p>The following code doesn't work:</p>
<pre><code class="language-css">/* Doesn't work */
ul {
li {
border-color: green;
}
}</code></pre>
<p>To work around that limitation the nested selector can start with an <code>&</code>.</p>
<pre><code class="language-css">ul {
& li {
border-color: green;
}
}
/*
Same as:
ul li { }
*/</code></pre>
<p>Besides this limitation, everything works as expected for me. Here are some of the things I've tested:</p>
<pre><code class="language-css">a {
&:hover {
background-color: aqua;
}
&:focus-visible {
background-color: aqua;
}
}
/*
Same as:
a:hover { }
a:focus-visible { }
*/</code></pre>
<pre><code class="language-css">a {
&:is(:hover, :focus-visible) {
background-color: aqua;
}
}
/*
Same as:
a:is(:hover, :focus-visible) { }
*/</code></pre>
<pre><code class="language-css">h2 {
font-family: sans-serif;
&::first-letter {
color: red;
}
}
/*
Same as:
h2 { }
h2::first-letter { }
*/</code></pre>
<pre><code class="language-css">h2 {
& + p {
background-color: red;
}
}
/*
Same as:
h2 + p { }
*/</code></pre>
<pre><code class="language-css">h2 {
.parent & {
background-color: aqua;
}
}
/*
Same as:
.parent h2 { }
*/</code></pre>
<pre><code class="language-css">h2 {
@media (min-width: 400px) {
background: red;
}
}
/*
Same as:
@media (min-width: 400px) {
h2 { }
}
*/</code></pre>
<pre><code class="language-css">h2 {
@media (min-width: 400px) {
background: red;
&::before {
content: "!";
color: #fff;
}
& ~ p {
& span {
background-color: #000;
}
:is(span) {
color: #fff;
}
}
}
}
/*
Same as:
@media (min-width: 400px) {
h2 { }
h2::before { }
h2 ~ p span { }
h2 ~ p :is(span) { }
}
*/</code></pre>
<pre><code class="language-css">div {
& & & h3 {
background-color: green;
}
}
/*
Same as:
div div div h3 { }
*/</code></pre>
<pre><code class="language-css">h3 {
:is(div) & {
color: #fff;
}
}
/*
Same as:
:is(div) h3 { }
*/</code></pre>
<pre><code class="language-css">a {
&[download] {
border: 1px solid red;
}
}
/*
Same as:
a[download] { }
*/</code></pre>
<p>You can try it today in <a href="https://www.google.com/chrome/dev/">Chrome Dev</a>, <a href="https://developer.apple.com/safari/technology-preview/">Safari Technology Preview</a>, or <a href="https://polypane.app/blog/polypane-13-css-nesting-extension-support-in-beta-search-by-selector-and-chromium-110/">Polypane 13</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+99%3A+native+nesting%E2%80%9D">blog@matuzo.at</a>.</p> Why I'm not the biggest fan of Single Page Applications2023-02-10T00:00:00+00:00https://www.matuzo.at/blog/2023/single-page-applications-criticism
<p>Sometimes it seems like accessibility experts and other web professionals hate JavaScript. This might be true for some, but most understand that JavaScript can be useful for improving UX and even accessibility. JavaScript solutions are often more accessible than their <a href="https://daverupert.com/2020/02/html-the-inaccessible-parts/">pure HTML</a> or <a href="https://www.smashingmagazine.com/2021/06/css-javascript-requirements-accessible-components/">CSS counterparts</a>.</p><p>We know <a href="https://www.marcozehe.de/javascript-not-enemy-accessibility/">JavaScript is not an enemy</a>, but, admittedly, there is a certain reluctance towards building client-side rendered websites by some developers. These people are often dismissed as the “old guard” because of their seemingly irrational aversion towards modern web development. If you don’t understand where this is coming from, it might seem incomprehensible why someone would not want to build single page applications (SPA), but go for the standard way of building websites, sometimes referred to as multi page applications (MPA), instead. </p>
<p>There has been a lot of criticism about SPAs and especially React recently. On a personal level, I can absolutely understand that. I don’t like Meta and their business model; I don’t like how React thought leaders <a href="https://infrequently.org/2023/02/the-market-for-lemons/#what-did-they-know-and-when-did-they-know-it%3F">sold their library as something that it’s not</a>, and I don’t like the bro culture that sparked from this whole thing. On a professional level, I understand that there are people for whom React is nothing more than a tool or a .js file. People who, you know,…just build websites for a living. Although I’m not happy about it, I also understand that JS libraries and frameworks are often the entry point to web development for new web developers. Nevertheless, there is also valid and serious professional criticism. With this post I’m not trying to convince anyone to build their websites differently, I just want to share my view and my experiences as someone who has been building websites for over 20 years, and I try to summarize reasons why some developers, including me, are wary of building SPAs. Before I give you some examples, let me say one important thing: I know that it’s possible to create accessible single page applications and I know that there are people who work with JS frameworks (because they want to or have to), and optimise the shit out of their websites because they care about their users.</p>
<hr>
<p>A lot of the scepticism comes from the fact that some critical features of a website work fundamentally different in single page applications. </p>
<h2>Routing</h2>
<p>When you create 3 HTML documents, let’s call them home.html, about.html, and dashboard.html, and you link them, clicking a link the following happens: The browser navigates to the new page, the page loads, the title of the page changes in the browser’s tab, focus is on the body, and, if you’re using a screen reader, the software announces the title of the page. Of course, a lot more happens. This is a simplified and incomplete depiction of the process, but the important bit is that no matter how you’re accessing and activating the link (mouse, screen reader, touch, <a href="https://youtu.be/V1yoOLhx_qA">switch device</a>, etc.), you know what happened, that something happened, and where you are. The loading process and the transition from one page to another tell you that you’ve navigated to a new page. Focus is at the beginning of the DOM, where you would expect it, and your screen reader announces the title of the page automatically.</p>
<figure>
<video src="/blog/2023/single-page-applications-criticism/routing_default.mov" controls></video>
<figcaption>When I click a link, NVDA, the screen reader I'm using, announces the title of the page, for example “Dashboard - My Website”.</figcaption>
</figure>
<p>In a Single Page Application, you create only one HTML document and you replace the main content of the page when the user clicks a link. To achieve that, you may have to use a routing library like React Router. Compared to the process of native routing I’ve described earlier, the following happens: DOM content changes, focus is still on the link, the title in the browser tab doesn’t change, and a screen reader announces nothing. If you’re lucky, you know what happened, that something happened, and where you are. If you’re a blind screen reader user, you’ll most likely get no feedback at all.</p>
<figure>
<video src="/blog/2023/single-page-applications-criticism/routing_spa.mov" controls></video>
<figcaption>When I click a link, NVDA announces nothing.</figcaption>
</figure>
<p>While not intentionally, single page applications, at least in terms of page navigation, are inaccessible by design. The creators of reach router did their best to make routing accessible by managing focus, but since data loading and focus management on route transitions are coupled, it was <a href="https://github.com/remix-run/react-router/discussions/9555?sort=new">too complicated to do it correctly</a>. Despite all their efforts, routing became completely inaccessible again, when <a href="https://reactrouter.com/en/main/upgrading/reach">reach router and react router</a> merged and they stopped <q>doing not-good-enough focus management by default</q>.</p>
<p>Custom routing and focus management done right can actually work even better than the native behaviour without JavaScript, but it must be built manually. You don’t see that in many SPAs unfortunately. I’d rather serve users with the not so great, but consistent default experience than no experience at all.<br />
Anyway, there's a lot we can learn from SPAs and from people with accessbility in mind working with them. There’s fantastic research about that topic by Marcy Sutton (“<a href="https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/">What we learned from user testing of accessible client-side routing techniques with Fable Tech Labs</a>”).</p>
<p><strong>Note</strong>: In the previous demo I was using “Create React App”, one of the <a href="https://reactjs.org/docs/create-a-new-react-app.html#recommended-toolchains">recommended toolchains</a> and the <a href="https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic?file=src/main.tsx">basic example in React Router</a>. I can imagine that there are frameworks that support accessible routing by default.</p>
<h2>The document title</h2>
<p>The <code><title></code> of the page is one of the most important elements in a HTML document. Users benefit from a well-formed and descriptive page title in many ways. </p>
<p>To mitigate inaccessible routing and the missing feedback after clicking a link, screen reader users help themselves by pressing shortcuts that announce the title of the page. The page title tells them on which page they are. So even if there’s no feedback, they know they’re on a new page when the title has changed. If the title has changed! In a SPA, you usually have to implement title management manually. Again, this is something that just works in native routing. </p>
<div class="table-wrapper" aria-labelledby="form1" tabindex="0" role="region">
<table>
<caption id="form1">Screen reader shortcuts for announcing the page title</caption>
<thead>
<tr>
<th>Screen Reader</th>
<th>Command</th>
<th>Announcement</th>
</tr>
</thead>
<tbody>
<tr>
<td>NVDA</td>
<td><kbd>Ins</kbd> + <kbd>T</kbd></td>
<td>Page title</td>
</tr>
<tr>
<td>JAWS</td>
<td><kbd>Ins</kbd> + <kbd>T</kbd></td>
<td>Page title</td>
</tr>
<tr>
<td>Voice Over on macOS</td>
<td><kbd>VO</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
<td>Page summary, including page title</td>
</tr>
<tr>
<td>Voice Over on macOS</td>
<td><kbd>VO</kbd> + <kbd>F2</kbd></td>
<td>Page title</td>
</tr>
</tbody>
</table>
</div>
<p>There are more reasons you'd want to change the page title. It serves as the label for bookmarked pages/favorites, and search engines use the title in their search results pages. Social media sites, chat or mail applications, and similar software use the title in link previews when no other title is specified.</p>
<p>Before you start sending me links: I know that it's possible, I've done my research. My point is, that it's yet another thing you have to reimplement manually. </p>
<ul>
<li><a href="https://hidde.blog/accessible-page-titles-in-a-single-page-app/">Accessible page titles in a Single Page App</a> by Hidde de Vries</li>
<li><a href="https://github.com/gaearon/react-document-title">React Document Title</a></li>
<li><a href="https://github.com/nfl/react-helmet">React Helmet</a></li>
<li><a href="https://github.com/nuxt/vue-meta">Vue-meta</a></li>
</ul>
<h2>DOM changes</h2>
<p>DOM changes are not specific to SPAs, but to using JavaScript in general. The thing is that reactive DOM changes are at the very core of single page applications. For every significant change you make, you have to communicate that change not just visually, but also semantically. <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">Live regions</a>, ARIA attributes, or focus management can help with that. You won’t find any information about live regions or using <code>focus()</code> for announcing DOM changes in the <a href="https://reactjs.org/docs/accessibility.html">official React accessibility docs</a>, the <a href="https://vuejs.org/guide/best-practices/accessibility.html">Vue accessibility docs</a>, or the <a href="https://angular.io/guide/accessibility">Angular docs</a>.<br />
The more JavaScript we add to our page, the more fragile our interface gets and the more we have to manage and communicate manually, if we want to create <a href="https://inclusivedesignprinciples.org/#provide-comparable-experience">equal experiences for all</a>.</p>
<h2>JSX</h2>
<p>JSX kinda looks like HTML, but it’s not really HTML because it’s a mix of HTML with component tags and made-up attributes (I’m talking about you <code>className</code>). I know that React doesn’t require using JSX, but many new devs learn React and JSX first (and only), because boot camps and indoctrinated communities tell them it's the only real way to build websites and they need to know React in order to get a job. This abstraction can be terrible if you don’t understand the difference between the actual standard (HTML) and the abstraction layer. We’re already <a href="https://www.youtube.com/watch?v=Wno1IhEBTxc">really bad at writing native HTML</a> and a language that obfuscates the standard language doesn’t make things better.</p>
<pre><code class="language-js"><li className="item">
{isPacked ? (
<del>
{name + ' ✔'}
</del>
) : (
name
)}
</li></code></pre>
<p>Before we choose to work with any language that is not native HTML, but compiles to HTML, we should <a href="https://web.dev/learn/html/">learn HTML</a> first. This doesn’t just apply to JSX, but Markdown, Pug, NunJucks, etc. Our input will be much better if we know how the output should look like.</p>
<h2>The page context</h2>
<p>In SPAs, you create components. A component might be part of another component, and another component until it finally ends up in the wrapper <code><div></code> of your index.html. If you look at the final document of most SPAs, the page doesn’t look like a coherent entity, but more like a dump for components that happen to form a page visually. As already mentioned, HTML is a cornerstone of any accessible website and the page structure (a sound heading outline, landmarks, DOM order, semantic HTML in general) plays an important role. </p>
<p>Don’t get me wrong, I also build components that are part of a system, but I also spend a good amount of time crafting pages that work well. The fact that the HTML document is something that you barely touch, because everything you need in there will be injected via JavaScript, puts the document and the page structure out of focus.<br />
I don’t suggest that a component-based approach is wrong, not even one that is written in a JavaScript environment, quite the opposite, but just because it’s more convenient and efficient to work with a JS framework, doesn’t mean that we have to serve all that JavaScript to the client. The traditional way of serving websites entirely on the client is outdated, but I also don’t believe that concepts like hydration are much better.<br />
It’s worth exploring approaches that separate concerns and try to minimize the amount of JS shipped. Some notable projects are <a href="https://lit.dev/">Lit</a>, <a href="https://astro.build/">Astro</a>, <a href="https://svelte.dev/">Svelte</a>, or <a href="https://github.com/11ty/webc">WebC</a>.</p>
<h2>Performance</h2>
<p>Most SPAs probably perform well on my 3,500 Euro laptop and my 500 Euro smartphone, but a Macbook Pro and Huawai P30 are not necessarily the average device the average user uses to access the web. I’m not in the position to talk about the performance implications of client-site rendering and especially React, but there has been <a href="https://www.zachleat.com/web/react-criticism/">a lot of criticism early on</a> because SPAs just don’t perform as well as thought leaders at Meta or Vercel <a href="https://github.com/reactjs/reactjs.org/pull/5487#issuecomment-1409720741">used to make you think</a>.</p>
<p>I suggest you read the following articles:</p>
<ul>
<li><a href="https://infrequently.org/2022/12/performance-baseline-2023/">The Performance Inequality Gap, 2023</a></li>
<li><a href="https://infrequently.org/2023/02/the-market-for-lemons/">The Market for Lemons</a></li>
<li><a href="https://www.zachleat.com/web/react-criticism/">A historical reference of react criticism</a></li>
<li><a href="https://andy-bell.co.uk/speed-for-who/">Speed for who?</a></li>
</ul>
<h2>Complexity</h2>
<p>Setting up a SPA from scratch is not simple, it involves many tedious steps. Even if you use one of the boilerplate setups, you’re handling a compound dependency monster that just waits to ruin your day because one tiny detail doesn’t work out of the box the way you want it. There’s so much complexity in these things that it makes some people doubt whether it’s worth the effort and if it’s not maybe smarter to spend the time working on something else, something that actually benefits users.</p>
<p>I guess the biggest criticism here is that it feels like people who believe in the superiority of single page applications and the entire ecosystem focus more on developer experience (DX) than user experience. That sounds like a dangerous blanket statement, but after all these years, I never had the feeling that the argument “better DX leads to better UX” was ever true. It’s nothing more than a justification for the immense complexity and potentially significantly worse UX. And even if the core argument isn't DX, other arguments like scalability, maintainability, competitive ability, easier recruiting (“everyone uses React”), and cost effectiveness, in my experience, only sound good, but rarely hold up to their promises.</p>
<h1>Conclusion</h1>
<p>I hope this didn’t come off as a rant, it’s not. I’m just trying to explain why SPAs are not the first choice or no choice at all for some people. I’m glad that this diverse landscape of tools exists because we can learn from innovation and use this knowledge to create better experiences. I’ve worked with several JS libraries and frameworks, because it pays the bills, but I’ve never used one for my own projects and I’ve never consulted a client to do so, and I probably never will. The simple reason is that I build websites most of the time, and not complex applications. There is a place and time for JS frameworks and client side rendering, but it’s usually not the right choice for the typical website.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWhy+I%27m+not+the+biggest+fan+of+Single+Page+Applications%E2%80%9D">blog@matuzo.at</a>.</p> Day 100: it's over, or is it!?2023-02-10T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day100
<p>OMG, I did it, day 100! 4 months and 16 days ago I published the first post and then I wrote another post every workday for 138 days straight without missing a single day. In this final post, I want to do a quick recap and give an outlook for what's coming next.</p><h2>Recap</h2>
<p>Starting this project was one of the best and worst ideas I ever had. </p>
<p>It was a great idea because I’m finally up to date with modern CSS. I’ve learned so so much, not just about the fancy new stuff, but also about CSS in general. Based on the feedback I’ve received, I wasn’t the only one who benefited from this project. This format, short, focused posts about a single topic, worked well for many people. That makes me happy. Thank you for reading and participating!</p>
<p>It was a bad idea because it was so much work and it put quite a lot of pressure on me at times. Each post took me at least 1 hour to write, sometimes up to 4 hours. In some weeks, that meant spending an entire working day or more writing these posts on top of my other work. Consequently, the quality of some posts is not as good as I’d like it to be.</p>
<p>Anyway, I’m glad that I finished it. Especially because I’m someone who tends to abandon their side projects quickly since the next more exciting project is already around the corner. </p>
<h2>My favorite posts</h2>
<p>I’m happy with how most posts I’ve written have turned out, but, of course, I have some favorites.</p>
<h3>Top 3 favorite new features</h3>
<p>I’ve covered a lot of new features in CSS, but I’m most excited about these.</p>
<ul>
<li><a href="https://www.matuzo.at/blog/2022/100daysof-day37/">cascade layers (day37)</a></li>
<li><a href="/blog/2022/100daysof-day56/">container queries (day 56)</a></li>
<li><a href="/blog/2023/100daysof-day84/">@property (day 84)</a></li>
</ul>
<h3>Top 3 favorite posts</h3>
<p>Like I said, this experiment turned out to not just focus on modern CSS, but core CSS concepts, too. These are 3 of my favorite posts.</p>
<ul>
<li><a href="/blog/2023/100daysof-day82/">value processing (day 82)</a></li>
<li><a href="/blog/2023/100daysof-day74/">!important (day 74)</a></li>
<li><a href="/blog/2023/100daysof-day85/">typed custom properties (day 85)</a></li>
</ul>
<h3>Top 3 most useful properties/values</h3>
<p>There are certain new features that (will) make my life as a frontend dev much easier.</p>
<ul>
<li><a href="/blog/2022/100daysof-day38/">dvh (day 38)</a></li>
<li><a href="/blog/2023/100daysof-day92/">relative color syntax (day 92)</a></li>
<li><a href="/blog/2022/100daysof-day20/">scrollbar-gutter (day 20)</a></li>
</ul>
<h3>Top 3 most surprising features</h3>
<p>I honestly knew nothing about, I don’t know, 80%? of the stuff I wrote about before I did my research. Some things surprised me.</p>
<ul>
<li><a href="/blog/2023/100daysof-day75/">font palettes (day 75)</a></li>
<li><a href="/blog/2023/100daysof-day80/">container style queries (day 80)</a></li>
<li><a href="/blog/2023/100daysof-day88/">CSS Motion Path (day 88)</a></li>
</ul>
<p>Soooo, now that I’m done, what’s next? Well, I’ll probably not write about CSS too soon again, haha. My personal next step is to implement many of these features and techniques in the websites I’m building to get some actual hands-on experience. Once I have that, I can write blog posts with a bit more substance. I will also talk about my insights as <a href="https://cssday.nl/2023">CSS Day in June</a>.</p>
<h2>Interop 2023</h2>
<p>I’m looking forward to learning what’s coming next to the web platform. We can track the progress in the <a href="https://wpt.fyi/interop-2023?stable">Interop 2023 Dashboard</a>. This year’s focus areas are: </p>
<ul><li><a href="https://developer.mozilla.org/docs/Web/CSS/border-image" rel="noopener">Border Image in CSS</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/color_value" rel="noopener">Color Spaces and Functions in CSS</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_Container_Queries" rel="noopener">Container Queries in CSS</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_Containment" rel="noopener">Containment in CSS</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes" rel="noopener">CSS Pseudo-classes</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/@property" rel="noopener">Custom Properties in CSS</a></li><li><a href="https://developer.mozilla.org/docs/Learn/CSS/CSS_layout/Flexbox" rel="noopener">Flexbox</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/@supports#function_syntax" rel="noopener">Font feature detection</a> and <a href="https://developer.mozilla.org/docs/Web/CSS/font-palette" rel="noopener">palettes</a></li><li><a href="https://developer.mozilla.org/docs/Web/HTML/Element/form" rel="noopener">Forms</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_Grid_Layout" rel="noopener">Grid</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/:has" rel="noopener">:has()</a></li><li><a href="https://developer.mozilla.org/docs/Web/API/HTMLElement/inert" rel="noopener">Inert</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_Masking" rel="noopener">Masking in CSS</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_Functions#math_functions" rel="noopener">Math Functions in CSS</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/Media_Queries/Using_media_queries" rel="noopener">Media Queries</a></li><li><a href="https://developer.mozilla.org/docs/Web/API/Web_Workers_API/Using_web_workers" rel="noopener">Modules in Web Workers</a></li><li><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_Motion_Path" rel="noopener">Motion Path in CSS Animations</a></li><li><a href="https://developer.mozilla.org/docs/Web/API/OffscreenCanvas" rel="noopener">Offscreen Canvas</a></li><li><a href="https://developer.mozilla.org/docs/Web/API/Pointer_events" rel="noopener">Pointer and mouse Events</a></li><li><a href="https://developer.mozilla.org/docs/Web/API/URL" rel="noopener">URL</a></li><li>Web Compat 2023: A catchall focus area for small bugs that cause known site compatibility issues</li><li><a href="https://developer.mozilla.org/docs/Web/API/WebCodecs_API" rel="noopener">Web Codecs (video)</a></li><li><a href="https://developer.mozilla.org/docs/Web/Web_Components" rel="noopener">Web Components</a></li></ul>
<p>Alright, that's it. Thank you so much for following along and being a part of this. You can find a collection of <a href="https://codepen.io/collection/Yyxyad">all demos on CodePen</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+100%3A+it%27s+over%2C+or+is+it%21%3F%E2%80%9D">blog@matuzo.at</a>.</p> My CSS wish list2023-02-14T00:00:00+00:00https://www.matuzo.at/blog/2023/css-wish-list
<p>I know I’m late to the party, but there are a few things on my CSS wish list I haven’t seen on others, so I thought I’d share.</p><h2>Visually hidden content</h2>
<p>I'd love to see a native implementation of visually hidden text. I’m not the biggest fan of hiding stuff only for some, but it’s inevitable sometimes.</p>
<p>Instead of this:</p>
<pre><code class="language-css">.visually-hidden {
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}</code></pre>
<p>I want this:</p>
<pre><code class="language-css">.visually-hidden {
visibility: visually-hidden;
}</code></pre>
<p>Or another example:</p>
<pre><code class="language-css">.skip-link {
position: absolute;
visibility: visually-hidden;
}
.skip-link:focus-visible {
visibility: visible;
}</code></pre>
<h2>Alternative text for pseudo elements</h2>
<p>It would be great if I could exclude generated content from the accessibility tree or give it an accessible name.</p>
<pre><code class="language-css">button::before {
content: "c";
font-family: MyIconFont;
alt: "";
}</code></pre>
<p>There is actually an implementation in some browsers that looks like this:</p>
<pre><code class="language-css">label:has(+[required])::after {
content: '★' / 'required';
}</code></pre>
<p>Or even better:</p>
<pre><code class="language-css">label:has(+[required])::after {
content: '★' / attr(data-label-required);
}</code></pre>
<h3>Related posts</h3>
<ul>
<li><a href="/blog/heres-what-i-didnt-know-about-content/">Here’s what I didn’t know about “content”</a></li>
<li><a href="https://adrianroselli.com/2020/10/alternative-text-for-css-generated-content.html">Alternative Text for CSS Generated Content</a></li>
<li><a href="https://www.stefanjudis.com/today-i-learned/css-content-accepts-alternative-text/">The CSS "content" property accepts alternative text</a></li>
</ul>
<h2>Block links</h2>
<p>So-called <a href="https://adrianroselli.com/2020/02/block-links-cards-clickable-regions-etc.html">block links</a> is something that’s still unsolved in CSS. Let’s say you have a card with a heading, text, image, and a link. In order to avoid accessibility issues, you only want to include a single link and you don’t want to wrap all elements in that link, but you still want the entire card to be clickable. There are different ways of doing that, but there are drawbacks to all of them. I’d basically want to have a native implementation of Heydons <a href="https://inclusive-components.design/cards/#thepseudocontenttrick">pseudo-content trick</a>. </p>
<p>Maybe something like this:</p>
<pre><code class="language-html"><div class="card">
<h2>Heading</h2>
<img src="" alt="">
<p>asdasd</p>
<a href="#">Link</a>
</div></code></pre>
<pre><code class="language-css">.card {
container-type: block-link;
}</code></pre>
<h2>Bleeding backgrounds</h2>
<p>When you define a fixed width and a background color for the <code><body></code>, the background fills the entire viewport and not just the body. I think that’s because the property is propagated to the viewport, <code><body></code> is an exception with this behavior. Anyway, I want a similar behavior for other elements, too, so that I can make the background of an element expand in all directions.</p>
<pre><code class="language-css">div {
max-width: 960px;
background-color: hotpink;
background-bleed: inline;
}</code></pre>
<p>It should kinda work like this solution I stole from <a href="https://github.com/SelenIT">Ilya Streltsyn</a>. I have absolutely no idea what’s going on, but it works.</p>
<pre><code class="language-css">div {
border-image: conic-gradient( hotpink 0 0) fill 1//0 50vw
}</code></pre>
<style>
[data-sample] div {
border-image: conic-gradient( hotpink 0 0) fill 1//0 50vw
}
</style>
<div data-sample="demo">
<div>
test
</div>
</div>
<p>PS: <em>Bleeding backgrounds</em> is a fantastic name for a band.</p>
<h2>Other wish lists</h2>
<p>There’s more on my list, but it has been already covered by others.</p>
<ul>
<li>From Chris’ wish list: <a href="https://chriscoyier.net/2022/12/21/things-css-could-still-use-heading-into-2023/#animate-to-auto">animate to auto</a></li>
<li>From Tyler’s list: <a href="https://cloudfour.com/thinks/tylers-css-wish-list-for-2023/">View Transitions API</a></li>
<li>From Dave’s list: <a href="https://daverupert.com/2023/01/css-wishlist-2023/">leading-trim</a></li>
<li>From Eric’s list: <a href="https://meyerweb.com/eric/thoughts/2023/02/08/css-wish-list-2023/#more-attr">More attr()</a></li>
<li>From Ahmad’s list: <a href="https://ishadeed.com/article/css-wishlist-2023/#detect-when-sticky-is-active">sticky detection</a></li>
</ul>
<h2>In the works</h2>
<p>And then there’s other stuff already partially implemented that I can’t wait to get better support.</p>
<ul>
<li><a href="/blog/2023/100daysof-day84/">@property</a></li>
<li><a href="/blog/2022/100daysof-day56/">container size and style queries</a></li>
<li><a href="/blog/2022/100daysof-day38/">dvh</a></li>
<li><a href="/blog/2022/100daysof-day6/">has()</a></li>
<li><a href="/blog/2023/100daysof-day92/">relative color syntax</a></li>
</ul>
<p>I believe we have no right to complain. We’re super spoiled, especially with the stuff happening around Interop 2022 and 2023, but these are just examples of things that would make my life easier. I’d love to hear what’s on your wish list, too.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CMy+CSS+wish+list%E2%80%9D">blog@matuzo.at</a>.</p> 50.1% empty links2023-03-04T00:00:00+00:00https://www.matuzo.at/blog/2023/empty-links
<p>The new <a href="https://webaim.org/projects/million/">WebAim 1 Million report</a> was recently published, and the results are sobering. Compared to the previous year, 0.5% fewer websites contained automatically detectable accessibility issues, but the total number of erroneous websites is still 96.3%.</p><p>The number of empty links increased by 0.4% from 49.7% to 50.1%. More than half of the websites tested contained empty links (links with no text).<br />
That usually happens when you link an image, but it has no alt attribute or the alt attribute has no value. </p>
<p>To give you an idea of how this affects the user experience and accessibility, I tested the following empty links with screen readers.</p>
<pre><code class="language-html"><a href="https://webaim.org/projects/million/">
<img src="/images/screenshot-04-03-23_copy.jpg">
</a></code></pre>
<p>Here's what different screen readers announce using the Tab key on desktop and touch on mobile.</p>
<table>
<caption>Linked image with no alt</caption>
<thead>
<tr>
<th>Screen reader</th>
<th>Browser</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>NVDA</td>
<td>Firefox</td>
<td><strong>million, graphic, link</strong></td>
</tr>
<tr>
<td>JAWS</td>
<td>Firefox</td>
<td><strong>H T T P S colon slash slash webaim dot org slash projects slash million slash</strong></td>
</tr>
<tr>
<td>JAWS</td>
<td>Edge</td>
<td><strong>images slash screenshot dash 04 dash 03 dash 23 underline copy, link, graphic</strong></td>
</tr>
<tr>
<td>Talkback</td>
<td>Chrome</td>
<td><strong>screenshot the 3rd of April 23 underscore copy, link</strong></td>
</tr>
<tr>
<td>VoiceOver macOS</td>
<td>Safari</td>
<td><strong>link, million </strong></td>
</tr>
<tr>
<td>VoiceOver macOS</td>
<td>Chrome</td>
<td><strong>unlabelled image</strong></td>
</tr>
<tr>
<td>VoiceOver macOS</td>
<td>Firefox</td>
<td><strong>link, image, million </strong></td>
</tr>
<tr>
<td>VoiceOver iOS</td>
<td>Safari</td>
<td><strong>million, link</strong></td>
</tr>
</tbody>
</table>
<pre><code class="language-html"><a href="https://webaim.org/projects/million/">
<img src="/images/screenshot-04-03-23_copy.jpg" alt="">
</a></code></pre>
<table>
<caption>Linked image with empty alt</caption>
<thead>
<tr>
<th>Screen reader</th>
<th>Browser</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>NVDA</td>
<td>Firefox</td>
<td><strong>million, link</strong></td>
</tr>
<tr>
<td>JAWS</td>
<td>Firefox</td>
<td><strong>link, H T T P S colon slash slash webaim dot org slash projects slash million slash</strong></td>
</tr>
<tr>
<td>JAWS</td>
<td>Edge</td>
<td><strong>million, link</strong></td>
</tr>
<tr>
<td>Talkback</td>
<td>Chrome</td>
<td><strong>million, link</strong></td>
</tr>
<tr>
<td>VoiceOver macOS</td>
<td>Safari</td>
<td><strong>link, million</strong></td>
</tr>
<tr>
<td>VoiceOver macOS</td>
<td>Chrome</td>
<td><strong>link, million</strong></td>
</tr>
<tr>
<td>VoiceOver macOS</td>
<td>Firefox</td>
<td><strong>link, million</strong></td>
</tr>
<tr>
<td>VoiceOver iOS</td>
<td>Safari</td>
<td><strong>million, link</strong></td>
</tr>
</tbody>
</table>
<p>That sucks. At best, screen reader users can only guess what they can expect when they click an empty link. </p>
<h2>How can you avoid repeating these mistakes?</h2>
<p>Test your sites at least with an automatic testing tool like <a href="https://www.deque.com/axe/devtools/">axe</a>, <a href="https://developer.chrome.com/docs/lighthouse/overview/">Lighthouse</a>, or <a href="https://wave.webaim.org/">Wave</a>, and label linked graphics. I’ve described several ways in <a href="/blog/2022/button-baader/">“Buttons and the Baader–Meinhof phenomenon.”</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9C50.1%25+empty+links%E2%80%9D">blog@matuzo.at</a>.</p> It's very likely that…2023-04-17T00:00:00+00:00https://www.matuzo.at/blog/its-very-likely-1
<p>I repeatedly see certain bad practices in HTML that ironically contain clues for implementing them properly in their class names or in the way they're built. In this evergreen post, I collect them.</p><p>…if you're using <code>javascript:void(0)</code> as the value of the <code>href</code> attribute, the element you're actually looking for is <code><button></code>. </p>
<pre><code class="language-html"><a href="javascript:void(0);">
Open modal
</a></code></pre>
<p>More accessible alternative:</p>
<pre><code class="language-html"><button>
Open modal
</button></code></pre>
<h2>Explanation</h2>
<p>The role of <code><a></code> is <code>link</code> and the role of <code><button></code> is <code>button</code>. Users have certain expectations when they find an element. A general rule of thumb: Use a link if it takes you somewhere else. Use a button if you submit a form or run JavaScript.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CIt%27s+very+likely+that%E2%80%A6%E2%80%9D">blog@matuzo.at</a>.</p> Invalid at computed-value time2023-04-18T00:00:00+00:00https://www.matuzo.at/blog/2023/invalid-custom-properties
<p>I rewatched <a href="https://www.youtube.com/watch?v=ZuZizqDF4q8">Lea Verous’s talk about custom properties</a> recently and learned something I missed the first time I watched it.</p><p>A declaration of a custom property can be <a href="https://www.w3.org/TR/css-variables-1/#invalid-variables">invalid at computed-value time</a>, if its value is invalid. Depending on the property’s type, this results in the property being set to <code>unset</code>, so either the property’s inherited value or its initial value, depending on whether the property is inherited or not.</p>
<p>That’s confusing, I know; here’s an example to better understand why it’s essential to know that.</p>
<p>If we select a button and set its <code>border</code> and <code>background</code> to an invalid value, nothing happens to the button. The browser just throws away the entire declaration.</p>
<style>
.sample1 button {
button {
border: bla;
background: bla;
}
}
.sample2 button {
--bla: bla;
border: var(--bla);
background: var(--bla);
}
</style>
<pre><code class="language-css">button {
border: bla;
background: bla;
}</code></pre>
<div data-sample="demo" class="sample1">
<button>test</button>
</div>
<p>If we do the same but now put the invalid value in a custom property instead, the button looks different because the custom property is <em>invalid at computed-value time</em> and falls back to <code>unset</code>, which for <code>background</code> and <code>border</code> means <code>initial</code> since they're not <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Inheritance#inherited_properties">inherited properties</a>.</p>
<pre><code class="language-css">
button {
--bla: bla;
border: var(--bla);
background: var(--bla);
}</code></pre>
<div data-sample="demo" class="sample2">
<button>test</button>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CInvalid+at+computed-value+time%E2%80%9D">blog@matuzo.at</a>.</p> details/summary inconsistencies2023-04-21T00:00:00+00:00https://www.matuzo.at/blog/2023/details-summary
<p>Scott O'Hara wrote a <a href="https://www.scottohara.me/blog/2022/09/12/details-summary.html">fantastic blog post about the details and summary elements</a> last year. He explains that there are a lot of oddities and inconsistencies, and he backs his statements with <a href="https://codepen.io/scottohara/pen/aaJXYG">detailed testing</a>.</p><p>To better understand the extent of these oddities and inconsistencies, I did <a href="https://codepen.io/matuzo/pen/XWxNxyg?editors=1100">my own testing</a> (not as detailed as Scott's), and here's what I found:</p>
<ul>
<li>Announcements are very different across different screen readers/browsers. It goes from little information (“show more” in VoiceOver on iOS) to too much information (“Right pointing triangle, Show more, collapsed, summary, group” in Firefox on macOs)</li>
<li>Removing or changing the triangle doesn’t seem to affect any screen reader/browser pairing except Firefox with all tested screen readers.</li>
<li>Voice Over macOS Chrome/Edge/Arc, Voice Over macOS Safari, and Talkback Android Chrome provide the most consistent experience.</li>
<li>Voice Over iOS Safari is also very consistent but in a bad way. It doesn’t announce any role or state.</li>
<li>Details only expands in Chrome, Arc, Edge when you search with <kbd>CMD</kbd>/<kbd>Ctrl</kbd> + <kbd>F</kbd> (find-in-page).</li>
<li>To remove the triangle in Safari, you must set <code>::-webkit-details-marker</code> to <code>display: none</code>. <br><code>::marker</code> or <code>list-style: none;</code> don't work.
</li>
</ul>
<p>What should you do with this information?</p>
<p>I don't know, probably test with users?! I'll just quote Scott here:</p>
<blockquote>
<p>If your goal is to create an absolutely consistent disclosure widget behavior across browsers, i.e., ensuring that all <code><summary></code>s are exposed as expand/collapse buttons, then you’d be better off creating your own using JavaScript and the necessary ARIA attributes. You’d lose out on the find-in-page functionality that the native disclosure widget provides… but depending on the type of disclosure widget you’re creating, you may actually want that.</p>
</blockquote>
<style>
.nomarker summary {
list-style-type: none;
}
.nomarker2 summary::marker,
.nomarker2 summary::-webkit-details-marker {
content: "";
}
.custommarker summary {
list-style-type: square;
}
.custommarker2 summary::marker,
.custommarker2 summary::-webkit-details-marker {
content: "🦫"
}
.custommarker3 summary::-webkit-details-marker {
display: none;
}
.custommarker3 summary {
list-style: none;
}
thead th {
white-space: nowrap;
}
td {
vertical-align: top;
}
</style>
<h2>OS/browsers/software</h2>
<ul>
<li>macOS 13.0.1 / Windows 11 / Android 13 / iOS 15.7.5</li>
<li>Chrome 112</li>
<li>Firefox 112</li>
<li>Safari 16.1 (macOS)</li>
<li>JAWS 2023.2212.13</li>
<li>NVDA 2023.1</li>
</ul>
<h2>Results</h2>
<h3>default</h3>
<pre><code class="language-html"><details>
<summary>Show More</summary>
<p>Here’s Johnny!</p>
</details></code></pre>
<details>
<summary>Show More</summary>
<p>Here’s Johnny!</p>
</details>
<div class="table-wrapper" aria-labelledby="form1" tabindex="0" role="region">
<table>
<caption id="form1">Screen reader testing with default element</caption>
<thead>
<tr>
<td></td>
<th>Voice Over macOS Chrome/Edge/arc</th>
<th>Voice Over macOS Firefox</th>
<th>Voice Over macOS Safari</th>
<th>Voice Over iOS Safari</th>
<th>Talkback Android Chrome</th>
<th>NVDA Firefox</th>
<th>JAWS Chrome</th>
</tr>
</thead>
<tbody>
<tr>
<th>
Announcement on focus
</th>
<td>
Show More, collapsed, disclosure triangle, group
</td>
<td>
Right pointing triangle, Show more, collapsed, summary, group<br>
or<br>
Show more, collapsed, summary, group
</td>
<td>Show More, collapsed, summary, group</td>
<td>Show More</td>
<td>
Collapsed, show more, disclosure triangle
</td>
<td>
Filled right pointing small triangle, show more, button, collapsed
</td>
<td>Show more, button, collapsed</td>
</tr>
<tr>
<th>
Announcement on toggle
</th>
<td>
Show More, expanded, disclosure triangle, group
</td>
<td>
Show More
</td>
<td>Show More, expanded, summary, group</td>
<td>Show More</td>
<td>
expanded
</td>
<td>
expanded, filled down pointing small triangle, show more
</td>
<td>expanded</td>
</tr>
</tbody>
</table>
</div>
<h3>custom marker: <code>list-style-type: square</code></h3>
<pre><code class="language-css">summary {
list-style-type: square;
}</code></pre>
<details class="custommarker">
<summary>Show More</summary>
<p>Here’s Johnny!</p>
</details>
<div class="table-wrapper" aria-labelledby="form2" tabindex="0" role="region">
<table>
<caption id="form2">Screen reader testing with custom marker</caption>
<thead>
<tr>
<td></td>
<th>Voice Over macOS Chrome/Edge/arc</th>
<th>Voice Over macOS Firefox</th>
<th>Voice Over macOS Safari</th>
<th>Voice Over iOS Safari</th>
<th>Talkback Android Chrome</th>
<th>NVDA Firefox</th>
<th>JAWS Chrome</th>
</tr>
</thead>
<tbody>
<tr>
<th>
Announcement on focus
</th>
<td>
Show More, collapsed, disclosure triangle, group
</td>
<td>
Black small square, Show more, collapsed, summary, group<br>
or<br>
Show more, collapsed, summary, group
</td>
<td>Show More, collapsed, summary, group</td>
<td>Show More</td>
<td>
Collapsed, show more, disclosure triangle
</td>
<td>
Show more, button, collapsed
</td>
<td>Show more, button, collapsed</td>
</tr>
<tr>
<th>
Announcement on toggle
</th>
<td>
Show More, expanded, disclosure triangle, group
</td>
<td>
/
</td>
<td>Show More, expanded, summary, group</td>
<td>Show More</td>
<td>
expanded
</td>
<td>
expanded
</td>
<td>expanded</td>
</tr>
</tbody>
</table>
</div>
<h3>custom marker: <code>::marker</code></h3>
<pre><code class="language-css">summary::marker {
content: "🦫"
}</code></pre>
<details class="custommarker2">
<summary>Show More</summary>
<p>Here’s Johnny!</p>
</details>
<div class="table-wrapper" aria-labelledby="form3" tabindex="0" role="region">
<table>
<caption id="form3">Screen reader testing with custom marker</caption>
<thead>
<tr>
<td></td>
<th>Voice Over macOS Chrome/Edge/arc</th>
<th>Voice Over macOS Firefox</th>
<th>Voice Over macOS Safari</th>
<th>Voice Over iOS Safari</th>
<th>Talkback Android Chrome</th>
<th>NVDA Firefox</th>
<th>JAWS Chrome</th>
</tr>
</thead>
<tbody>
<tr>
<th>
Announcement on focus
</th>
<td>
Show More, collapsed, disclosure triangle, group
</td>
<td>
Beaver, Show more, collapsed, summary, group<br>
or<br>
Show more, collapsed, summary, group
</td>
<td>Show More, collapsed, summary, group</td>
<td>Show More</td>
<td>
Collapsed, show more, disclosure triangle
</td>
<td>
Beaver, Show more, button, collapsed
</td>
<td>Show more, button, collapsed</td>
</tr>
<tr>
<th>
Announcement on toggle
</th>
<td>
Show More, expanded, disclosure triangle, group
</td>
<td>
Beaver, Show More
</td>
<td>Show More, expanded, summary, group</td>
<td>Show More</td>
<td>
expanded
</td>
<td>
expanded
</td>
<td>expanded</td>
</tr>
</tbody>
</table>
</div>
<h3>no marker: <code>list-style-type: none;</code></h3>
<pre><code class="language-css">summary {
list-style-type: none;
}</code></pre>
<details class="nomarker">
<summary>Show More</summary>
<p>Here’s Johnny!</p>
</details>
<div class="table-wrapper" aria-labelledby="form4" tabindex="0" role="region">
<table id="form4">
<caption>Screen reader testing with no marker</caption>
<thead>
<tr>
<td></td>
<th>Voice Over macOS Chrome/Edge/arc</th>
<th>Voice Over macOS Firefox</th>
<th>Voice Over macOS Safari</th>
<th>Voice Over iOS Safari</th>
<th>Talkback Android Chrome</th>
<th>NVDA Firefox</th>
<th>JAWS Chrome</th>
</tr>
</thead>
<tbody>
<tr>
<th>
Announcement on focus
</th>
<td>
Show More, collapsed, disclosure triangle, group
</td>
<td>
Show more, collapsed, summary, group
</td>
<td>Show More, collapsed, summary, group</td>
<td>Show More</td>
<td>
Collapsed, show more, disclosure triangle
</td>
<td>
Show more, button, collapsed
</td>
<td>Show more, button, collapsed</td>
</tr>
<tr>
<th>
Announcement on toggle
</th>
<td>
Show More, expanded, disclosure triangle, group
</td>
<td>
/
</td>
<td>Show More, expanded, summary, group</td>
<td>Show More</td>
<td>
expanded
</td>
<td>
expanded
</td>
<td>expanded</td>
</tr>
</tbody>
</table>
</div>
<h3>no marker: <code>::marker content:""</code></h3>
<pre><code class="language-css">summary::marker {
content: "";
}</code></pre>
<details class="nomarker2">
<summary>Show More</summary>
<p>Here’s Johnny!</p>
</details>
<div class="table-wrapper" aria-labelledby="form5" tabindex="0" role="region">
<table id="form5">
<caption>Screen reader testing with no marker</caption>
<thead>
<tr>
<td></td>
<th>Voice Over macOS Chrome/Edge/arc</th>
<th>Voice Over macOS Firefox</th>
<th>Voice Over macOS Safari</th>
<th>Voice Over iOS Safari</th>
<th>Talkback Android Chrome</th>
<th>NVDA Firefox</th>
<th>JAWS Chrome</th>
</tr>
</thead>
<tbody>
<tr>
<th>
Announcement on focus
</th>
<td>
Show More, collapsed, disclosure triangle, group
</td>
<td>
Show more, collapsed, summary, group
</td>
<td>Show More, collapsed, summary, group</td>
<td>Show More</td>
<td>
Collapsed, show more, disclosure triangle
</td>
<td>
Show more, button, collapsed
</td>
<td>Show more, button, collapsed</td>
</tr>
<tr>
<th>
Announcement on toggle
</th>
<td>
Show More, expanded, disclosure triangle, group
</td>
<td>
/
</td>
<td>Show More, expanded, summary, group</td>
<td>Show More</td>
<td>
expanded
</td>
<td>
expanded
</td>
<td>expanded</td>
</tr>
</tbody>
</table>
</div>
<h3>Remove triangle in all browsers</h3>
<pre><code class="language-css">summary::-webkit-details-marker {
display: none;
}
summary {
list-style: none;
}</code></pre>
<details class="custommarker3">
<summary>Show More</summary>
<p>Here’s Johnny!</p>
</details><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9Cdetails%2Fsummary+inconsistencies%E2%80%9D">blog@matuzo.at</a>.</p> Workshop: Deep Dive on Accessibility Testing2023-05-23T00:00:00+00:00https://www.matuzo.at/blog/2023/sm-a11y-testing
<p>Once again I’ve teamed up with my friends at <a href="https://www.smashingmagazine.com/">Smashing Magazine</a> 😻 to share with you everything I know about web accessibility testing! In this <a href="https://smashingconf.com/online-workshops/workshops/manuel-matuzovic/">smashing workshop</a> we’ll talk about automatic and manual testing, screen reader basics, Single Page Applications, Dev Tools, and more.</p><!-- teaser -->
<p>Sounds interesting? Great! Here are some more details about the workshop:</p>
<video src="/images/workshop_promo.mp4" controls>
<track default kind="captions" srclang="en" src="/images/workshop_promo.vtt" label="English">
<track default kind="subtitles" srclang="de" src="/images/workshop_promo_de.vtt" label="Deutsch">
Sorry, your browser doesn't support embedded videos.
</video>
<h2>What you will learn in this workshop</h2>
<ul><li>Which <strong>testing tools</strong> are available and most commonly used.</li><li>How to <strong>assess the accessibility</strong> of a component or page.</li><li>The difference between <strong>automatic and manual testing</strong>.</li><li>How to use <strong>automatic accessibility testing</strong> tools and how to interpret the results.</li><li>How to use the <strong>keyboard to discover accessibility bugs</strong>.</li><li><strong>Screen reader basics</strong> and how to use them for accessibility testing, both on desktop and mobile devices.</li><li>How to test the accessibility of <strong>Single Page Applications</strong>.</li><li>Common <strong>pitfalls of Single Page Applications</strong> and how to avoid them.</li><li>How to <strong>integrate accessibility testing</strong> in your day-to-day development workflow.</li><li>Running <strong>tests on the command line</strong> and creating automated reports.</li><li><strong>Integrating accessibility testing in your build pipeline</strong>.</li><li>Where to find <strong>information and help on how to build complex components</strong>.</li></ul>
<h2>Time and schedule</h2>
<p>This workshop is split over <strong>five days</strong>. The workshop sessions will run on the following days:</p>
<ul><li>Mon, June 19, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Tue, June 20, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Mon, June 26, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Tue, June 27, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Mon, July 3, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li></ul>
<h2>Ticket</h2>
<p>If you use this <a href="https://ti.to/smashingmagazine/online-workshops-2022/discount/welcometomyworkshop">special link</a> you get a 15% discount on the original price.</p>
<p>If you have any questions about the workshop, feel free to get in touch <a href="manuel@matuzo.at">via mail</a>. I hope to see you soon!</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWorkshop%3A+Deep+Dive+on+Accessibility+Testing%E2%80%9D">blog@matuzo.at</a>.</p> Deleted files in a freshly cloned git repo2023-05-29T00:00:00+00:00https://www.matuzo.at/blog/2023/deleted-files-clone
<p>The other day someone emailed me and told me that they tried to clone the <a href="https://github.com/matuzo/HTMHell">HTMHell repo</a>, but they only got an empty folder (except for the hidden .git folder), and all files were deleted as if they cloned the repo and immediately moved all files into the trash.</p><style>
li {
margin: 0 !important;
}
</style>
<p>I couldn't reproduce the issue on my Mac, but I had the same problem on my Windows machine. This <a href="https://stackoverflow.com/a/61079472">post on StackOverflow</a> suggested that it was related to using forbidden characters in file names. </p>
<div class="highlight">
<p>Forbidden characters in file names on Windows:</p>
<ul>
<li>< (less than)</li>
<li>> (greater than)</li>
<li>: (colon)</li>
<li>" (double quote)</li>
<li>/ (forward slash)</li>
<li>\ (backslash)</li>
<li>| (vertical bar or pipe)</li>
<li>? (question mark)</li>
<li>* (asterisk)</li>
</ul>
</div>
<p>I reviewed the files and found an image named <em>30-bullet-"list".png</em>. I have a node script that automatically creates the social media preview images for blog posts and uses the title as the file name. I forgot to take care of special characters.</p>
<p>I tweaked the script and renamed the file, and – voilà – cloning now works as expected on Windows.</p>
<p>Edit: <a href="https://front-end.social/@tanepiper@tane.codes/110450742495407620">Tane added on social media</a> that he had an issue raised once on one of his repos because he was using an emoji in a filename and it broke on OSX.</p>
<p>Conclusion: Don't use special characters, spaces, or emojis in your file names.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDeleted+files+in+a+freshly+cloned+git+repo%E2%80%9D">blog@matuzo.at</a>.</p> CSS! CSS! CSS!2023-06-11T00:00:00+00:00https://www.matuzo.at/blog/2023/css-css-css
<p>I just came home after three beautiful days in Amsterdam, where I gave a talk at the CSS Day conference. I’ve watched many inspirational and engaging presentations and had many interesting conversations. My biggest takeaway: <strong>The CSS community needs you!</strong></p><p>First things first: <a href="https://cssday.nl/2023">CSS Day</a> is a wonderful event, and the community is lovely. If you can, consider attending it!</p>
<p><a href="https://nerdy.dev/">Adam Argyle</a> was the MC on the first day, and after every talk, he chanted, <strong>“CSS! CSS! CSS!“</strong>. It was silly but funny. It was Adam’s way of showing appreciation for how far we’ve come with the language and how powerful it is.</p>
<p>After the first day, I was chatting about CSS in a bar with some friends. Stephan said it’s great that browsers are shipping so many new features, but we need people to use them in real projects, share their experiences in talks and articles, and show the world what CSS is capable of.<br />
I agree because most of us still need to understand how groundbreaking some of these additions to the language are. That can only change if we’re curious and experiment, share what we’ve learned, and discuss it, but it’s not enough to rely on people like Stephanie Eckles, Ahmad Shadeed, Michelle Barker, Adam Argyle, Bramus Van Damme, Una Kravets, or Kevin Powell to do that. Especially with CSS-Tricks dying slowly*, we need more people to give CSS a stage.<br />
We also agreed that we need more people to push CSS to its limits and explore what else we can get from Grid, custom properties, :has(), container queries, etc., beyond the obvious use cases.</p>
<p>That’s where I ask you a favour: <strong>Please write more about CSS.</strong> Show us what you’ve built and how you build it. The CSS community needs more voices, ideas, and solutions.</p>
<img alt="Screenshot of Unas Talk. Her on the left on the right a slide that says 'organize a css meetup/event'" height="491" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/css-css-css/a596ee439f-1698950547/csscsscss.jpg" width="873">
<p>That's not the only favour I have to ask you. </p>
<p>In her talk, Una Kravets informed us that there's only 1 CSS conference. <a href="https://dev.events/">dev.events</a> currently lists 53 JavaScript and 0 CSS events for the rest of the year. How is that possible?<br />
My friends Björn and Jan started the <a href="https://www.meetup.com/css-in-vienna/">CSS-in-Vienna</a> meetup several years ago and I absolutely love it. Meeting with like-minded folks regularly and talking about CSS is fun, helpful, and motivating. We need more conferences and meetups that focus on CSS.</p>
<p>The second favour I have to ask you is to <strong>organize a CSS event in your hometown</strong>!</p>
<p>If you've written an article about CSS or if you are organizing an event, send me a link, and I'll share it!</p>
<p><em>* I’m usually not a pessimist, but I don’t see how CSS-Tricks could get back on its feet, and I fear that it could meet a <a href="https://build.typogram.co/p/dont-sell-your-indie-business-to">similar fate like scotch.io</a> which also got acquired by Digital Ocean.</em></p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CCSS%21+CSS%21+CSS%21%E2%80%9D">blog@matuzo.at</a>.</p> Syntax podcast episode 623: “Nothing in CSS” errata2023-06-12T00:00:00+00:00https://www.matuzo.at/blog/2023/syntax-fm-623-errata
<p>I just listened to the Syntax podcast for the first time because they were discussing topics near and dear to my heart, HTML and CSS. The episode is called “Nothing in CSS - 0 vs 0px, no, none, hidden, initial and unset”, and they’re talking about all the things that can be 0, none, or hidden in CSS and HTML. Super interesting stuff, but unfortunately, they got some things wrong. The don‘t have a comment section, so I’m commenting on the episode here in case someone else gets confused by their misinformation.</p><p><strong>Disclaimer:</strong> I don't want to shame them; we're all humans and make mistakes, but their podcast reaches many people, and I feel like I can't keep their statements uncommented.</p>
<h2>4:15 visibility hidden and screen readers</h2>
<p>Wes said, as opposed to <code>display: none</code> <q><code>visibility: hidden</code> is not hidden from screen readers</q> and he suggested to add <code>aria-hidden="true"</code> to make sure these elements are hidden from screen readers.<br />
You don't have to add the attribute because <code>visibility: hidden</code> already removes an element from the accessibility tree.</p>
<p><img src="/images/syntax-errata.jpg" alt="Dev Tools accessibility panel showing that a visibility: hidden element is not exposed to the accessibility tree" /></p>
<h2>4:35 opacity: 0</h2>
<p>Wes then continued and said, <q>If you put <code>opacity: 0</code>, [...], that's kinda the same thing as <code>visibility: hidden</code>.</q>.<br />
Not really, they're different because an element with <code>opacity</code> set to <code>0</code> is still accessible to screen readers. Also, interactive items within an element with <code>visibility</code> set to <code>hidden</code> are not focusable. On the other hand, items within an element with <code>opacity</code> set to <code>0</code> are.</p>
<pre><code class="language-css">div:focus-within {
visibility: visible !important;
opacity: 1 !important;
}</code></pre>
<pre><code class="language-html"><!-- div never visible because the button is not focusable -->
<div style="visibility: hidden">
<button>nope</button>
</div>
<!-- div visible when the button is focused -->
<div style="opacity: 0">
<button>yep</button>
</div></code></pre>
<h2>10:30 ghost spaces</h2>
<p>Wes suggests two solutions to get around the “issue” where you have two images and want them to be in one line visually but without the <em>ghost space</em>, or how I like to call them: <em>space</em>, that gets added automatically.</p>
<ol>
<li><strong>Remove the space in HTML.</strong><br />
I’d consider that a bad practice because you shouldn’t rely on the position of your tags within your document for styling.</li>
<li><strong>Set the font size of a parent element to 0 and reset it on the element</strong><br />
Setting the font size to 0 solely for layout purposes is also a bad practice. That can confuse anyone else who works on the same code base and mess with accessibility and inheritance.</li>
</ol>
<p>If you want to align elements horizontally, use Flexbox.</p>
<h2>11:45 border</h2>
<p>Scott said that <code>border: 0</code> and <code>border: none</code> are effectively doing the same thing.<br />
That's not entirely true. <code>border</code> is a shorthand property, and <code>border: 0</code> sets the <code>border-width</code> property to <code>0</code> while <code>border: none</code> sets the <code>border-style</code> property to <code>none</code>.</p>
<h2>14:10 none and 0 in color functions</h2>
<p>Scott explains how there's no difference between using 0 or none in color functions. I'm not a color expert, but what he said sounded wrong, so I did what they should've done; I read the specs.<br />
0 represents the amount 0, while none represents a missing color component. A <a href="https://www.w3.org/TR/css-color-4/#missing">missing component</a> behaves as a zero value unless you interpolate it. Let me just quote the spec here:</p>
<blockquote>
“In the course of converting the two colors to the interpolation color space, any missing components will be replaced with the value 0.
Thus, the first stage in interpolating two colors is to classify any missing components in the input colors, and compare them to the components of the interpolation color space. If any analogous missing components are found, they will be carried forward and re-inserted in the converted color before linear interpolation takes place.”
</blockquote>
<blockquote>
“If a color with a carried forward missing component is interpolated with another color which is not missing that component, the missing component is treated as having the other color’s component value.”
</blockquote>
<p>In the following example, you can see how <code>.none</code> and <code>.zero</code> are different. <code>.none</code> and <code>.value</code> are the same because <code>.none</code> uses the h color component from <code>--color-a</code>.</p>
<style>
.none0 {
display: flex;
min-block-size: 100px;
--color-a: oklch(78.3% 0.108 200);
--color-b-l: 39.2%;
--color-b-c: 0.4;
}
.none0 > * {
flex-grow: 1;
}
.none {
background-color: color-mix( in oklch, var(--color-a), oklch(var(--color-b-l) var(--color-b-c) none));
}
.zero {
background-color: color-mix( in oklch, var(--color-a), oklch(var(--color-b-l) var(--color-b-c) 0));
}
.value {
background-color: color-mix( in oklch, var(--color-a), oklch(var(--color-b-l) var(--color-b-c) 200));
}
</style>
<pre><code class="language-css">.none0 {
--color-a: oklch(78.3% 0.108 200);
--color-b-l: 39.2%;
--color-b-c: 0.4;
}
.none {
background-color: color-mix( in oklch, var(--color-a), oklch(var(--color-b-l) var(--color-b-c) none));
}
.zero {
background-color: color-mix( in oklch, var(--color-a), oklch(var(--color-b-l) var(--color-b-c) 0));
}
.value {
background-color: color-mix( in oklch, var(--color-a), oklch(var(--color-b-l) var(--color-b-c) 200));
}</code></pre>
<pre><code class="language-html"><div class="none0">
<div class="none"></div>
<div class="zero"></div>
<div class="value"></div>
</div></code></pre>
<div class="none0">
<div class="none"></div>
<div class="zero"></div>
<div class="value"></div>
</div>
<h2>23:40 unset and initial</h2>
<p>Scott explains unset and initial like that: </p>
<blockquote>
<p><code>unset</code> will revert to the inherited value and it will ignore anything set directly on the element itself, where <code>initial</code> will revert back to the browser default itself.</p>
</blockquote>
<p>He explains it some more and then they summarize it by saying that <code>unset</code> inherits a value and <code>initial</code> reverts to the default browser styles. Both statements are wrong.</p>
<p>It's true that <code>unset</code> resets a property to its inherited value, but only if the property naturally inherits from its parent. If not, it uses its initial value.</p>
<p><code>initial</code> sets the value of the property to its <em>initial value</em>. Each property has an <em>initial value</em>, defined in the property’s definition table. For example, if you look at the <a href="https://w3c.github.io/csswg-drafts/css-color/#the-color-property">color property in the specification</a>, you see that the defined initial value in the definition table is <a href="https://w3c.github.io/csswg-drafts/css-color/#css-system-colors">CanvasText</a>.<br />
<strong>The initial value is not the default value of the property</strong> defined in the user agent (browser). For example, the default margin of the body in most (all?) browsers is <code>8px</code>, but the initial value of the margin property is <code>0</code>.</p>
<p>I explain that in detail in <a href="/blog/2022/100daysof-day63/">Day 63: explicit defaulting with inherit, initial, unset, and revert</a>.</p>
<h2>29:40 role=none</h2>
<p>Scott and Wes tried to explain what <code>role=none</code> does by explaining what <code>role=button</code> does.</p>
<p>Wes said <q>I know that this is just a regular old div and this is just ignored by screen readers…</q></p>
<p>The div is not ignored by screen readers, it just has no semantic meaning. Screen reader users can still read the text. A div is not very different to a paragraph.<br />
He continues with:</p>
<p><q>…but if I put a role equals button, it will show the browsers and screen readers ‘okay, this is something that's clickable and should be focusable‘.</q></p>
<p>I found that statement confusing. To be clear: <code>role=button</code> doesn't add any interactivity; it only gives the element a semantic role. You need <code>tabindex="0"</code> to make it focusable.</p>
<p>They also use <code>input</code> as an example of an element where you might want to use <code>role=none</code>, but according to the spec <a href="https://www.w3.org/TR/wai-aria/#conflict_resolution_presentation_none">user agents must ignore <code>role=none</code> and <code>role=presentation</code> on interactive elements</a>.</p>
<h2>30:34 honeypot technique and screen readers</h2>
<p>Scott and Wes discussed whether <code>role=none</code> would be a good fit for the honey pot technique in forms.</p>
<p>You don't want to use <code>role=none</code> for hiding form inputs because it has no effect when applied to the element directly. Applied to a parent, it probably doesn't do what you expect. Instead, you want <code>aria-hidden="true"</code> because this removes an element from the accessibility tree, but you must not use <code>aria-hidden="true"</code> on interactive elements or elements that contain interactive elements.<br />
Long story short: don't use either, and maybe also avoid the honeypot technique.</p>
<h2>32:30 boolean, keyword and enumerated attributes</h2>
<p>Wes said <q>With all HTML attributes, except for ARIA attributes, the simple existence of the attribute will make it true.</q>. </p>
<p>That's only true for boolean attributes in HTML, not keyword and enumerated attributes. Here's an example:</p>
<pre><code class="language-html"><div contenteditable="false">Not editable</div></code></pre>
<p>You can read more about that in Hidde's post <a href="https://hidde.blog/boolean-attributes-in-html-and-aria-whats-the-difference/">Boolean attributes in HTML and ARIA: what's the difference?</a>.</p>
<h2>Bonus</h2>
<p>Since I'm already doing this, I thought I'd add some general comments.</p>
<h3>4:48 visiblity: hidden transition</h3>
<p>Scott said <q>You can't really fade-in <code>visibility: hidden</code></q>. That's true, but it is animatable, meaning you can combine it with <code>transform</code> or <code>opacity</code>.</p>
<h3>27:55 user-scaleable: no</h3>
<p>It's true that <code>user-scaleable: no</code> <a href="https://www.matuzo.at/blog/2022/please-stop-disabling-zoom/">prevents users from zooming</a>, but only on some mobile browsers.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CSyntax+podcast+episode+623%3A+%E2%80%9CNothing+in+CSS%E2%80%9D+errata%E2%80%9D">blog@matuzo.at</a>.</p> Day 101: scoping2023-06-16T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day101
<p>Similar to container queries or cascade layers, we have another new impactful feature in CSS: scoping.</p><p>Let's start nice and easy by reading the <a href="https://drafts.csswg.org/css-cascade-6/#scoped-styles">spec</a>.</p>
<blockquote>
<p>A scope is a subtree or fragment of a document, which can be used by selectors for more targeted matching. </p>
</blockquote>
<p><em>More targeted matching</em> sounds great. We can use the <code>@scope</code> rule to scope styles of a child to a parent selector.</p>
<pre><code class="language-html"><div class="wrapper">
<div class="card">
<div class="content">
the cascade is unavoidable
</div>
</div>
</div></code></pre>
<pre><code class="language-css">@scope(.card) {
.content {
border: 5px solid red;
}
}</code></pre>
<p>Okay, cool, but we could do that already, right? By combining selectors, we can scope a child to a parent.</p>
<pre><code class="language-css">.card .content {
border: 5px solid red;
}</code></pre>
<p>The biggest difference in this simple example is that the cascade prioritizes declarations with a more proximate scoping root, regardless of specificity or source order.<br />
The background color will be blue in the following example because of the order of appearance.</p>
<pre><code class="language-css">.card .content {
background-color: green;
}
.wrapper .content {
background-color: blue;
}
/* -> blue */</code></pre>
<p>In the following example, it's green because before the order of appearance can say anything, scope proximity compares declarations that appear in style rules with different scoping roots and picks the declaration with the fewest generational or sibling-element hops between the scoping root (<code>.card</code> or <code>.wrapper</code>) and the scoped style rule subject (<code>.content</code>). In other words: the cascade prioritizes declarations with a more proximate scoping root.</p>
<pre><code class="language-css">@scope (.card) {
.content {
background: green;
}
}
@scope (.wrapper) {
.content {
background: blue;
}
}
/* -> green */</code></pre>
<p>There's more to proximity than that, but we'll save that for another day.</p>
<p>That's not everything. <code>@scope</code> allows us to define scope limits, which I'll cover on another day. That's where it gets really interesting, but before we can talk about that, here are a couple of things to note:</p>
<ol>
<li>
Selectors within a scoped style rule can only match elements that are in scope, but this applies only to the subject (the actual selector that matches the element). Any other selector in the selector list can be out of scope.
<pre><code class="language-css"> @scope(.card) {
/*
`.content` (the subject) must be in scope
`body` is not in scope and doesn't have to
*/
body & .content {
border: 5px solid red;
}
}</code></pre>
</li>
<li>
Selectors within a scope block are relative to the scope.
<pre><code class="language-css"> @scope(.card) {
/*
works with the markup structure at the beginning
of this post because it selects `.card .content`
*/
.content {
}
/*
doesn't work because it selects
`.card .card .content`
*/
.card .content {
}
}</code></pre>
</li>
<li>
<p>Unlike Nesting, selectors within an scoped style rule do not acquire the specificity of any parent selector(s) in the <code>@scope</code> prelude.</p>
<pre><code class="language-css"> @scope(.card) {
/* the specificity of .content is (0, 1, 0) */
.content {
}
}</code></pre>
</li>
</ol>
<p>To try out <code>@scope</code> you have to download <a href="https://www.google.com/chrome/canary/">Chrome Canary</a> and enable the <code>Experimental Web Platform features </code> flag in chrome://flags/.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+101%3A+scoping%E2%80%9D">blog@matuzo.at</a>.</p> Cascade Layers are useless*2023-06-21T00:00:00+00:00https://www.matuzo.at/blog/2023/cascade-layers-are-useless
<p>*if you don‘t understand the problems they solve and use them in combination with other solutions that tackle the same challenges albeit less elegantly and with the downside of limiting you in taking full advantage of selectors, one of the coolest features in CSS, and if you ignore the fact that they can help you organise and manage your own and third-party code.</p><p>I've heard several people say they've tried Cascade Layers but didn't see any changes, so they dropped them again. That can also easily happen to you when you structure your CSS in layers for the first time. I can explain why.</p>
<p>Let's say we have three layers (base, components, and utility) and three rules, one in each layer. It doesn't matter whether you have 3, 30, or 300 rules. The underlying principle I'm trying to explain applies regardless of the size of your CSS.</p>
<pre><code class="language-html"><label for="email">
E-Mail
</label>
<input type="email" class="form__input--error u-no-shadow" aria-invalid="true" required id="email"></code></pre>
<pre><code class="language-css">@layer base {
input {
--_border-color: #000;
border: 3px solid var(--_border-color);
box-shadow: 0 0 10px 2px rgb(0 0 0 / 0.4);
}
}
@layer components {
.form__input--error {
--_border-color: #F00;
}
}
@layer utility {
.u-no-shadow {
box-shadow: none !important;
}
}</code></pre>
<style>
label {
display: block;
font-weight: bold;
}
input {
display: block;
padding: 0.4em;
}
@layer base {
.demo1 input {
--_border-color: #000;
border: 3px solid var(--_border-color);
box-shadow: 0 0 10px 2px rgb(0 0 0 / 0.4);
}
}
@layer components {
.demo1 .form__input--error {
--_border-color: #F00;
}
}
@layer utility {
.demo1 .u-no-shadow {
box-shadow: none !important;
}
}
.demo2 input {
--_border-color: #000;
border: 3px solid var(--_border-color);
box-shadow: 0 0 10px 2px rgb(0 0 0 / 0.4);
}
.demo2 .form__input--error {
--_border-color: #F00;
}
.demo2 .u-no-shadow {
box-shadow: none !important;
}
.demo3 input[type="text"],
.demo3 input[type="email"] {
--_border-color: #000;
border: 3px solid var(--_border-color);
box-shadow: 0 0 10px 2px rgb(0 0 0 / 0.4);
}
.demo3 [aria-invalid="true"] {
--_border-color: #F00;
}
.demo3 .u-no-shadow {
box-shadow: none;
}
@layer base {
.demo4 input[type="text"],
.demo4 input[type="email"] {
--_border-color: #000;
border: 3px solid var(--_border-color);
box-shadow: 0 0 10px 2px rgb(0 0 0 / 0.4);
}
}
@layer components {
.demo4 [aria-invalid="true"] {
--_border-color: #F00;
}
}
@layer utility {
.demo4 .u-no-shadow {
box-shadow: none;
}
}
</style>
<p>You can see how the input field has a red border and no shadow.</p>
<div data-sample="demo" class="demo1">
<label for="email">
E-Mail
</label>
<input type="email" class="form__input--error u-no-shadow" size="30" required aria-invalid="true" id="email">
</div>
<p>If we remove the layers, the input field still looks the same.</p>
<pre><code class="language-css">input {
--_border-color: #000;
border: 3px solid var(--_border-color);
box-shadow: 0 0 10px 2px rgb(0 0 0 / 0.4);
}
.form__input--error {
--_border-color: #F00;
}
.u-no-shadow {
box-shadow: none !important;
}</code></pre>
<div data-sample="demo" class="demo2">
<label for="email2">
E-Mail
</label>
<input type="email" class="form__input--error u-no-shadow" size="30" required aria-invalid="true" id="email2">
</div>
<p>Strange, right? Now the question arises what the actual purpose of cascade layers is.<br />
Let's have a look at the <a href="https://drafts.csswg.org/css-cascade-5/#layering">spec</a> (emphasis mine):</p>
<blockquote>
<p>Authors can create layers to represent element defaults, third-party libraries, themes, components, overrides, and other styling concerns—and are able to re-order the cascade of layers in an explicit way, <strong>without altering selectors or specificity within each layer, or relying on order of appearance to resolve conflicts across layers</strong>.</p>
</blockquote>
<p>Aha! Now it makes much more sense why we don’t see any difference. If you analyse the CSS, you’ll know that we’ve altered selectors and specificity so that we don’t have any conflicts with the order of appearance.</p>
<p>We deliberately keep the specificity of our reset and base styles low to avoid issues with our component styles.</p>
<pre><code class="language-css">input { }</code></pre>
<p>We ensure that the specificity in our components is larger than in our base styles, and we keep the specificity flat (single-class selectors only).</p>
<pre><code class="language-css">.form__input--error { }</code></pre>
<p>We use <code>!important</code> to ensure that certain styles will <s>always</s> most of the time have precedence over other styles.</p>
<pre><code class="language-css">.u-no-shadow {
box-shadow: none !important;
}</code></pre>
<p>That’s fine, but it’s a hack because we’re restricting ourselves from using one of the most valuable features in CSS: selectors.<br />
CSS offers many practical ways of selecting elements. The complexity of the Cascade shouldn’t keep us from using them.<br />
Cascade Layers allow us to be more expressive with our selectors without worrying too much about their specificity. If we rewrite the CSS and use more explicit base styles, semantic selectors in our component styles, and no <code>!important</code> in our utilities, we get this:</p>
<pre><code class="language-css">input[type="text"],
input[type="email"] {
--_border-color: #000;
border: 3px solid var(--_border-color);
box-shadow: 0 0 10px 2px rgb(0 0 0 / 0.4);
}
[aria-invalid="true"] {
--_border-color: #F00;
}
.u-no-shadow {
box-shadow: none;
}</code></pre>
<div data-sample="demo" class="demo3">
<label for="email3">
E-Mail
</label>
<input type="email" class="u-no-shadow" size="30" required aria-invalid="true" id="email3">
</div>
<p>We're running into specificity issues. The colour of the border should be red, and there should be no shadow.<br />
That's where Cascade Layers can help. Our rules are scoped to each layer, and layers defined later in the document have precedence over layers defined earlier, regardless of the selectors used.</p>
<pre><code class="language-css">@layer base {
input[type="text"],
input[type="email"] {
--_border-color: #000;
border: 3px solid var(--_border-color);
box-shadow: 0 0 10px 2px rgb(0 0 0 / 0.4);
}
}
@layer components {
[aria-invalid="true"] {
--_border-color: #F00;
}
}
@layer utility {
.u-no-shadow {
box-shadow: none;
}
}</code></pre>
<div data-sample="demo" class="demo4">
<label for="email4">
E-Mail
</label>
<input type="email" class="u-no-shadow" size="30" required aria-invalid="true" id="email4">
</div>
<p>Tools like Cascade Layers and the <code>:where()</code> pseudo-class enable us to write more expressive CSS and use the language’s full range of selectors. For these tools to show their power, you can’t just drop them into your stylesheets and hope for magic to happen, you have to adapt how you think about the Cascade and how you write CSS.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CCascade+Layers+are+useless%2A%E2%80%9D">blog@matuzo.at</a>.</p> form and search landmarks2023-06-28T00:00:00+00:00https://www.matuzo.at/blog/2023/form-and-search-landmark
<p>I wanted to know how well common screen readers and browsers support <code>search</code> and <code>form</code> <a href="https://www.htmhell.dev/tips/landmarks/">landmarks</a>. Here are my results:</p><style>
table {
table-layout: fixed;
white-space: nowrap;
}
tbody td {
background: green;
color: #fff;
}
tbody th {
font-weight: normal;
/* position: sticky;
left: 0;*/
}
td.no {
background: #bf0404;
}
th a:is(:link, :visited) {
color: #fff;
}
th a:focus-visible {
outline-color: currentColor;
}
</style>
<h2>Software/OS/browser</h2>
<ul>
<li>NVDA 2023.1 with Firefox 114</li>
<li>VoiceOver macOS 13.4.1 with Safari 16.5.1</li>
<li>Talkback Android 13 with Chrome 114</li>
<li>JAWS 2023.2212.13 with Edge 114</li>
<li>Narrator Windows 10 with Edge 114</li>
<li>VoiceOver iOS 15.7.7 with Safari</li>
</ul>
<p>I tested using the following shorcuts, commands and gestures.</p>
<ul>
<li><kbd>D</kbd> key + Elements list in NVDA</li>
<li>Rotor in VO iOS</li>
<li>Rotor + single key quick nav in VO macOS</li>
<li>Swiping + landmark navigation in Talkback</li>
<li><kbd>R</kbd> key + landmarks list (<kbd>Insert</kbd> + <kbd>Ctrl</kbd> + <kbd>R</kbd> in JAWS</li>
<li><kbd>D</kbd> key + Landmarks List in Narrator</li>
</ul>
<h2>form role</h2>
<p><strong>Summary:</strong> You can use it, but forms won't be exposed as landmarks on VoiceOver and Talkback. To get the best results label the form.</p>
<div class="table-wrapper" aria-labelledby="form1" tabindex="0" role="region">
<table>
<caption id="form1">form role test results</caption>
<thead>
<tr>
<td></td>
<th>NVDA</th>
<th>Voice Over (macOS)</th>
<th>Talkback</th>
<th>Jaws</th>
<th>Narrator</th>
<th>VoiceOver (iOs)</th>
</tr>
</thead>
<tbody>
<tr>
<th><a href="#form1">no role and no label</a></th>
<td class="no">no landmark</td>
<td class="no">no landmark</td>
<td class="no" aria-describedby="sidenote">no landmark *</td>
<td class="no">no landmark</td>
<td class="no">no landmark</td>
<td class="no">no landmark</td>
</tr>
<tr>
<th><a href="#form2">no role, labelled by heading</a></th>
<td>form (labelled)</td>
<td class="no">no landmark</td>
<td class="no" aria-describedby="sidenote">no landmark *</td>
<td>form (labelled)</td>
<td>form landmark (labelled)</td>
<td class="no">no landmark</td>
</tr>
<tr>
<th><a href="#form3">role but no label</a></th>
<td>form (unlabelled)</td>
<td class="no">no landmark</td>
<td class="no" aria-describedby="sidenote">no landmark *</td>
<td class="no">no landmark</td>
<td>form landmark (unlabelled)</td>
<td class="no">no landmark</td>
</tr>
<tr>
<th><a href="#form4">role, labelled by heading</a></th>
<td>form (labelled)</td>
<td class="no">no landmark</td>
<td class="no" aria-describedby="sidenote">no landmark *</td>
<td>form (labelled)</td>
<td>form landmark (labelled)</td>
<td class="no">no landmark</td>
</tr>
<tr>
<th><a href="#form5">no role, labelled by legend</a></th>
<td>form (labelled)</td>
<td class="no">no landmark</td>
<td class="no" aria-describedby="sidenote">no landmark *</td>
<td aria-describedby="sidenote2">group (labelled) **</td>
<td>form landmark (labelled)</td>
<td class="no">no landmark</td>
</tr>
<tr>
<th><a href="#form6">role, labelled by legend</a></th>
<td>form (labelled)</td>
<td class="no">no landmark</td>
<td class="no" aria-describedby="sidenote">no landmark *</td>
<td aria-describedby="sidenote2">group (labelled) **</td>
<td>form landmark (labelled)</td>
<td class="no">no landmark</td>
</tr>
</tbody>
</table>
</div>
<p>* <span id="sidenote">not announced as a “form” landmark, but it's accessible via landmark navigation</span><br />
** <span id="sidenote">listed as “form” landmark in the landmarks list</span></p>
<h3 id="form1">form with no role and no label</h3>
<pre><code class="language-html"><form>
<h3>example 1</h3>
<label>
XY
<input type="text">
</label>
</form></code></pre>
<h3 id="form2">form with no role, labelled by heading</h3>
<pre><code class="language-html"><h3 id="form2">example2</h3>
<form aria-labelledby="form2">
<label>
XY
<input type="text">
</label>
</form></code></pre>
<h3 id="form3">form with form role and no label</h3>
<pre><code class="language-html"><form role="form">
<label>
XY
<input type="text">
</label>
</form></code></pre>
<h3 id="form4">form with form role labelled by heading</h3>
<pre><code class="language-html"><h3 id="form4">example 4</h3>
<form role="form" aria-labelledby="form4">
<label>
XY
<input type="text">
</label>
</form></code></pre>
<h3 id="form5">form with no role labelled by legend</h3>
<pre><code class="language-html"><form aria-labelledby="form5">
<fieldset>
<legend id="form5">example 5</legend>
<label>
XY
<input type="text">
</label>
</fieldset>
</form></code></pre>
<h3 id="form6">form with form role labelled by legend</h3>
<pre><code class="language-html"><form role="form" aria-labelledby="form6">
<fieldset>
<legend id="form6">example 6</legend>
<label>
XY
<input type="text">
</label>
</fieldset>
</form></code></pre>
<h2>search role</h2>
<p><strong>Summary:</strong> Great overall support for the <code>search</code> role. The <code><search></code> element has no support yet. You can use the search element in combination with the <code>role</code> attribute.</p>
<div class="table-wrapper" aria-labelledby="form2" tabindex="0" role="region">
<table>
<caption id="form2">search role test results</caption>
<thead>
<tr>
<td></td>
<th>NVDA</th>
<th>Voice Over (macOS)</th>
<th>Talkback</th>
<th>Jaws</th>
<th>Narrator</th>
<th>VoiceOver (iOs)</th>
</tr>
</thead>
<tbody>
<tr>
<th><a href="#search1">role but no label</a></th>
<td>search landmark (unlabelled)</td>
<td>search (unlabelled)</td>
<td>search (unlabelled)</td>
<td>search region (unlabelled)</td>
<td>search landmark (unlabelled)</td>
<td>search landmark (unlabelled)</td>
</tr>
<tr>
<th><a href="#search2">role, labelled by heading</a></th>
<td>search landmark (labelled)</td>
<td>search (labelled)</td>
<td>search (labelled)</td>
<td>search region (labelled)</td>
<td>search landmark (labelled)</td>
<td>search landmark (labelled)</td>
</tr>
<tr>
<th><a href="#search3">search element with no label</a></th>
<td class="no">no landmark</td>
<td class="no">no landmark</td>
<td class="no" aria-describedby="sidenote">no landmark *</td>
<td class="no">no landmark</td>
<td class="no">no landmark</td>
<td class="no">no landmark</td>
</tr>
<tr>
<th><a href="#search4">search element with role but no label</a></th>
<td>search landmark (unlabelled)</td>
<td>search (unlabelled)</td>
<td>search (unlabelled)</td>
<td>search region (unlabelled)</td>
<td>search landmark (unlabelled)</td>
<td>search landmark (unlabelled)</td>
</tr>
<tr>
<th><a href="#search5">search element with role, labelled by heading</a></th>
<td>search landmark (labelled)</td>
<td>search (labelled)</td>
<td>search (labelled)</td>
<td>search region (labelled)</td>
<td>search landmark (labelled)</td>
<td>search landmark (labelled)</td>
</tr>
</tbody>
</table>
</div>
<h3 id="search1">form with search role and no label</h3>
<pre><code class="language-html"><form role="search">
<label>
XY
<input type="text">
</label>
</form></code></pre>
<h3 id="search2">form with search role labelled by heading</h3>
<pre><code class="language-html"><h3 id="form8">example 8</h3>
<form role="search" aria-labelledby="form8">
<label>
XY
<input type="text">
</label>
</form></code></pre>
<h3 id="search3">search element with no label</h3>
<pre><code class="language-html"><search>
<form>
<label>
XY
<input type="text">
</label>
</form>
</search></code></pre>
<h3 id="search4">search element with search role and no label</h3>
<pre><code class="language-html"><search role="search">
<form>
<label>
XY
<input type="text">
</label>
</form>
</search></code></pre>
<h3 id="search5">search element with search role labelled by heading</h3>
<pre><code class="language-html"> <search role="search" aria-labelledby="form11">
<h3 id="form11">example 11</h3>
<form>
<label>
XY
<input type="text">
</label>
</form>
</search></code></pre><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9Cform+and+search+landmarks%E2%80%9D">blog@matuzo.at</a>.</p> the details element and in-page search2023-06-30T00:00:00+00:00https://www.matuzo.at/blog/2023/details-find-in-page
<p>An important factor in terms of UX and accessibility for deciding whether the <code><details></code> element is the right solution for a problem is the find-in-page behaviour.</p><p>In Chromium-based browsers, the details element automatically opens when it contains a string the user searches for.<br />
If Safari and Firefox, it has to be opened for the find-in-page feature to find the string.</p>
<img alt="Comparison of Chrome (left) and Firefox. Search for the term 'cook' shows two results in Chrome, details element opened by the browser. Firefox only shows one result, details element closed." height="563" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/details-find-in-page/8493d73759-1698950551/details-find-in-page.webp" width="1000">
<p>You can try it yourself in this <a href="https://codepen.io/matuzo/debug/yLQbVYN">demo</a>.</p>
<p>I often hear the question, “Can I use the details element for page navigation?”. My answer is always “No” for two main reasons:</p>
<ol>
<li>You probably don’t want your navigation to open randomly only because a user searched for a term on the page.</li>
<li>There are still a lot of <a href="/blog/2023/details-summary/">browser inconsistencies</a>, and you want such an important part as the navigation for your site to work consistently and reliably.</li>
</ol>
<p>You can find more examples and details in Adrian Roselli's <a href="https://adrianroselli.com/2019/04/details-summary-are-not-insert-control-here.html">“Details / Summary Are Not [insert control here]”</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9Cthe+details+element+and+in-page+search%E2%80%9D">blog@matuzo.at</a>.</p> the article element and screen readers2023-07-04T00:00:00+00:00https://www.matuzo.at/blog/2023/article-screen-readers
<p>I wanted to know how and if common screen readers expose the <code><article></code> element.<br />
Here are my results:</p><h2>Summary</h2>
<p><abbr title="too long, didn't read">tl;dr</abbr>: shit's complicated.</abbr></p>
<p>Some screen readers don't announce articles and have no default quick nav shortcuts. Some don't announce them but treat them as landmarks. Others announce them as articles and treat them as landmarks. There's no difference whether you label them or not.</p>
<style>
table {
table-layout: fixed;
white-space: nowrap;
}
tbody td {
background: green;
color: #fff;
}
tbody th {
font-weight: normal;
}
td.no {
background: #bf0404;
}
th a:is(:link, :visited) {
color: #fff;
}
th a:focus-visible {
outline-color: currentColor;
}
</style>
<div class="table-wrapper" aria-labelledby="form1" tabindex="0" role="region">
<table>
<caption id="form1">article element screen reader support results</caption>
<thead>
<tr>
<td></td>
<th>NVDA</th>
<th>Jaws</th>
<th>Voice Over (macOS)</th>
<th>Narrator</th>
<th>VoiceOver (iOs)</th>
<th>Talkback</th>
</tr>
</thead>
<tbody>
<tr>
<th>Virtual Cursor/Swipe</th>
<td class="no">no</td>
<td>yes</td>
<td>yes</td>
<td class="no">no</td>
<td>yes</td>
<td class="no">no</td>
</tr>
<tr>
<th>Landmark list</th>
<td class="no">no</td>
<td class="no">no</td>
<td class="no">no</td>
<td class="no">no</td>
<td>yes</td>
<td>yes</td>
</tr>
<tr>
<th>Custom article list</th>
<td class="no">no</td>
<td>yes</td>
<td>yes</td>
<td class="no">no</td>
<td>yes</td>
<td class="no">no</td>
</tr>
<tr>
<th>Default quick nav key</th>
<td class="no">no</td>
<td>yes</td>
<td class="no">no</td>
<td class="no">no</td>
<td class="no">no</td>
<td class="no">no</td>
</tr>
</tbody>
</table>
</div>
<h2>Demo code</h2>
<pre><code class="language-html"><article>
<h2>Unlabelled</h2>
</article>
<article aria-labelledby="heading">
<h2 id="heading">Labelled by heading</h2>
</article>
<article aria-label="Labelled by attribute">
<h2>Labelled by attribute</h2>
</article></code></pre>
<h2>NVDA 2023.1 with Firefox 114</h2>
<p>NVDA doesn't announce the article's role when you use the arrow keys or list it in the elements list. You can add a custom quick nav shortcut for article navigation, but I couldn't get it working.</p>
<h2>JAWS 2023.2212.13 with Edge 114</h2>
<p>Jaws announces labelled and unlabelled articles when you use the arrow keys or the <kbd>O</kbd> key to navigate. They're not included in the list of landmarks, but you can list all articles by pressing <kbd>Ctrl</kbd> + <kbd>Insert</kbd> + <kbd>O</kbd>.</p>
<img alt="Jaws listing an unlabelled article and two labelled articles in the articles list." height="810" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/article-screen-readers/5be4cbeed0-1698950540/article-sr2.webp" width="1440">
<h2>VoiceOver macOS 13.4.1 with Safari 16.5.1</h2>
<p>VoiceOver announces labelled and unlabelled articles when you use the virtual cursor. It also adds a new list of articles to the rotor. The landmarks list in the rotor doesn't include articles.</p>
<img alt="Voice Over listing an unlabelled article and two labelled articles in the rotor." height="900" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/article-screen-readers/56832da44e-1698950540/article-sr1.webp" width="1440">
<h2>Narrator Windows 10 with Edge 114</h2>
<p>Narrator doesn't announce the article's role or list it in the elements list.</p>
<h2>VoiceOver iOS 15.7.7 with Safari</h2>
<p>VoiceOver announces articles when you select contained items or swipe. It includes articles in the landmarks list and adds a new list of articles to the rotor.</p>
<h2>Talkback Android 13 with Chrome 114</h2>
<p>Talkback doesn't announce articles when you select contained items or swipe, but labelled and unlabelled articles are accessible via the landmark navigation.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9Cthe+article+element+and+screen+readers%E2%80%9D">blog@matuzo.at</a>.</p> O dialog focus, where art thou?2023-07-17T00:00:00+00:00https://www.matuzo.at/blog/2023/focus-dialog
<p>Here’s a job interview question for you: When you click a button and call the <code>showModal()</code> method to open a modal <code><dialog></code>, where does the focus go by default, and how can you move it elsewhere?</p><p>Don't know the answer? Neither did I, so I tested it.</p>
<h2>OS/Browsers</h2>
<p>macOs 13.4.1 Ventura<br />
Chrome 114<br />
Firefox 115<br />
Safari 16.5.1</p>
<p>Here’s a <a href="https://codepen.io/matuzo/pen/PoxOMYg?editors=1010">Codepen with all demos</a> so you can follow along.</p>
<h2>Demo 1: Dialog with no interactive element</h2>
<pre><code class="language-html"><button>demo 1</button>
<dialog>
<h1>Demo 1</h2>
</dialog></code></pre>
<pre><code class="language-js">document.addEventListener('click', e => {
if (e.target.closest('button')) {
e.target.nextElementSibling.showModal()
}
})</code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>dialog</strong><br />
Firefox: <strong>body</strong><br />
Safari: <strong>body</strong> </p>
<h2>Demo 2: Dialog with interactive elements</h2>
<pre><code class="language-html"><button>demo 2</button>
<dialog>
<h1>Demo 2</h2>
<button>First focusable element</button>
<a href="#">Last focusable element</a>
</dialog></code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>first focusable element</strong><br />
Firefox: <strong>first focusable element</strong><br />
Safari: <strong>first focusable element</strong> </p>
<h2>Demo 3: Dialog with interactive elements and close button first</h2>
<pre><code class="language-html">
<button>demo 3</button>
<dialog>
<form method="dialog">
<button>close</button>
</form>
<h1>Demo 3</h2>
<button>First focusable element</button>
<a href="#">Last focusable element</a>
</dialog></code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>close button</strong><br />
Firefox: <strong>close button</strong><br />
Safari: <strong>first focusable element after close button</strong></p>
<h2>Demo 4: Dialog with interactive elements and close button last</h2>
<pre><code class="language-html"><button>demo 4</button>
<dialog>
<h1>Demo 4</h2>
<button>First focusable element</button>
<a href="#">Last focusable element</a>
<form method="dialog">
<button>close</button>
</form>
</dialog></code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>first focusable element</strong><br />
Firefox: <strong>first focusable element</strong><br />
Safari: <strong>first focusable element</strong> </p>
<h2>Demo 5: Dialog without interactive elements and autofocus on dialog</h2>
<pre><code class="language-html"><button>demo 5</button>
<dialog autofocus>
<h1>Demo 5</h2>
</dialog></code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>dialog</strong><br />
Firefox: <strong>body</strong><br />
Safari: <strong>body</strong> </p>
<h2>Demo 6: Dialog with interactive elements and autofocus on dialog</h2>
<pre><code class="language-html"><button>demo 6</button>
<dialog autofocus>
<h1>Demo 6</h2>
<button>First focusable element</button>
<a href="#">Last focusable element</a>
</dialog></code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>first focusable element</strong><br />
Firefox: <strong>first focusable element</strong><br />
Safari: <strong>first focusable element</strong> </p>
<h2>Demo 7: Dialog with autofocus on last interactive element</h2>
<pre><code class="language-html"><button>demo 7</button>
<dialog>
<h1>Demo 7</h2>
<button>First focusable element</button>
<a href="#" autofocus>Last focusable element</a>
</dialog></code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>last focusable element</strong><br />
Firefox: <strong>last focusable element</strong><br />
Safari: <strong>last focusable element</strong> </p>
<p>Okay, so far, so inconsistent. The specs says “The <code>tabindex</code> attribute must not be specified on dialog elements.”.</p>
<div class="quote">
<blockquote>
I won't do what you tell me.
</blockquote>
<p>– Rage Against the Machine</p>
</div>
<h2>Demo 8: Dialog with tabindex and no interactive element</h2>
<pre><code class="language-html"><button>demo 8</button>
<dialog tabindex="-1">
<h1>Demo 8</h2>
</dialog></code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>dialog</strong><br />
Firefox: <strong>dialog</strong><br />
Safari: <strong>dialog</strong> </p>
<h2>Demo 9: Dialog with tabindex and interactive elements</h2>
<pre><code class="language-html"><button>demo 9</button>
<dialog tabindex="-1">
<h1>Demo 9</h2>
<button>First focusable element</button>
<a href="#">Last focusable element</a>
</dialog></code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>first focusable element</strong><br />
Firefox: <strong>first focusable element</strong><br />
Safari: <strong>first focusable element</strong> </p>
<h2>Demo 10: Dialog with tabindex, autofocus, and interactive elements</h2>
<pre><code class="language-html"><button>demo 10</button>
<dialog tabindex="-1" autofocus>
<h1>Demo 10</h2>
<button>First focusable element</button>
<a href="#">Last focusable element</a>
</dialog></code></pre>
<p>Chrome: <strong>first focusable element</strong><br />
Firefox: <strong>first focusable element</strong><br />
Safari: <strong>first focusable element</strong> </p>
<h2>Demo 11: Dialog with tabindex, <code>focus()</code>, and interactive elements</h2>
<pre><code class="language-html"><button>demo 11</button>
<dialog tabindex="-1">
<h1>Demo 11</h2>
<button>First focusable element</button>
<a href="#">Last focusable element</a>
</dialog></code></pre>
<pre><code class="language-js">dialog.showModal()
dialog.focus()</code></pre>
<p>Focus is on:</p>
<p>Chrome: <strong>dialog</strong><br />
Firefox: <strong>dialog</strong><br />
Safari: <strong>dialog</strong> </p>
<h2>Conclusion</h2>
<p>The answer is: It depends. It depends on several factors:</p>
<ul>
<li>The browser you're using.</li>
<li>The presence of interactive elements.</li>
<li>The presence of a dialog close button.</li>
<li>The presence of the <code>autofocus</code> attribute.</li>
<li>The presence of the <code>tabindex</code> attribute.</li>
</ul>
<p>There was <a href="https://www.scottohara.me/blog/2023/01/26/use-the-dialog-element.html">a lot of discussion</a> on how browsers should handle focus in modal dialogs. They finally concluded and summarized the rules in <a href="https://html.spec.whatwg.org/#dialog-focusing-steps">the spec</a> earlier this year. If I read it right, Chrome is the only browser that follows most rules correctly at this point. Other browsers will likely follow soon.</p>
<p>Right now you get the most consistent behaviour when:</p>
<ul>
<li>you have interactive elements and do nothing</li>
<li>you put the <code>autofocus</code> attribute on one of the interactive elements</li>
<li>you put <code>tabindex=-1</code> on the dialog (and <code>focus()</code> it).</li>
</ul>
<h2>Updates</h2>
<p><time datetime="2023-07-20">20.7.2023</time>: Changed <em>body</em> to <em>first focusable element</em> for Chrome in demo 6 and demo 10. I had experimental web platform features enabled, which changed the current default behaviour.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CO+dialog+focus%2C+where+art+thou%3F%E2%80%9D">blog@matuzo.at</a>.</p> Visually hidden links with 0 dimensions2023-07-19T00:00:00+00:00https://www.matuzo.at/blog/2023/zero-width-height-skip
<p>If you have used a visually-hidden class in the past, you might have noticed that the width and height is set to 1px and not 0. I’ve always wondered why.</p><p class="code-intro">
Even in James Edwards’ “<a href="https://www.tpgi.com/the-anatomy-of-visually-hidden/">The anatomy of visually-hidden</a>” I didn’t find the answer because he wasn’t sure either.
While testing a client’s site a few minutes ago, I found at least one good reason.
</p>
<pre><code class="language-css">/* A typical .visually-hidden class */
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}</code></pre>
<p class="code-intro">
My client has skip links on their site, and they work perfectly fine in Chrome and Firefox but not in Safari. They use the following code for hiding links visually:
</p>
<pre><code class="language-css">.skip-links a:not(:focus) {
overflow: hidden;
width: 0;
height: 0;
padding: 0;
border: 0;
margin: 0;
}</code></pre>
<p class="code-intro">
As it turns out, links are not focusable in Safari if they have 0 dimensions. Adding 1px padding, border, or width and height fixes the issue.
</p>
<pre><code class="language-css">.skip-links a:not(:focus) {
overflow: hidden;
width: 1px;
height: 1px;
padding: 0;
border: 0;
margin: 0;
}</code></pre>
<p>So, there you go. That's why you want to keep the 1px height and width in your visually-hidden classes. You can test it in this <a href="https://codepen.io/matuzo/pen/yLQjQKq">CodePen</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CVisually+hidden+links+with+0+dimensions%E2%80%9D">blog@matuzo.at</a>.</p> aria-haspopup and screen readers2023-08-17T00:00:00+00:00https://www.matuzo.at/blog/2023/aria-haspopup
<p>I read Steve Faulkners <a href="https://html5accessibility.com/stuff/2021/02/02/haspopup-haspoop/">“hasPopup hasPoop”</a> where he mentions differences in what screen readers announce when dealing with the <code>aria-haspopup</code> attribute. I wanted to know how that manifests used on a button.</p><style>
table {
table-layout: fixed;
white-space: nowrap;
}
tbody td {
background: green;
color: #fff;
}
tbody th {
font-weight: normal;
/* position: sticky;
left: 0;*/
}
td.no {
background: #bf0404;
}
td.kinda {
background: #bf8504;
}
th a:is(:link, :visited) {
color: #fff;
}
th a:focus-visible {
outline-color: currentColor;
}
</style>
<h2>Summary</h2>
<p>The situation isn't too bad because all screen readers and browsers, except Narrator in Firefox and Chrome, at least support the attribute. Talback and NVDA don't support the <em>grid</em>, <em>listbox</em>, and <em>tree</em> values. NVDA also doesn't support <em>dialog</em>. Other than that, it works great. </p>
<p>I noticed some interesting details:</p>
<ul>
<li>VoiceOver with Safari doesn't announce the <code>aria-expanded</code> attribute in combination with <code>aria-haspopup</code> on macOS and iOs.</li>
<li>VoiceOver on MacOS with Firefox announces <code>aria-haspopup="true"</code> differently than <code>aria-haspopup="menu"</code>: <em>Settings, menu button, group</em> for <code>true</code> and <em>Settings, menu pop-up, button</em> for <code>menu</code>.</li>
<li>Talkback with Chrome doesn't support <em>grid</em>, <em>listbox</em>, and <em>tree</em>.</li>
<li>Jaws adds specific instructions for <code>true</code> and <code>menu</code>: “Press Space to activate the menu. Then navigate with arrow keys”, and for <code>listbox</code>, <code>tree</code>, and <code>grid</code>: <em>“To activate press Enter”</em>.</li>
<li>Jaws doesn't announce the <code>aria-expanded</code> attribute initially in all three browsers, but it does announce it on activation.</li>
<li><s>NVDA doesn't support <em>dialogue</em>, <em>grid</em>, <em>listbox</em>, and <em>tree</em></s>. (NVDA added support with in version 2023.2)</li>
<li>NVDA with Firefox announces <code>aria-haspopup="true"</code> differently than <code>aria-haspopup="menu"</code>: <em>Settings, menu button, subMenu</em> for <code>true</code> and <em>Settings, button, subMenu</em> for <code>menu</code>.</li>
<li>NVDA with Chrome/Edge announces <code>aria-haspopup="dialog"</code> differently than <code>aria-haspopup="grid|tree|listbox"</code>: <em>Settings, button, opens dialog</em> and <em>Settings, menu button, opens grid|tree|listbox</em>.</li>
<li>Narrator announces “collapsed” on <code>aria-haspopup="true|menu"</code> even when <code>aria-expanded</code> isn't present.</li>
<li>Narrator with Firefox doesn't supprt the attribute.</li>
<li>Narrator with Chrome announces “menu item” instead of “menu button“, except for <code>aria-haspopup="dialog"</code> where it's just “button”.</li>
</ul>
<h2>Software/OS/browser</h2>
<p>I tested using the <kbd>Tab</kbd> key only and I've used this <a href="https://codepen.io/matuzo/debug/mdabQpa">CodePen</a>.</p>
<ul>
<li>VoiceOver iOS 15.7.7 with Safari</li>
<li>Talkback Android 13 with Chrome 116</li>
<li>VoiceOver macOS 13.4.1 with Safari 16.5.2</li>
<li>VoiceOver macOS 13.4.1 with Firefox 116</li>
<li>VoiceOver macOS 13.4.1 with Chrome 116</li>
<li>JAWS 2023.2307.37 with Edge 116, Chrome 116, and Firefox 116</li>
<li>NVDA 2023.2 with Firefox 116</li>
<li>NVDA 2023.2 with Chrome 116 and Edge 116</li>
<li>Narrator Windows 10 with Firefox, Chrome, and Edge 116</li>
</ul>
<h2>form role</h2>
<div class="table-wrapper" aria-labelledby="form1" tabindex="0" role="region">
<table>
<caption id="form1">form role test results</caption>
<thead>
<tr>
<td></td>
<th>VoiceOver (iOs)</th>
<th>Talkback</th>
<th>Voice Over Safari (macOS)</th>
<th>Voice Over Firefox (macOS)</th>
<th>Voice Over Chrome (macOS)</th>
<th>Jaws</th>
<th>NVDA (Firefox)</th>
<th>NVDA (Chrome, Edge)</th>
<th>Narrator (Edge)</th>
<th>Narrator (Firefox)</th>
<th>Narrator (Chrome)</th>
</tr>
</thead>
<tbody>
<tr>
<th><a href="#demo1">aria-haspopup="true"</a></th>
<td>Settings, pop-up button, menu pop-up</td>
<td>Settings, menu pop-up button</td>
<td>Settings, menu pop-up, button</td>
<td class="kinda">Settings, menu button, group</td>
<td>Settings, menu pop-up, button</td>
<td>Settings, button, menu</td>
<td class="kinda">Settings, menu button, subMenu</td>
<td>Settings, menu button, subMenu</td>
<td class="kinda">Settings, button, collapsed, has pop-up</td>
<td class="kinda">Settings, menu item</td>
<td class="kinda">Settings, menu item</td>
</tr>
<tr>
<th><a href="#demo2">aria-haspopup="menu"</a></th>
<td>Settings, pop-up button, menu pop-up</td>
<td>Settings, menu pop-up button</td>
<td>Settings, menu pop-up, button</td>
<td>Settings, menu pop-up, button</td>
<td>Settings, menu pop-up, button</td>
<td>Settings, button, menu</td>
<td>Settings, button, subMenu</td>
<td>Settings, menu button, subMenu</td>
<td class="kinda">Settings, button, collapsed, has pop-up</td>
<td class="no">Settings, button</td>
<td class="kinda">Settings, menu item</td>
</tr>
<tr>
<th><a href="#demo3">aria-haspopup="menu"<br>+ aria-expanded="false"</a></th>
<td class="kinda">Settings, pop-up button, menu pop-up</td>
<td>Collapsed, Settings, menu pop-up button</td>
<td class="kinda">Settings, menu pop-up, button</td>
<td>Settings, menu pop-up collapsed, button</td>
<td>Settings, menu pop-up collapsed, button</td>
<td class="kinda">Settings, button, menu</td>
<td>Settings, button collapsed, subMenu
<td>Settings, menu button collapsed, subMenu</td>
<td>Settings, button, collapsed, has pop-up</td>
<td class="no">Settings, button, collapsed</td>
<td class="kinda">Settings, menu item, collapsed</td>
</tr>
<tr>
<th><a href="#demo4">aria-haspopup="dialog"</a></th>
<td>Settings, pop-up button, dialogue pop-up</td>
<td>Settings, dialogue pop-up button</td>
<td>Settings, dialogue pop-up, button</td>
<td>Settings, dialogue pop-up, button</td>
<td>Settings, dialogue pop-up, button</td>
<td>Settings, button has pop-up dialogue</td>
<td>Settings, button, opens dialog</td>
<td>Settings, button, opens dialog</td>
<td class="kinda">Settings, button, has pop-up</td>
<td class="no">Settings, button</td>
<td class="no">Settings, button</td>
</tr>
<tr>
<th><a href="#demo5">aria-haspopup="grid"</a></th>
<td>Settings, pop-up button, grid pop-up</td>
<td class="kinda">Settings, pop-up button</td>
<td>Settings, grid pop-up, button</td>
<td>Settings, grid pop-up, button</td>
<td>Settings, grid pop-up, button</td>
<td>Settings, button has pop-up grid</td>
<td>Settings, button, opens grid</td>
<td>Settings, menu button, opens grid</td>
<td class="kinda">Settings, button, has pop-up</td>
<td class="no">Settings, button</td>
<td class="kinda">Settings, menu item</td>
</tr>
<tr>
<th><a href="#demo6">aria-haspopup="listbox"</a></th>
<td>Settings, pop-up button, list box pop-up</td>
<td class="kinda">Settings, pop-up button</td>
<td>Settings, list box pop-up, button</td>
<td>Settings, list box pop-up, button</td>
<td>Settings, list box pop-up, button</td>
<td>Settings, button has pop-up list box</td>
<td>Settings, button, opens list</td>
<td>Settings, menu button, opens list</td>
<td class="kinda">Settings, button, has pop-up</td>
<td class="no">Settings, button</td>
<td class="kinda">Settings, menu item</td>
</tr>
<tr>
<th><a href="#demo7">aria-haspopup="tree"</a></th>
<td>Settings, pop-up button, tree pop-up</td>
<td class="kinda">Settings, pop-up button</td>
<td>Settings, tree pop-up, button</td>
<td>Settings, tree pop-up, button</td>
<td>Settings, tree pop-up, button</td>
<td>Settings, button has pop-up tree</td>
<td>Settings, button, opens tree</td>
<td>Settings, menu button, opens tree</td>
<td class="kinda">Settings, button, has pop-up</td>
<td class="no">Settings, button</td>
<td class="kinda">Settings, menu item</td>
</tr>
</tbody>
</table>
</div>
<h3 id="demo1">aria-haspopup="true"</h3>
<pre><code class="language-html"><button class="toggle" aria-haspopup="true">
Settings
</button></code></pre>
<h3 id="demo2">aria-haspopup="menu"</h3>
<pre><code class="language-html"><button class="toggle" aria-haspopup="menu">
Settings
</button></code></pre>
<h3 id="demo3">aria-haspopup="menu" + aria-expanded="false"</h3>
<pre><code class="language-html"><button class="toggle" aria-haspopup="menu" aria-expanded="false">
Settings
</button></code></pre>
<h3 id="demo4">aria-haspopup="dialog"</h3>
<pre><code class="language-html"><button class="toggle" aria-haspopup="dialog">
Settings
</button></code></pre>
<h3 id="demo5">aria-haspopup="grid"</h3>
<pre><code class="language-html"><button class="toggle" aria-haspopup="grid">
Settings
</button></code></pre>
<h3 id="demo6">aria-haspopup="listbox"</h3>
<pre><code class="language-html"><button class="toggle" aria-haspopup="listbox">
Settings
</button></code></pre>
<h3 id="demo7">aria-haspopup="tree"</h3>
<pre><code class="language-html"><button class="toggle" aria-haspopup="tree">
Settings
</button></code></pre>
<h2>Update 4.9.2023</h2>
<p>NVDA supports the <code>dialog</code>, <code>grid</code>, <code>list</code>, and <code>tree</code> keywords starting with <a href="https://www.nvaccess.org/post/nvda-2023-2/">NVDA 2023.2</a>. In prior versions it only announced <em>Settings, button, subMenu</em>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9Caria-haspopup+and+screen+readers%E2%80%9D">blog@matuzo.at</a>.</p> Pros and cons of using Shadow DOM and style encapsulation2023-08-23T00:00:00+00:00https://www.matuzo.at/blog/2023/pros-and-cons-of-shadow-dom
<p>When I started to work with web components, I compared different options and decided to go with <a href="https://lit.dev">lit</a>. I knew the extra performance cost would pay off quickly, and it fit into my performance budget. I’m still happy with my decision.</p><style>
.pro {
color: green;
}
.con {
color: red;
}
</style>
<p>I was new to web components and lit, so I had to consult the docs quite often. On the <a href="https://lit.dev/docs/components/shadow-dom/">page about Shadow DOM</a> it says: </p>
<div class="quote">
<blockquote>
<p>Rendering into children and not shadow DOM is generally not recommended. Your element will not have access to DOM or style scoping, and it will not be able to compose elements into its internal DOM.</p>
<blockquote>
</div>
<p>To affirm that, the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">Using shadow DOM</a> page on MDN says:</p>
<div class="quote">
<blockquote>
<p>An important aspect of web components is encapsulation — being able to keep the markup structure, style, and behavior hidden and separate from other code on the page so that different parts do not clash, and the code can be kept nice and clean.</p>
<blockquote>
</div>
<p>That confirmed my perception of how web components work: they go hand in hand with Shadow DOM. I thought that's the USP (unique selling point), so I used it in almost every component I built. </p>
<p>Nearly a year later, after fighting many problems and annoyances, I wondered whether Shadow DOM was actually that useful. So I <a href="https://front-end.social/@matuzo/110904820573072435">posted this on Mastodon</a>:</p>
<div class="quote">
<blockquote>
<p>After almost a year working with web components I'm starting to doubt the usefulness of style encapsulation and shadow DOM in general.</p>
<p>Styling and some accessibility stuff is so much easier without…</p>
<blockquote>
</div>
<p>To my surprise, many people replied that they agreed, default to light DOM, and only opt-in when it makes sense. I'm glad because it confirmed my feeling and encouraged me to rethink and restructure some of the components I built. Shadow DOM has its justification but does not always have to be the first choice.</p>
<p>To help you make similar decisions, I summarized my pros and cons of Shadow DOM and style encapsulation. Please note that I'm writing this from the perspective of someone who, first and foremost, cares about accessibility and well-structured, semantic HTML. For me, JavaScript is an optional add-on and not the foundation of my code. That implies that I might not have to solve the same problems as others who primarily work with JS-heavy websites. That might also explain why my list of cons is longer than the list of pros.</p>
<p>Here's a collection on CodePen with <a href="https://codepen.io/collection/zxJjoj">all the demos in this post</a>.</p>
<h2>Overview</h2>
<ol>
<li>
<a href="#pro-style-encapsulation"><span class="pro">Pro:</span> Style encapsulation</a>
</li>
<li>
<a href="#pro-dom-encapsulation"><span class="pro">Pro:</span> DOM encapsulation</a>
</li>
<li>
<a href="#pro-slots"><span class="pro">Pro:</span> Slots</a>
</li>
<li>
<a href="#con-style-encapsulation"><span class="con">Con:</span> Style “encapsulation”</a>
</li>
<li>
<a href="#con-styling-slotted-content"><span class="con">Con:</span> Styling slotted content</a>
</li>
<li>
<a href="#con-styling-nested-slotted-content"><span class="con">Con:</span> Styling nested slotted content</a>
</li>
<li>
<a href="#con-selecting-slotted-content"><span class="con">Con:</span> Selecting slotted content</a>
</li>
<li>
<a href="#con-broken-references"><span class="con">Con:</span> Broken references</a>
</li>
<li>
<a href="#con-javascript-dependency"><span class="con">Con:</span> JavaScript dependency</a>
</li>
<li>
<a href="#con-fouce"><span class="con">Con:</span> FOUCE (Flash of unstyled custom element)</a>
</li>
<li>
<a href="#con-forms-behave-differently"><span class="con">Con:</span> Forms behave differently</a>
</li>
<li>
<a href="#con-missing-global-styles"><span class="con">Con:</span> Missing global styles</a>
</li>
<li>
<a href="#con-debugging-a11y"><span class="con">Con:</span> Debugging with accessibility testing tools</a>
</li>
<li>
<a href="#con-debugging-a11y2"><span class="con">Con:</span> Debugging with DevTools</a>
</li>
</ol>
<h2 id="pro-style-encapsulation"><span class="pro">Pro:</span> Style encapsulation</h2>
<p>When you build components that others use in environments where you have no access or control over light DOM styles, style encapsulation can be immensely helpful. You provide devs who work with your components with one or more JS files that contain all the structure and styles needed to make your custom element display and work just as you intended. If there are new versions of a component, they don't have to worry too much about the structure and the classes used because everything's neatly hidden in its own DOM. They only have to replace the JavaScript files.</p>
<p><a href="https://front-end.social/@dutchcelt@mastodon.social/110914690188092126">Egor, for example, replied</a> to my post on Mastodon:</p>
<div class="quote">
<blockquote>
<p>I almost can’t work without them when creating a library of components, and I have no access to the document(light dom) styles. I’m working on a project requiring components in React, Angular, and Blazor. Web Components make that possible. </p>
<blockquote>
</div>
<p>In cases like that, when you work on a design system and don't know who's using your components, combined with which external stylesheets, and in which environment, style encapsulation makes a lot of sense.</p>
<p>Here's an example: a paragraph in light DOM and one in shadow DOM of a web component. </p>
<pre><code class="language-html"><p>
Hello World!
</p>
<cus-tom>
Hello encapsulated World!
</cus-tom></code></pre>
<pre><code class="language-css">p {
border: 4px dotted aqua;
}</code></pre>
<p>The paragraph within the component is not affected by global styles and comes with its own styles that don't affect the outside.</p>
<div data-sample="demo" class="demo1">
<p>
Hello World!
</p>
<cus-tom>
Hello encapsulated World!
</cus-tom>
</div>
<script>
class Custom1 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
p {
font-family: Comic Sans MS;
}
`
let content = document.createElement('p');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom', Custom1);
</script>
<style>
.demo1 p {
border: 4px dotted aqua;
}
</style>
<p>You only pass text to the web component and it returns a styled paragraph.</p>
<pre><code class="language-js">class Custom1 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
p {
font-family: Comic Sans MS;
}
`
let content = document.createElement('p');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom', Custom1);</code></pre>
<h2 id="pro-dom-encapsulation"><span class="pro">Pro:</span> DOM encapsulation</h2>
<p>Similar to style encapsulation, with Shadow DOM, you also get DOM encapsulation, which can be helpful.</p>
<p>In the following demo, you can see how it says there are only three buttons on the page, but there are actually four. It's just that one is in the shadow DOM of the custom component. The click event handler attached to the document also only affects the buttons in the light DOM.</p>
<div data-sample="demo" class="demo2">
<p>There are <span>0</span> buttons!</p>
<button>Click me</button>
<button>Click me</button>
<cus-tom2></cus-tom2>
<button>Click me</button>
</div>
<script>
class Custom2 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const button = document.createElement('button')
button.textContent = 'Click me'
button.addEventListener('click', e => {
alert('in the shadow ohoh')
})
this.shadowRoot.append(button)
}
}
customElements.define('cus-tom2', Custom2);
const demo = document.querySelector('.demo2')
const buttons = demo.querySelectorAll('button')
demo.querySelector('span').textContent = buttons.length
demo.addEventListener('click', e => {
if (e.target.closest('button')) {
alert('light')
}
})
</script>
<pre><code class="language-html"><p>There are <span>0</span> buttons!</p>
<button>Click me</button>
<button>Click me</button>
<cus-tom2></cus-tom2>
<button>Click me</button></code></pre>
<pre><code class="language-js">// simple web component with a button attached to the shadow DOM
class Custom2 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const button = document.createElement('button')
button.textContent = 'Click me'
button.addEventListener('click', e => {
alert('in the shadow ohoh')
})
this.shadowRoot.append(button)
}
}
customElements.define('cus-tom2', Custom2);
// Get all buttons on the page
const buttons = document.querySelectorAll('button')
// Update the amount of buttons
document.querySelector('span').textContent = buttons.length
// Attach a click event to all buttons
document.addEventListener('click', e => {
if (e.target.closest('button')) {
alert('light')
}
})</code></pre>
<p>It's worth noting that nodes are scoped to your component even without Shadow DOM. For example, <code>this.querySelectorAll('button')</code> only returns all the buttons inside your component.</p>
<h2 id="pro-slots"><span class="pro">Pro:</span> Slots</h2>
<p>You can define a web component without shadow DOM and pass content from the light DOM.</p>
<pre><code class="language-js">class Custom3 extends HTMLElement {
constructor() {
super();
}
}
customElements.define('cus-tom3', Custom3);</code></pre>
<pre><code class="language-html"><cus-tom3>
<h3>That's my heading</h3>
<p>That's my content!</p>
</cus-tom3></code></pre>
<div data-sample="demo" class="demo3">
<cus-tom3>
<h3>That's my heading</h3>
<p>That's my content!</p>
</cus-tom3>
</div>
<p>That's fine, but with shadow DOM you have more control over slotted content regarding its position in the DOM. Being able to create named slots is one of my favorite features.</p>
<pre><code class="language-html"><cus-tom4>
<p slot="content">That's my content!</p>
<h3 slot="heading">That's my heading</h3>
<p slot="content">That's even more content!</p>
</cus-tom4></code></pre>
<pre><code class="language-js">class Custom4 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<slot name="heading"></slot>
<p>Other content</p>
<slot name="content"></slot>
`
}
}
customElements.define('cus-tom4', Custom4);</code></pre>
<p>See how I messed up the order of the content in light DOM. As long as JavaScript works, it doesn't matter because you can define the order of how slots are rendered. You can also add other elements in between or pass content multiple times into a single slot.</p>
<div data-sample="demo" class="demo4">
<cus-tom4>
<p slot="content">That's my content!</p>
<h3 slot="heading">That's my heading</h3>
<p slot="content">That's even more content!</p>
</cus-tom4>
</div>
<script>
class Custom4 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<slot name="heading"></slot>
<p>Other content</p>
<slot name="content"></slot>
`
}
}
customElements.define('cus-tom4', Custom4);
</script>
<h2 id="con-style-encapsulation"><span class="con">Con:</span> Style “encapsulation”</h2>
<p>Styles within a web component are contained; they can't affect elements on the outside. Styles from the outside also don't affect elements within the Shadow DOM, with two exceptions: inheritable properties and custom properties.</p>
<pre><code class="language-html"><div>
<cus-tom5>
Hello World!
</cus-tom5>
</div></code></pre>
<pre><code class="language-css">/* Styles don't apply */
p {
border: 10px solid aqua;
}
/* Styles apply */
div {
--background: black;
color: red;
}</code></pre>
<div data-sample="demo" class="demo5">
<div>
<cus-tom5>
Hello World!
</cus-tom5>
</div>
</div>
<script>
class Custom5 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
p {
background: var(--background, transparent);
}
`
let content = document.createElement('p');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom5', Custom5);
</script>
<style>
.demo5 p {
border: 10px solid aqua;
}
/* Styles apply */
.demo5 div {
--background: black;
color: red;
}
</style>
<p>Selecting and styling all paragraphs on the page doesn't affect the encapsulated paragraph. Still, it inherits the value of the color property from its parent div in light DOM, and it also reads its custom properties.</p>
<pre><code class="language-js">class Custom5 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
p {
background: var(--background, transparent);
}
`
let content = document.createElement('p');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom5', Custom5);</code></pre>
<p>Don't get me wrong. Inheritance is a good thing. If web components couldn't consume custom properties from the outside, I would've thrown them out of the window months ago. It's just that style encapsulation in web components isn't 100% encapsulation. To prevent inheritable properties from leaking in, you must providently reset them. Also, it would help if you prefixed your custom properties to avoid clashes with other properties on the page.</p>
<h2 id="con-styling-slotted-content"><span class="con">Con:</span> Styling slotted content</h2>
<p>In the following component, I'm slotting a paragraph. All paragraphs in the light DOM have global styles applied to them. In my component, I'm trying to overwrite some of them, but my rules have no effect.</p>
<pre><code class="language-html"><cus-tom6>
<p>Hello World!</p>
</cus-tom6></code></pre>
<pre><code class="language-css">p {
color: red;
border: 10px solid red;
}</code></pre>
<pre><code class="language-js">class Custom6 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
::slotted(p) {
color: blue;
border-color: blue;
}
`
let content = document.createElement('div');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom6', Custom6);</code></pre>
<div data-sample="demo" class="demo6">
<cus-tom6>
<p>Hello World!</p>
</cus-tom6>
</div>
<style>
:is(.demo6, .demo7) p {
color: red;
border: 10px solid red;
}
</style>
<script>
class Custom6 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
::slotted(p) {
color: blue;
border-color: blue;
}
`
let content = document.createElement('div');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom6', Custom6);
class Custom7 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
::slotted(p) {
color: blue !important;
border-color: blue !important;
}
`
let content = document.createElement('div');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom7', Custom7);
</script>
<p>The explanation is that we're dealing with two different contexts: the document and the component. For normal rules, declarations from the outer context have precedence over rules from the inner context. For important rules, it's reversed. That means to overwrite styles from the outer context, you must use <code>!important</code>, which is annoying.</p>
<pre><code class="language-js">style.textContent= `
::slotted(p) {
color: blue !important;
border-color: blue !important;
}
`</code></pre>
<div data-sample="demo" class="demo7">
<cus-tom7>
<p>Hello World!</p>
</cus-tom7>
</div>
<h2 id="con-styling-nested-slotted-content"><span class="con">Con:</span> Styling nested slotted content</h2>
<p>You cannot style nested slotted content. With <code>::slotted()</code>, you can only select elements on the first level. In the following example, we can only style the <code><ul></code>, not the <code><li></code>.</p>
<pre><code class="language-html"><cus-tom8>
<ul>
<li>
Hello World
</li>
</ul>
</cus-tom8></code></pre>
<pre><code class="language-js">class Custom8 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
::slotted(ul) {
border: 10px solid red;
}
::slotted(li) {
border: 10px solid blue;
}
::slotted(ul li) {
border: 10px solid blue;
}
::slotted(ul) li {
border: 10px solid blue;
}
::slotted(ul) ::slotted(li) {
border: 10px solid blue;
}
`
let content = document.createElement('div');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom8', Custom8);</code></pre>
<div data-sample="demo" class="demo8">
<cus-tom8>
<ul>
<li>
Hello World
</li>
</ul>
</cus-tom8>
</div>
<script>
class Custom8 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
::slotted(ul) {
border: 10px solid red;
}
::slotted(li) {
border: 10px solid blue;
}
::slotted(ul li) {
border: 10px solid blue;
}
::slotted(ul) li {
border: 10px solid blue;
}
::slotted(ul) ::slotted(li) {
border: 10px solid blue;
}
`
let content = document.createElement('div');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom8', Custom8);
</script>
<h2 id="con-selecting-slotted-content"><span class="con">Con:</span> Selecting slotted content</h2>
<p>You're limited in what you can do with the <code>::slotted()</code> pseudo-element. For example, combinators don't work. </p>
<pre><code class="language-js">class Custom9 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
::slotted(p + p) {
border: 10px solid red;
}
::slotted(p) + ::slotted(p) {
border: 10px solid red;
}
`
let content = document.createElement('div');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom9', Custom9)</code></pre>
<pre><code class="language-html"><cus-tom9>
<p>Hello World</p>
<p>Hello World</p>
</cus-tom9></code></pre>
<div data-sample="demo" class="demo9">
<cus-tom9>
<p>Hello World</p>
<p>Hello World</p>
</cus-tom9>
</div>
<script>
class Custom9 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
let style = document.createElement('style');
style.textContent= `
::slotted(p + p) {
border: 10px solid red;
}
::slotted(p) + ::slotted(p) {
border: 10px solid red;
}
`
let content = document.createElement('div');
content.innerHTML = `<slot></slot>`
this.shadowRoot.append(style)
this.shadowRoot.append(content)
}
}
customElements.define('cus-tom9', Custom9)
</script>
<h2 id="con-broken-references"><span class="con">Con:</span> Broken references</h2>
<p>You can't reference an element in light DOM from shadow DOM or vice versa. That means that things like anchor links or aria references don't work.</p>
<p>The for-id relation, the <code>aria-labelledby</code> reference, and the anchor link in the following example don't work.</p>
<pre><code class="language-html"><p>
<a href="#name" id="skip">Jump to name</a>
</p>
<label for="name">Name:</label>
<cus-tom10></cus-tom10></code></pre>
<pre><code class="language-js">class Custom10 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<input type="text" id="name">
<h2 aria-labelledby="skip">Test</h2>
`
}
}
customElements.define('cus-tom10', Custom10);
</code></pre>
<div data-sample="demo" class="demo9">
<p>
<a href="#name" id="skip">Jump to name</a>
</p>
<label for="name">Name:</label>
<cus-tom10></cus-tom10>
</div>
<script>
class Custom10 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<input type="text" id="name">
<h2 aria-labelledby="skip">Test</h2>
`
}
}
customElements.define('cus-tom10', Custom10);
</script>
<p><a href="https://github.com/leobalter/cross-root-aria-delegation/blob/main/explainer.md">Cross-root ARIA</a> might fix some of these issues in the future.</p>
<h2 id="con-javascript-dependency"><span class="con">Con:</span> JavaScript dependency</h2>
<p>Firefox doesn't support <a href="https://developer.chrome.com/en/articles/declarative-shadow-dom/">declarative Shadow DOM</a> (yet), and if you don't use it and don't work with light DOM, it means that JavaScript is a dependency for rendering anything on the screen. That might not be a concern for every one, but it's not an option for me.</p>
<h2 id="con-fouce"><span class="con">Con:</span> FOUCE (Flash of unstyled custom element)</h2>
<p>JavaScript being a dependency means that your components must wait until JavaScript is loaded before they can be rendered. That <s>may</s> will result in flash of unstyled content (FOUC) and/or layout shifts.</p>
<p>Many suggest this horrible hack: Hide the element until it's defined.</p>
<pre><code class="language-css">:not(:defined) {
visibility: hidden;
}</code></pre>
<p>That's not a solution, that's a workaround.</p>
<h2 id="con-forms-behave-differently"><span class="con">Con:</span> Forms behave differently</h2>
<p>I didn’t know that elements inside shadow DOM in a form can cause problems until I read Simon MacDonald’s post <a href="https://begin.com/blog/posts/2023-08-18-shadow-dom-not-by-default">“Shadow DOM: Not by Default”</a>.</p>
<p>For example, in the following form, only the input field in light DOM is associated with the form.</p>
<div data-sample="demo" class="demo9">
<form>
<p>
<label for="name1">Name</label>
<input type="text" name="name" id="name1" value="johanna">
</p>
<cus-tom11></cus-tom11>
<button>Send</button>
</form>
</div>
<pre><code class="language-html"><form>
<p>
<label for="name">Name</label>
<input type="text" name="name" id="name" value="johanna">
</p>
<cus-tom11></cus-tom11>
<button>Send</button>
</form></code></pre>
<pre><code class="language-js">class Custom11 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<p>
<label for="email">E-Mail</label>
<input type="text" name="email" id="email" value="johanna@matuzo.at">
</p>`
}
}
customElements.define('cus-tom11', Custom11);
const form = document.querySelector('form')
form.addEventListener('submit', e => {
e.preventDefault()
const data = new FormData(form);
for (let nv of data.entries()) {
alert(` ${ nv[0] }: ${ nv[1] }`);
}
})</code></pre>
<script>
class Custom11 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<p>
<label for="email">E-Mail</label>
<input type="text" name="email" id="email" value="johanna@matuzo.at">
</p>`
}
}
customElements.define('cus-tom11', Custom11);
const form = document.querySelector('form')
form.addEventListener('submit', e => {
e.preventDefault()
const data = new FormData(form);
for (let nv of data.entries()) {
alert(` ${ nv[0] }: ${ nv[1] }`);
}
})
</script>
<p>You can work around that issue using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals">ElementInternals API</a> as described in the blog post <a href="https://kinsta.com/blog/web-components/#ignored-inputs">A Complete Introduction to Web Components in 2023</a> by Craig Buckler. I'm not sure, though, if it's worth the effort. Let me quote <a href="https://begin.com/blog/posts/2023-08-18-shadow-dom-not-by-default#why-not-just-use-the-shadow-dom-from-the-start%3F">Simon Mcdonald</a>:</p>
<div class="quote">
<blockquote>
There is a spec called Form Associated Custom Elements (FACE) that gives you the APIs to build web components that participate in forms. However, fixing a problem created by JavaScript by writing more JavaScript is like handing a drowning man a glass of water, IMHO.
</blockquote>
</div>
<h2 id="con-missing-global-styles"><span class="con">Con:</span> Missing global styles</h2>
<p>Style encapsulation sounds great until you want certain styles to apply everywhere. For example, I always define focus styles globally, which are usually the same for every element.</p>
<p>Button two and three in the following example have default styles, while button one and four use custom styles.</p>
<style>
:focus-visible {
outline: 2px solid;
outline-offset: 2px;
}
</style>
<div data-sample="demo" class="demo12">
<button class="button1">Button 1</button>
<cus-tom12></cus-tom12>
<button class="button4">Button 4</button>
</div>
<script>
class Custom12 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<button class="button2">Button 2</button>
<button class="button3">Button 3</button>
`
}
}
customElements.define('cus-tom12', Custom12);
</script>
<pre><code class="language-html"><button class="button1">Button 1</button>
<cus-tom12></cus-tom12>
<button class="button4">Button 4</button></code></pre>
<pre><code class="language-js">class Custom12 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<button class="button2">Button 2</button>
<button class="button3">Button 3</button>
`
}
}
customElements.define('cus-tom12', Custom12);</code></pre>
<pre><code class="language-css">:focus-visible {
outline: 2px solid;
outline-offset: 2px;
}</code></pre>
<p>To get the same focus styles in shadow DOM as in light DOM, I have to repeat the global declarations in each component with interactive elements.</p>
<pre><code class="language-css">:root {
--site-focus-outline: 2px solid;
--site-focus-offset: 2px;
}
:focus-visible {
outline: var(--site-focus-outline);
outline-offset: var(--site-focus-offset);
}</code></pre>
<pre><code class="language-js">class Custom12 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
:focus-visible {
outline: var(--site-focus-outline);
outline-offset: var(--site-focus-offset);
}
</style>
<button class="button2">Button 2</button>
<button class="button3">Button 3</button>
`
}
}
customElements.define('cus-tom12', Custom12);</code></pre>
<h2 id="con-debugging-a11y"><span class="con">Con:</span> Debugging with accessibility testing tools</h2>
<p>Not all accessibility testing tools support shadow DOM. For example, Wave flags zero errors, but there are at least three. </p>
<pre><code class="language-js">class Custom13 extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<img src="#test">
<button></button>
<button aria-hidden="true">Click</button>
`
}
}
customElements.define('cus-tom13', Custom13);</code></pre>
<img alt="Browser version of wave showing 0 errors" height="680" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/pros-and-cons-of-shadow-dom/762f1eeec7-1698950551/shadow-dom-debugging4.webp" width="1200">
<h2 id="con-debugging-a11y2"><span class="con">Con:</span> Debugging with DevTools</h2>
<p>I like to use the live expression feature in Chrome's Dev Tools to debug keyboard accessibility issues by logging <code>document.activeElement</code>.</p>
<p>When I focus a button in light DOM, it returns the focused button.</p>
<img alt="Focus on button 1. Dev tools logs button.button1" height="352" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/pros-and-cons-of-shadow-dom/6372ce9715-1698950551/shadow-dom-debugging.webp" width="1200">
<p>When I focus a button in shadow DOM, it returns the parent component.</p>
<img alt="Focus on button 2. Dev tools logs cus-tom12" height="313" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/pros-and-cons-of-shadow-dom/df7abd0217-1698950551/shadow-dom-debugging1.webp" width="1200">
<p>To workaround that I have to create a second live expression that logs <code>document.activeElement.shadowRoot.activeElement</code>.</p>
<img alt="Focus on button 2. Dev tools logs cus-tom12 and button.button2" height="384" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/pros-and-cons-of-shadow-dom/f08c80db10-1698950551/shadow-dom-debugging2.webp" width="1200">
<p>That's okay, but you get an annoying error when focusing an element without a shadow root.</p>
<img alt="Focus on button 1. Dev tools logs button.button1 and Uncaught TypeError: Cannot read properties of null…" height="384" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/pros-and-cons-of-shadow-dom/9a325cf235-1698950551/shadow-dom-debugging3.webp" width="1200">
<p>I understand that most of the cons described in this post are not critical issues, and there are ways to work around them. The thing is, it's just a lot of stuff we have to consider, and we could avoid that by simply not using Shadow DOM. Sounds easier than it is. I don't know; time will tell. I will keep you posted! :)</p>
<p>A big thank you to Egor, Dave, and Simon for their feedback!</p>
<h2>Further reading</h2>
<ul>
<li><a href="https://every-layout.dev/blog/eschewing-shadow-dom/">Eschewing Shadow DOM</a> by Heydon Pickering</li>
<li><a href="https://begin.com/blog/posts/2023-08-18-shadow-dom-not-by-default">Shadow DOM: Not by Default</a> by Simon MacDonald</li>
<li><a href="https://youtu.be/zEPXyqj7pEA">The CSS Cascade, a deep dive</a> by Bramus Van Damme </li>
<li><a href="https://alice.pages.igalia.com/blog/how-shadow-dom-and-accessibility-are-in-conflict/">How Shadow DOM and accessibility are in conflict</a> by Alice Boxhall</li>
<li><a href="https://dutchcelt.nl/posts/shadow-themes/">Shadow Themes</a> by Egor Kloos</li>
<li><a href="https://kinsta.com/blog/web-components/#ignored-inputs">A Complete Introduction to Web Components in 2023</a> by Craig Buckler</li>
<li><a href="https://nolanlawson.com/2022/11/28/shadow-dom-and-accessibility-the-trouble-with-aria/">Shadow DOM and accessibility: the trouble with ARIA</a> by Nolan Lawson</li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CPros+and+cons+of+using+Shadow+DOM+and+style+encapsulation%E2%80%9D">blog@matuzo.at</a>.</p> Day 102: selecting the scoping root2023-09-01T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day102
<p>There are different ways of selecting the scoping root inside a <code>@scope</code> rule.</p><p>When you use the <code>:scope</code> pseudo-class in a stylesheet, it matches the <code>:root</code> element.</p>
<pre><code class="language-css">:root {
border: 10px solid red;
}
:scope {
border-color: blue;
}
/* -> 10px blue border on the <html> element */</code></pre>
<p>When you use it inside a scope rule, it matches the rule's scoping root.</p>
<pre><code class="language-html"><div class="wrapper">
<div class="content">
the cascade is unavoidable
</div>
</div></code></pre>
<pre><code class="language-css">@scope (.wrapper) {
:scope {
border: 5px solid red;
}
}
/* -> 5px red border on the .wrapper element */</code></pre>
<p>Selectors inside a scope rule can only match elements that are in scope. Selecting <code>.content</code> within the <code>.wrapper</code> scope works:</p>
<pre><code class="language-css">@scope (.wrapper) {
.content {
background: aqua;
}
}
/* That's like writing .wrapper .content {} */</code></pre>
<p>Selecting <code>.wrapper .content</code> within the <code>.wrapper</code> scope doesn't work:</p>
<pre><code class="language-css">@scope (.wrapper) {
.wrapper .content {
background: aqua;
}
}
/* That's like writing .wrapper .wrapper .content {} */</code></pre>
<p>You can use <code>:scope</code> instead of <code>.wrapper</code>. That works because it doesn't match an element with the class <code>.wrapper</code> inside of <code>.wrapper</code>, but the scoping root itself.</p>
<pre><code class="language-css">@scope (.wrapper) {
:scope .content {
background: aqua;
}
}
/* That's like writing .wrapper .content {} */</code></pre>
<p>Instead of <code>:scope</code>, you can also use <code>&</code>.</p>
<pre><code class="language-css">@scope (.wrapper) {
& {
border: 5px solid orange;
}
& .content {
background: aqua;
}
}
/*
-> 5px orange border on the .wrapper element
and aqua background on .content.
*/</code></pre>
<p>There are two differences between <code>:scope</code> and <code>&</code> in this context. They're only evident if you have a list of scoping roots.</p>
<p>The first difference in specificity. <code>:scope</code> has the specificity of a pseudo-class. <code>&</code> takes on the specificity of the most specific selector in the selector list of scoping roots. In the following example <code>:scope</code> overrules <code>&</code> because <code>&</code> has the specificity of a tag selector.</p>
<pre><code class="language-html"><section id="section">
<h2>
<span>yo!</span>
</h2>
<p>
<span>yo!</span>
</p>
</section></code></pre>
<pre><code class="language-css">@scope (section, p) {
:scope {
border: 10px solid green;
}
& {
border: 10px solid red;
}
}
/* -> 10px green border on section and p */</code></pre>
<p>If you scope the section via its id instead of the tag, <code>&</code> takes on the specificity of an id selector and thus overrules <code>:scope</code>.</p>
<pre><code class="language-css">@scope (#section, p) {
& {
border: 10px solid red;
}
:scope {
border: 10px solid green;
}
}
/* -> 10px red border on the section and p */</code></pre>
<p>The second difference is that <code>:scope</code> only matches the scoping root itself. <code>&</code> can match any element that is matched by the selector list. </p>
<pre><code class="language-html"><section>
<h2>
<span>yo!</span>
</h2>
<p>
<span>yo!</span>
</p>
</section></code></pre>
<pre><code class="language-css">@scope (section, p) {
:scope span { background: fuchsia; }
/*
section span { }
p span { }
-> fuchsia background on span within h2 and p
*/
:scope & span { background: aqua; }
/*
section p span { }
-> aqua background only on span within p
*/
:scope :scope span { background: red }
/*
Doesn't match any element because `@scope (section, p)` only
defines multiple scopes, it doesn't nest them.
*/
}</code></pre>
<p>Essentially, that means that <code>:scope</code> can only match a scoping root and <code>&</code> can match an element in the selector list, regardless of whether it's considered a scoping root in that context. At least, that's how I interpret it. The spec is still pretty fucking hard to read.</p>
<p>To try out <code>@scope</code> you have to download <a href="https://www.google.com/chrome/canary/">Chrome Canary</a> and enable the <code>Experimental Web Platform features</code> flag in chrome://flags/.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+102%3A+selecting+the+scoping+root%E2%80%9D">blog@matuzo.at</a>.</p> New workshop: Advanced Modern CSS Masterclass2023-09-05T00:00:00+00:00https://www.matuzo.at/blog/2023/css-masterclass-workshop
<p>In my newest workshop I introduce you to the most useful <strong>modern features in CSS</strong> and show how you can implement them today in your code base to improve scalability, maintainability, and productivity.</p><h2>About the workshop</h2>
<p><code>clamp()</code>, <code>:is()</code>, <code>:where()</code>, <code>min()</code>, <code>max()</code>, <code>lab()</code>, <code>lch()</code>, <code>oklab()</code>, <code>oklch()</code>, cascade layers, container queries, logical properties, <code>:has()</code>, <code>svh</code>, <code>dvh</code>, <code>lvh</code>, font-pallets, subgrid…How do you feel when you read these terms? Confused? Interested? Excited? Overwhelmed?<br />
Well, yeah, that’s how most of us feel. CSS has changed so much in the last few years that it’s almost impossible to stay up-to-date.</p>
<p>2022 and 2023 were critical years for CSS. Browsers got together and began shipping new features week after week almost simultaneously. While that’s good news for us developers, keeping track of and adapting to those changes is challenging.</p>
<p>In <strong>Advanced Modern CSS Masterclass</strong>, a brand new <a href="https://smashingconf.com/online-workshops">Smashing Magazine workshop</a>, I introduce you to the latest and greatest features in CSS.</p>
<div class="content__video-wrapper"><div class="video-wrapper">
<iframe title="Introduction to the course" src="https://player.vimeo.com/video/856902125?h=36cc1eeb9c" width="640" height="360" frameborder="0" allowfullscreen></iframe>
</div></div>
<h2>What will you learn in this workshop?</h2>
<ul><li><strong>How to manage specificity </strong>with the help of cascade layers, :is(), and :where().</li><li>New <strong>colour spaces and functions</strong> and their differences</li><li>How to <strong>reduce the dependency of JavaScript</strong> for layout.</li><li>Using <strong>custom properties to create scalable systems</strong>.</li><li>New functions in CSS such as <strong>min()</strong>, <strong>max()</strong>,<strong> clamp().</strong></li><li>How to use<strong> container queries to create truly responsive components.</strong></li><li>The benefits of using <strong>logical properties </strong>and what they are.</li><li>Creating flexible layouts using <strong>Grid and math functions in CSS</strong>.</li><li>New pseudo-classes such as<strong> :has.</strong></li><li>Improving UX with<strong> view transitions. </strong></li><li>Applying the <strong>progressive enhancement </strong>principle<strong>.</strong></li><li>Using and tweaking<strong> color fonts.</strong></li><li><strong>Native nesting.</strong></li></ul>
<h2>Who is this for?</h2>
<p>This is a full workshop, delivered in five 2.5-hour long sessions with lots of time for Q&A. It’s for web developers (front-end and back-end), UI designers and for anyone who has at least a basic understanding of HTML and CSS.</p>
<h2>Time & Schedule</h2>
<p>This workshop is split over five days. The workshop sessions will run on the following days:</p>
<ul>
<li>Mon, November 27, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li>
<li>Tue, November 28, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li>
<li>Mon, December 4, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li>
<li>Mon, December 11, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li>
<li>Tue, December 12, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li>
</ul>
<p style="text-align: center; margin: 2em;">
<a class="cta" href="https://ti.to/smashingmagazine/online-workshops-2022/discount/welcometomyworkshop">
Get your ticket here! (15% off)
</a>
</p>
<p>If you have any further questions about the workshop, feel free to contact me via e-mail (<a href="manuel@matuzo.at">manuel@matuzo.at</a>).</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CNew+workshop%3A+Advanced+Modern+CSS+Masterclass%E2%80%9D">blog@matuzo.at</a>.</p> Web Components Accessibility FAQ2023-09-07T00:00:00+00:00https://www.matuzo.at/blog/2023/web-components-accessibility-faq
<p>I specialize in HTML and CSS, but I also write JS. Especially in the last year or so, I wrote quite a lot of JavaScript because we decided to port the front end of one of my clients to web components.</p>
<p>When I first learned about web components, I had a lot of questions, especially regarding accessibility. While I found answers to many of them, I didn’t know everything I would’ve wanted to know. I wish I had a catalog of all the essential questions and answers when I started. That’s why I decided to design this post in a Q&A format. I’ll ask a question regarding the accessibility of web components, and then I’ll answer it.</p><h2>Questions</h2><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWeb+Components+Accessibility+FAQ%E2%80%9D">blog@matuzo.at</a>.</p> Mark all as read2023-10-05T00:00:00+00:00https://www.matuzo.at/blog/2023/mark-all-as-read
<p>I was on the train home from Hamburg when I decided to finally migrate my website from Netlify and 11ty to Kirby on my friend's server. I got most of the work done on the train and made some final changes on Monday.</p><p>There's a lot you should consider when migrating from one tech stack to another. I didn't. I yoloed it, uploaded the site, and changed the DNS records, knowing there would be some damage. </p>
<p><strong>Is that smart?</strong> No. </p>
<p><strong>Did it work?</strong> Seems like it. At least, I didn't notice too much. There's only one thing: If you've subscribed to my site via RSS, you might see all of my ~200 posts as unread. I'm sorry. I've messed something up with the dates. Please ignore that and mark all posts as read. Thank you! </p>
<p><strong>Was it worth it?</strong> Yes, because now I'm one step closer to finally redesigning my site.</p>
<p>If you notice anything else broken, please let me know via mail. (<a href="mailto:manuel@matuzo.at">manuel@matuzo.at</a>)</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CMark+all+as+read%E2%80%9D">blog@matuzo.at</a>.</p> Skip links on ikea.com2023-10-11T00:00:00+00:00https://www.matuzo.at/blog/2023/ikea-skip-links
<p>I am always pleasantly surprised when I find useful skip links. That's why I decided to collect examples here on my blog.</p><p>I'll start with <a href="https://www.ikea.com/at/en/cat/wardrobes-19053/">ikea.com</a>. The first focusable element on every page is a skip link that allows you to skip the entire header, which makes sense because there are 15 interactive elements in it.</p>
<p><a href="https://www.matuzo.at/media/pages/blog/2023/ikea-skip-links/36a4b601b6-1698950546/ikea-skip-link1.jpg"><img alt="A "skip to main content" link at the top left corner of the page" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/ikea-skip-links/36a4b601b6-1698950546/ikea-skip-link1.jpg"></a></p>
<p>Sidenote: I'm not sure why they set <code>role="button"</code> on the skip link because it's just an anchor link, and there doesn't seem to be any JavaScript involved. </p>
<figure>
<pre><code class="language-html"><a href="#hnf-content" role="button" class="hnf-skip-to-content hnf-btn hnf-btn--secondary">
<span class="hnf-btn__inner">
<span class="hnf-btn__label">Skip to main content</span>
</span>
</a></code></pre>
<figcaption>Main skip link on ikea.com</figcaption>
</figure>
<p><strong>Update 13.10.23:</strong> The button role is gone now. Hours after I published this post, someone from the IKEA frontend team contacted me to tell me why they believed it was there. The next day, another person informed me that they'd removed it. Skål! 🔥</p>
<p>There's another skip link right after the header, which allows you to skip anything between the header and the products list. In this case, that's 11 more interactive elements.</p>
<p><a href="https://www.matuzo.at/media/pages/blog/2023/ikea-skip-links/c39f9d5ac2-1698950546/ikea-skip-link2.jpg"><img alt="A "skip to product list" link between the header and breadcrumbs" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/ikea-skip-links/c39f9d5ac2-1698950546/ikea-skip-link2.jpg"></a></p>
<figure>
<pre><code class="language-html"><a href="#product-list-skip" class="plp-skip-to-product-list">
Skip to product list
</a></code></pre>
<figcaption>Contextual inline skip link.</figcaption>
</figure>
<p>Both skip links are super helpful because people who rely on keyboard accessibility only have to press Tab twice instead of 26 times.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CSkip+links+on+ikea.com%E2%80%9D">blog@matuzo.at</a>.</p> Day 103: the prefers-reduced-transparency media feature2023-11-01T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day103
<p>Design trends like <a href="https://www.designstudiouiux.com/blog/what-is-glassmorphism-ui-trend/">Glassmorphism</a> use translucent backgrounds to create a specific visual effect, resulting in underlying background colors or elements shimmering through the background of the overlaying element. That may be visually appealing, but it can distract some people and impair legibility.</p><p>Operating systems like macOS and Windows offer options to reduce transparency in the operating system.</p>
<p><strong>macOS</strong>: System settings - Accessibility - Display - Reduce transparency.</p>
<p><strong>iOS</strong>: Settings - Accessibility - Display - Reduce transparency</p>
<p><strong>Window:</strong> Settings - Ease of Access - Display - Show transparency in Windows</p>
<figure>
<img alt="A comparisson of the same dialog on macOS. In the first you can see the background shinning through and the second has a solid background color." loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/100daysof-day103/764153a5cf-1698950549/settings-comparisson.png">
<figcaption>
The settings panel in macOS without (left) and with (right) reduced transparency active.
</figcaption>
</figure>
<p>In CSS, you can query that setting using the <code>prefers-reduced-transparency</code> media feature.</p>
<pre><code class="language-css">dialog {
background: rgba(255, 255, 255, var(--bg-opacity, 1));
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
@media(prefers-reduced-transparency: no-preference) {
dialog {
--bg-opacity: 0.2;
}
}
</code></pre>
<style>
[data-sample] {
transform: rotate(0);
background: linear-gradient(45deg, #53c8ff, #ff0067);
}
[data-sample] dialog {
--bg-opacity: 0.2;
background: rgba(255, 255, 255, var(--bg-opacity));
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
height: 80%;
border: none;
display: flex;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.3);
}
@media not all and (prefers-reduced-transparency), (prefers-reduced-transparency) {
[data-sample] dialog {
--bg-opacity: 1;
}
}
@media(prefers-reduced-transparency: no-preference) {
[data-sample]:not(.no-transparency) dialog {
--bg-opacity: 0.2;
}
}
[data-sample].no-transparency dialog {
--bg-opacity: 1;
}
</style>
<p>Here's how the dialog looks by default:</p>
<div data-sample="with transparency">
If you came to conquer, you'll be king for a day
But you too will deteriorate and quickly fade away
And believe these words you hear when you think your path is clear
We have no control
We have no control
We have no control
We do not understand, you have no control
You are not in command
You have no control
We have no control
No control, no control
You have no control
<dialog open>
<h1>We have no control</h1>
</dialog>
</div>
<p>Here's how it looks with reduced transparency active:</p>
<div data-sample="without transparency" class="no-transparency">
If you came to conquer, you'll be king for a day
But you too will deteriorate and quickly fade away
And believe these words you hear when you think your path is clear
We have no control
We have no control
We have no control
We do not understand, you have no control
You are not in command
You have no control
We have no control
No control, no control
You have no control
<dialog open>
<h1>We have no control</h1>
</dialog>
</div>
<p>Currently supported in <a href="https://caniuse.com/mdn-css_at-rules_media_prefers-reduced-transparency">Chrome, Edge, and Firefox behind a flag</a>.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+103%3A+the+prefers-reduced-transparency+media+feature%E2%80%9D">blog@matuzo.at</a>.</p> Day 104: animation with registered custom properties2023-11-02T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day104
<p>All major browsers except Firefox (coming soon!) support the <a href="/blog/2023/100daysof-day84">@property at-rule</a>. It enables you to do things you previously couldn't, like animating custom properties.</p><p>Let's say you have two animations. One fades an element; the other moves it. Based on their motion preference, users see one or the other.</p>
<style>
.banner {
background: #000;
color: #fff;
display: inline-block;
font-size: 3rem;
padding: 0rem 1.5rem;
}
.banner1 {
--animation: fade;
}
.animate1{
animation: var(--animation) 3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
.banner2 {
--animation2: fade2;
translate: var(--position) 0;
opacity: var(--opacity);
}
.animate2{
animation: var(--animation2) 3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
.banner3 {
--animation3: fade3;
translate: var(--position3) 0;
opacity: var(--opacity3);
}
.animate3{
animation: var(--animation3) 3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
@media(prefers-reduced-motion: no-preference) {
.banner {
--animation: move;
--animation2: move2;
--animation3: move3;
}
}
@keyframes move {
from { translate: 100vw; }
to { translate: 0; }
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes move2 {
from { --position: 100vw; }
to { --position: 0; }
}
@keyframes fade2 {
from { --opacity: 0; }
to { --opacity: 1; }
}
@property --position3 {
syntax: "<length>";
inherits: false;
initial-value: 0;
}
@property --opacity3 {
syntax: "<number>";
inherits: false;
initial-value: 1;
}
@keyframes move3 {
from { --position3: 100vw; }
to { --position3: 0; }
}
@keyframes fade3 {
from { --opacity3: 0; }
to { --opacity3: 1; }
}
</style>
<pre><code class="language-css">.banner {
--animation: fade;
animation: var(--animation) 3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
@media(prefers-reduced-motion: no-preference) {
.banner {
--animation: move;
}
}
@keyframes move {
from { translate: 100vw; }
to { translate: 0; }
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}</code></pre>
<div data-sample="demo">
<strong class="banner banner1 animate1">Hello World!</strong>
<p>
<button class="btn1">Replay</button>
</p>
<script>
document.querySelector('.btn1').addEventListener('click', e => {
document.querySelector('.banner1').classList.remove('animate1')
setTimeout(()=> {
document.querySelector('.banner1').classList.add('animate1')
},0)
})
</script>
</div>
<p>That works nice and well, but if you use custom properties in your keyframe animations instead of redeclaring regular properties, it doesn't work anymore because you're trying to animate string values.</p>
<pre><code class="language-css">.banner {
--animation: fade;
translate: var(--position) 0;
opacity: var(--opacity);
animation: var(--animation) 3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
@media(prefers-reduced-motion: no-preference) {
.banner {
--animation: move;
}
}
@keyframes move {
from { --position: 100vw; }
to { --position: 0; }
}
@keyframes fade {
from { --opacity: 0; }
to { --opacity: 1; }
}</code></pre>
<div data-sample="demo">
<strong class="banner banner2">Hello World!</strong>
<p>
<button class="btn2">Replay</button>
</p>
<script>
document.querySelector('.btn2').addEventListener('click', e => {
document.querySelector('.banner2').classList.remove('animate2')
setTimeout(()=> {
document.querySelector('.banner2').classList.add('animate2')
},0)
})
</script>
</div>
<p>With the <code>@property</code> at-rule you can turn the two string values into a <code><length></code> and a <code><number></code>, which makes them animatable.</p>
<pre><code class="language-css">@property --position {
syntax: "<length>";
inherits: false;
initial-value: 0;
}
@property --opacity {
syntax: "<number>";
inherits: false;
initial-value: 1;
}
@keyframes move {
from { --position: 100vw; }
to { --position: 0; }
}
@keyframes fade {
from { --opacity: 0; }
to { --opacity: 1; }
}</code></pre>
<div data-sample="demo">
<strong class="banner banner3">Hello World!</strong>
<p>
<button class="btn3">Replay</button>
</p>
<script>
document.querySelector('.btn3').addEventListener('click', e => {
document.querySelector('.banner3').classList.remove('animate3')
setTimeout(()=> {
document.querySelector('.banner3').classList.add('animate3')
},0)
})
</script>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+104%3A+animation+with+registered+custom+properties%E2%80%9D">blog@matuzo.at</a>.</p> Totally remdom, or How browsers zoom text2023-11-03T00:00:00+00:00https://www.matuzo.at/blog/2023/how-browsers-zoom-text
<p>Last week, I lied to my students. After I explained how the rem unit worked, I told them that they could compare <code>px</code> and <code>rem</code> by increasing the font size in their mobile browsers and see how it affects text zoom.</p><p>Before I said that, we created a <a href="https://codepen.io/matuzo/pen/gOqMywj?editors=1100">simple test page</a> with two paragraphs and two rules. </p>
<p class="code-label"><strong>HTML</strong></p>
<pre><code class="language-html"><p class="px">Hello World!</p>
<p class="rem">Hello World!</p></code></pre>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">.px {
font-size: 16px;
}
.rem {
font-size: 1rem;
}</code></pre>
<p>Then, we opened the settings in Chrome, found the <em>Appearance</em> page, and changed the font size from <em>medium</em> to <em>very large</em>. The font size of the second paragraph, which uses rem, increased with the default font size in the browser. The first paragraph, which uses px, stayed the same.</p>
<p><a href="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/fe6981a9e6-1699006069/chrome-desktop-settings.webp"><img alt="Settings in Desktop Chrome on the right with the default font size set to large. On left two paragraphs. The font size of the second one is much larger.'" height="523" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/fe6981a9e6-1699006069/chrome-desktop-settings.webp" width="912"></a></p>
<div class="highlight">
<strong>Other browsers</strong>
<p>You can do the same in Firefox by visiting the <em>General</em> page in the browser settings, finding the <em>Fonts</em> section, and picking a font size larger than 16 in the <em>size</em> dropdown.</p>
<p>In Desktop Safari, you can define a minimum font size under <em>Settings</em> - <em>Advanced</em> - <em>Accessibility</em>, but it doesn't make a difference whether you use rem or px. </p>
</div>
<p>That's how I proved to them that using <code>rem</code> is a good idea because it respects user preferences and allows the UI to scale with the default font size set in the browser.</p>
<h2>Mobile browsers</h2>
<p>I'm currently writing the <a href="https://www.oreilly.com/library/view/web-accessibility-cookbook/9781098145590/">Accessibility Cookbook</a> for O'Reilly, and I wanted to give my readers the same advice but with a bit more detail.</p>
<p>Here's what I learned about <code>rem</code> and mobile operating systems and browsers:</p>
<h3>Safari</h3>
<p>In Safari, you can zoom the entire page by going to <em>Settings</em> - <em>Safari</em> - <em>Page Zoom</em>. Another option is to click the “small A Large A” icon in the address bar directly in Safari, as shown in the following screenshot. There, you can zoom text only instead of the entire page.<br />
However, both settings treat px and rem-based values equally.</p>
<img alt="A popup showing several page settings. One of them allows you to adjust the zoom level between 50% and 300%." height="1088" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/d4a443d893-1699006069/safari-mobile-settings.webp" width="506">
<h3>Chrome on Android</h3>
<p>In Chrome on Android, you can click on the three dots (kebab icon) next to the address bar and select <em>Settings</em> - <em>Accessibility</em>. There, you can change the text scaling.</p>
<p>I did that, and nothing happened on my test page. It did work on my personal site, though,...partially (we'll talk about that later).</p>
<img alt="Text scaled up to 200% in the settings, but the font size didn't change. demo on the left, settings on the right." height="562" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/f03db0c39a-1699006068/chrome-mobile-settings.webp" width="912">
<p>I did some debugging, and I learned that Chrome's text scaling only kicks in when there are at least 122 characters on the page (<a href="https://www.urbandictionary.com/define.php?term=For+fucks+sake">FFS!</a>).</p>
<p>Retrying the test with more than 122 characters shows that Chrome also treats rem and px-based font sizes equally.</p>
<img alt="Two paragraphs saying Hello World, with Hello repeated 20 times. The fonts size of both paragraphs is the same." height="539" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/d3d372467a-1699006068/chrome-mobile-settings2.webp" width="912">
<p>Of course, we don't just use <code>rem</code> for text sizing only. When you add a <code>width</code> to each paragraph, the second paragraph should be larger than the first if the base font size is larger than 16px.</p>
<pre><code class="language-css">.px {
font-size: 16px;
border: 1px solid red;
width: 320px;
}
.rem {
font-size: 1rem;
border: 1px solid red;
width: 20rem;
}</code></pre>
<p>Nope, both paragraphs have the same width, but the text got smaller. Why? Because text scaling on mobile Chrome is an unpredictable mess.</p>
<img alt="A red border shows that both pargraphs have the same width." height="539" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/2bd40c706d-1699006069/chrome-mobile-settings3.webp" width="912">
<p>Just look closely at my site, with text scaling set to 200%. Are you noticing anything? The text in the nav didn't scale. Wanna know why? Because I set <code>display: flex</code> on the ul. WTF?!</p>
<img alt="My homepage on the left with a regular font size in the nav, but large for the rest. On the right the same font size for the nav and text due to display: flex being removed from the nav." height="987" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/8c5d815949-1699006069/chrome-mobile-settings4.webp" width="912">
<p>There's more strange stuff I found, but I'll spare you the details because it's just too messy. Also, text scaling in Chrome should improve in an upcoming version, according to <a href="https://blog.google/products/android/new-android-features-february-2023/">a post on the Google blog</a>. It's from February 2023, and according to how they announced the feature, I would've expected it had already landed, but I don't see it on my phone (Pixel 7, Android 13, Chrome 118). </p>
<p>You can enable it by navigating to <code>chrome://flags</code>, searching for <em>zoom</em>, and enabling <em>Accessibility Page Zoom</em>. Now, all the bugs (features!?) mentioned earlier don't apply anymore, but Chrome still treats <code>px</code> and <code>rem</code> the same.</p>
<img alt="Both paragraphs now only contain "hello world" as their text again and they're both scaled according to the zoom level in the settings." height="987" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/d1d645f2a4-1699006069/chrome-mobile-settings5.webp" width="912">
<h3>Firefox on Android</h3>
<p>In Firefox on Android, you can click on the three dots (kebab icon) next to the address bar and select <em>Settings</em> - <em>Accessibility</em>. There, you can change the text scaling. It works like the improved text scaling feature in Chrome and treats <code>px</code> and <code>rem</code> equally.</p>
<img alt="Both paragraphs scaled according to the zoom level in the settings." height="987" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2023/how-browsers-zoom-text/14d907926f-1699006068/firefox-mobile-settings.webp" width="912">
<h2>So what?</h2>
<p>Nothing, keep using <code>rem</code>. People will benefit from it. It's just good to know that some (most?) operating systems and browsers do their own thing. Ultimately, it doesn't matter how browsers zoom text; what matters is that users can use that feature and that you build interfaces that respect user preferences. Part of that is working with relative units like rem.</p>
<p>One more thing: it's absolutely possible I'm wrong about some things because I don't know the ins and outs of all mobile operating systems and browsers. If that's the case, please correct me via <a href="mailto:manuel@matuzo.at">email</a> (manuel@matuzo.at) or find me on <a href="https://front-end.social/@matuzo/111346067326308020" rel="noopener">Mastodon</a>. :)</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CTotally+remdom%2C+or+How+browsers+zoom+text%E2%80%9D">blog@matuzo.at</a>.</p> Removing list styles without affecting semantics.2023-11-06T00:00:00+00:00https://www.matuzo.at/blog/2023/removing-list-styles-without-affecting-semantics
<p>Some people, I guess primarily developers and not actual users, don’t like the fact that <a href="https://gerardkcohen.me/writing/2017/voiceover-list-style-type.html">Safari removes list semantics</a> of lists that don’t look like lists (<code>list-style: none</code>). Scott O’Hara provided a <em>fix</em> in <a href="https://www.scottohara.me/blog/2019/01/12/lists-and-safari.html">“Fixing” Lists</a>, where he suggests setting <code>role="list"</code> explicitly on the list to re-add list semantics. </p>
<pre><code class="language-html"><ul style="list-style: none" role="list">
<li>…</li>
</ul></code></pre>
<p>That works, but I found a way of removing list styles without affecting semantics.</p><p>I learned in my post (lol) “<a href="/blog/heres-what-i-didnt-know-about-list-style-type/">Here’s what I didn’t know about list-style-type</a>” that you can use a string as the value of the <code>list-style-type</code> property. </p>
<p>Yesterday, I tried setting it to an empty string, and voilà, list style gone, semantics kept.</p>
<pre><code class="language-css">ul {
list-style-type: "";
}</code></pre>
<div data-sample="demo">
<ul style="list-style-type: ''; padding: 0; margin: 0;">
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
</div>
<p>This “solution” probably needs thorough testing, but I get the same results as with the <code>role</code> solution in the following screen readers and browsers:</p>
<ul>
<li>VO with Safari on iOS 15.7.7</li>
<li>Talkback with Chrome 118 on Android 13</li>
<li>VO with Safari 16.5.2 on macOS 13.4.1</li>
<li>NVDA 2023.2 with Firefox 119 (NVDA even announces “bullet”)</li>
<li>JAWS 2023.2307.37 with Chrome 119 on Windows 11</li>
<li>Narrator with Edge 119 on Windows 11</li>
</ul>
<p>Here's an <a href="https://codepen.io/matuzo/pen/abXBrGE?editors=1100">open Codepen</a> and a <a href="https://codepen.io/matuzo/debug/abXBrGE">debug version</a> you can use for testing.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CRemoving+list+styles+without+affecting+semantics.%E2%80%9D">blog@matuzo.at</a>.</p> Day 105: defining multiple syntax components2023-11-10T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day105
<p>As already explained on <a href="/blog/2023/100daysof-day84">day 84</a>, using the <code>syntax</code> descriptor, you can define the type of a custom property in an <code>@property</code> at-rule.</p><pre><code class="language-css">@property --lh {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
button {
--lh: 1;
line-height: var(--lh);
transition: --lh 1s;
width: min-content;
}
button:is(:focus-visible,:hover) {
--lh: 2;
}</code></pre>
<style>
@property --lh {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
@property --lhfixed {
syntax: '<number> | <length-percentage>';
inherits: false;
initial-value: 1;
}
.button {
--lh: 1;
line-height: var(--lh);
transition: --lh 1s;
width: min-content;
}
.button1:is(:focus-visible,:hover) {
--lh: 2;
}
.button2 {
--lh: 16px;
}
.button2:is(:focus-visible,:hover) {
--lh: 32px;
}
.button3 {
--lhfixed: 16px;
line-height: var(--lhfixed);
transition: --lhfixed 1s;
}
.button3:is(:focus-visible,:hover) {
--lhfixed: 32px;
}
</style>
<div data-sample="demo">
<button class="button button1">
Hello World
</button>
</div>
<p>That works well, but some properties, like <code>line-height</code>, <a href="https://drafts.csswg.org/css-inline/#line-height-property">support different types of values</a>.</p>
<p>If you use a length instead of a number, the transition doesn't work anymore because the provided value of the custom property must be valid as defined in the syntax descriptor.</p>
<pre><code class="language-css">@property --lh {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
button {
--lh: 16px;
line-height: var(--lh);
transition: --lh 1s;
width: min-content;
}
button:is(:focus-visible,:hover) {
--lh: 32px;
}</code></pre>
<div data-sample="demo">
<button class="button button2">
Hello World
</button>
</div>
<p>The good news is you can define multiple <a href="https://drafts.css-houdini.org/css-properties-values-api/#supported-names">syntax component names</a> using the '|' Combinator</p>
<pre><code class="language-css">@property --lh {
syntax: '<number> | <length-percentage>';
inherits: false;
initial-value: 1;
}
button {
--lh: 16px;
line-height: var(--lh);
transition: --lh 1s;
width: min-content;
}
button:is(:focus-visible,:hover) {
--lh: 32px;
}</code></pre>
<div data-sample="demo">
<button class="button button3">
Hello World
</button>
</div><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+105%3A+defining+multiple+syntax+components%E2%80%9D">blog@matuzo.at</a>.</p> Day 106: the scripting media feature2023-11-13T00:00:00+00:00https://www.matuzo.at/blog/2023/100daysof-day106
<p>The <a href="https://drafts.csswg.org/mediaqueries-5/#scripting">scripting media feature</a> is an excellent addition to CSS for those who believe in progressive enhancement: It enables you to detect whether scripting languages, such as JavaScript, are supported.</p><p>If you disable Javascript in <a href="https://caniuse.com/mdn-css_at-rules_media_scripting">Firefox 117+, Chrome 120+, or Safari 17+</a> on this page, the disclosure widget below hides the button and displays the content instead.</p>
<style>
[aria-expanded="false"] + .disclosure-content {
display: none;
}
@media (scripting: none) {
[aria-expanded="false"] + .disclosure-content {
display: block;
}
}
@media (scripting: enabled) {
[aria-expanded="false"] + .disclosure-content {
display: none;
}
}
@media (scripting: none) {
button[aria-expanded] {
display: none;
}
}
</style>
<div data-sample="demo">
<div class="disclosure">
<button aria-expanded="false">
Show details
</button>
<div class="disclosure-content" id="content">
<p>Detailed content goes here…</p>
</div>
</div>
</div>
<script>
const button = document.querySelector('button');
button.addEventListener('click', e => {
button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === "false")
})
</script>
<p class="code-label"><strong>CSS</strong></p>
<pre><code class="language-css">@media (scripting: enabled) {
.disclosure-content {
display: none;
}
}
@media (scripting: none) {
button[aria-expanded] {
display: none;
}
}</code></pre>
<p class="code-label"><strong>HTML</strong></p>
<pre><code class="language-html"><div class="disclosure">
<button aria-expanded="false">
Show details
</button>
<div class="disclosure-content" id="content">
<p>Detailed content goes here…</p>
</div>
</div></code></pre>
<p class="code-label"><strong>JS</strong></p>
<pre><code class="language-js">const button = document.querySelector('button');
button.addEventListener('click', e => {
button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === "false")
})</code></pre>
<p>PS: Yes, I know, it's better not to ship the button in the first place instead of hiding it.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CDay+106%3A+the+scripting+media+feature%E2%80%9D">blog@matuzo.at</a>.</p> Not all automated testing tools support Shadow DOM in web components2024-01-02T10:55:00+00:00https://www.matuzo.at/blog/2024/automated-testing-tools-and-web-components
<p>There isn't much more to say; it's all in the title.</p>
<p>Many automated testing tools are a collection of JavaScript functions you run on a page. Most of those rely on querying the DOM. If a tool doesn't consider shadow trees, it only catches accessibility errors in the Light DOM, which may give you a wrong sense of safety and potentially affect your users.</p><style>
tbody td {
background: green;
color: #fff;
}
td.no {
background: #bf0404;
}
td.noish {
background: #bf8504;
}
</style>
<p>That's one more reason <a href="/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/">not to rely on automated testing only</a>.</p>
<p>That doesn't mean you shouldn't use automated testing if your site contains web components. You just have to ensure that the tool you're using supports them. A good way of doing that is creating a simple component that contains some accessibility bugs caused by nodes attached to the component's Shadow DOM, like the following component.</p>
<pre><code class="language-html"><main>
<h1 id="h1">Testing a11y bugs in web components</h1>
<a href="#el">Jump to elem in shadow</a> <!-- Broken skip link -->
<some-bugs></some-bugs>
</main>
<script>
class SomeBugs extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>:host { color: #efefef }</style> <!-- Low contrast. -->
<h4 id="el">Heading</h4> <!-- Skipped heading. -->
<img src="#test"> <!-- Missing alt. -->
<button aria-labelledby="h1"></button> <!-- Broken ARIA reference/missing accessible name. -->
<input type="text" id="input"> <!-- Missing label. -->
`
}
}
customElements.define('some-bugs', SomeBugs);
</script></code></pre>
<p>CodePen: <a href="https://codepen.io/matuzo/pen/yLwYNYY?editors=1010">Test web component containing accessibility bugs</a></p>
<p>If you add this component to a page, testing tools should find at least five issues. I picked five popular tools: <a href="https://www.deque.com/axe/">axe DevTools</a>, <a href="https://developer.chrome.com/docs/lighthouse/overview/">Google Lighthouse</a>, <a href="https://www.tpgi.com/arc-platform/arc-toolkit/">ARC Toolkit</a>, <a href="https://wave.webaim.org/extension/">WAVE</a>, and <a href="https://www.ibm.com/able/toolkit/verify/automated/">IBM Equal Access (IBM EAAC)</a>, and ran them against the component.</p>
<p>axe DevTools and Lighthouse, both based on axe-core, found all five issues.<br />
WAVE and ARC found no problems in Shadow DOM, but WAVE reported a broken skip link in Light DOM. All other tools don’t check for broken anchor links at all. Arguably, because it’s not an obvious accessibility issue or WCAG violation, but according to the Web Aim 1 Million report, <a href="https://webaim.org/projects/million/#skip">one out of every six skip links</a> on tested sites that contained skip links were broken. They should check whether targets of anchor links exist.<br />
IBM EA found all issues except the skipped heading, which can be considered a bad practice and not a violation, which IBM EA doesn't report anyway, regardless of the type of DOM. </p>
<div class="table-wrapper" aria-labelledby="form1" tabindex="0" role="region" style="width: 100%; grid-column: 4 / -4">
<table style="width: 100%;">
<caption id="form1">Comparison of issues reported in different testing tools</caption>
<thead>
<tr>
<td>Bug</td>
<th>axe</th>
<th>WAVE</th>
<th>ARC</th>
<th>IBM EAAC</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Detected issues</th>
<td>5</td>
<td class="no">1</td>
<td class="no">0</td>
<td>4</td>
</tr>
<tr>
<th scope="row">Broken label/input</th>
<td>yes</td>
<td class="no">no</td>
<td class="no">no</td>
<td>yes</td>
</tr>
<tr>
<th scope="row">Broken ARIA</th>
<td>yes</td>
<td class="no">no</td>
<td class="no">no</td>
<td>yes</td>
</tr>
<tr>
<th scope="row">Missing alt</th>
<td>yes</td>
<td class="no">no</td>
<td class="no">no</td>
<td>yes</td>
</tr>
<tr>
<th scope="row">Skipped heading</th>
<td>yes</td>
<td class="no">no</td>
<td class="no">no</td>
<td class="no">no</td>
</tr>
<tr>
<th scope="row">Low contrast</th>
<td>yes</td>
<td class="no">no</td>
<td class="no">no</td>
<td>yes</td>
</tr>
<tr>
<th scope="row">Broken anchor link</th>
<td class="noish" aria-describedby="note">no*</td>
<td>yes</td>
<td class="noish" aria-describedby="note">no*</td>
<td class="noish" aria-describedby="note">no*</td>
</tr>
</tbody>
</table>
</div>
<p id="note">*not a direct accessibility concern, but should be checked.</p>
<p>ARC plans to support Shadow DOM in early 2024. I'm not aware if Shadow DOM is on WAVE's roadmap, but in the meantime, I'd recommend not using those tools with sites that contain web components or using another tool to double-check.</p>
<p>The same applies to HTML validators. The official <a href="https://validator.w3.org/">W3C validator</a>, for example, doesn't support Shadow DOM. </p>
<h2>Update 04.01.24</h2>
<p>Added info about ARC's plans to support Shadow DOM in early 2024.</p>
<h2>Update 08.01.24</h2>
<p>Added clarification about the broken skip link bug.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CNot+all+automated+testing+tools+support+Shadow+DOM+in+web+components%E2%80%9D">blog@matuzo.at</a>.</p> Workshop: Deep Dive on Accessibility Testing2024-01-02T13:05:00+00:00https://www.matuzo.at/blog/2024/workshop-deep-dive-on-accessibility-testing
<p>Once again, I’ve teamed up with my friends at <a href="https://www.smashingmagazine.com/">Smashing Magazine</a> 😻 to share with you everything I know about web accessibility testing! In this <a href="https://smashingconf.com/online-workshops/workshops/manuel-matuzovic/">smashing workshop</a> we’ll talk about automatic and manual testing, screen reader basics, Single Page Applications, Dev Tools, and more.</p><!-- teaser -->
<p>Sounds interesting? Great! Here are some more details about the workshop:</p>
<video src="/blog/2024/workshop-deep-dive-on-accessibility-testing/workshop_promo.mp4" controls>
<track default kind="captions" srclang="en" src="/blog/2024/workshop-deep-dive-on-accessibility-testing/workshop_promo.vtt" label="English">
<track default kind="subtitles" srclang="de" src="/blog/2024/workshop-deep-dive-on-accessibility-testing/workshop_promo_de.vtt" label="Deutsch">
Sorry, your browser doesn't support embedded videos.
</video>
<h2>What you will learn in this workshop</h2>
<ul><li>Which <strong>testing tools</strong> are available and most commonly used.</li><li>How to <strong>assess the accessibility</strong> of a component or page.</li><li>The difference between <strong>automatic and manual testing</strong>.</li><li>How to use <strong>automatic accessibility testing</strong> tools and how to interpret the results.</li><li>How to use the <strong>keyboard to discover accessibility bugs</strong>.</li><li><strong>Screen reader basics</strong> and how to use them for accessibility testing, both on desktop and mobile devices.</li><li>How to test the accessibility of <strong>Single Page Applications</strong>.</li><li>Common <strong>pitfalls of Single Page Applications</strong> and how to avoid them.</li><li>How to <strong>integrate accessibility testing</strong> in your day-to-day development workflow.</li><li>Running <strong>tests on the command line</strong> and creating automated reports.</li><li><strong>Integrating accessibility testing in your build pipeline</strong>.</li><li>Where to find <strong>information and help on how to build complex components</strong>.</li></ul>
<h2>Time and schedule</h2>
<p>This workshop is split over <strong>five days</strong>. The workshop sessions will run on the following days:</p>
<ul><li>Mon, January 8, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Tue, January 9, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Mon, January 15, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Tue, January 16, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li><li>Mon, January 22, <span class="small-caps">09:00 – 11:30 AM PT (18:00 – 20:30 CET)</span></li></ul>
<h2>Ticket</h2>
<p>If you have any questions about the workshop, feel free to get in touch <a href="manuel@matuzo.at">via mail</a>. I hope to see you soon!</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CWorkshop%3A+Deep+Dive+on+Accessibility+Testing%E2%80%9D">blog@matuzo.at</a>.</p> My CSS wish list 20242024-01-11T00:00:00+00:00https://www.matuzo.at/blog/2024/css-wish-list
<p>Following <a href="/blog/2023/css-wish-list">last year</a>, I created a CSS wishlist for 2024.</p><p>Before I get into the details, I have to say that creating a wishlist almost feels wrong because there are more features in CSS today than I could have ever wished for. So, I'd like to take this opportunity to thank everyone who contributed to CSS being where it is today: spec writers, invited experts, browser developers, everyone involved in Interop 202*, dev rels, bloggers, and people who report bugs and contribute to discussions about new and existing features. Thank you very much!</p>
<h2>Container Style Queries</h2>
<p>We <strong>need</strong> container style queries in all browsers. They will change how we write CSS and make our stylesheets more flexible, scalable, and robust. You can read why I believe that on the <a href="https://12daysofweb.dev/2023/container-style-queries/">12 Days of Web</a>, or you can watch <a href="https://youtu.be/L668dK6wFcM?t=1411">my talk from last year's CSS Day</a>.</p>
<p>Current support: Chrome/Edge 111+</p>
<h2>Scope</h2>
<p>I understand how someone can be skeptical about the usefulness of <a href="/blog/2023/100daysof-day101">scoping</a>, but trust me, once you learn how it works and what it can do for you, you'll also be convinced it's another missing piece in the puzzle.</p>
<p>Current support: Chrome/Edge 118+, Safari TP</p>
<h2>Mixins</h2>
<p><a href="https://github.com/w3c/csswg-drafts/issues/9350">Mixins in native CSS</a>? Yes, please!</p>
<p>Current support: none</p>
<h2>State Queries</h2>
<p>State queries will allow us to query when something is in a specific state, like sticky. Here's an <a href="https://www.youtube.com/watch?v=dkBeBxs48os&t=1026s">early demo</a> presented by Una.</p>
<p>Current support: none</p>
<h2>Gammut Mapping</h2>
<p>I was disappointed to learn that <a href="https://front-end.social/@leaverou/111491942156530717">there's no real gamut mapping in oklch/oklab</a>. I hope browser vendors listen to the <a href="https://github.com/w3c/csswg-drafts/issues/9449">feedback from developers</a> and reconsider their decision.</p>
<h2>Animation of discrete properties</h2>
<p>The <a href="https://codepen.io/matuzo/pen/oNmKBGo">transition-behavior</a> property and the <a href="https://codepen.io/matuzo/pen/MWLNJRj?editors=1100">@starting-style at-rule</a> are super useful because they allow you to animate discrete properties like <code>display</code>. I'd love to see support in all browsers.</p>
<p>Current support: Chrome/Edge 117+</p>
<h2>Other wish lists</h2>
<p>Here are some other 2024 CSS wish lists:</p>
<ul>
<li><a href="https://cloudfour.com/thinks/tylers-css-wish-list-for-2024/">Tyler Sticka</a></li>
<li><a href="https://knowler.dev/blog/2024-css-wishlist">Nathan Knowler</a></li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CMy+CSS+wish+list+2024%E2%80%9D">blog@matuzo.at</a>.</p>