<?xml version="1.0" encoding="utf-8"?>
  <rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom"
    xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
    xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
    xmlns:georss="http://www.georss.org/georss"
    xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
  >
    <channel>
      <title>Piccalilli - Everything</title>
      <link>https://piccalil.li/</link>
      <atom:link href="https://piccalil.li/feed.xml" rel="self" type="application/rss+xml" />
      <description>We are Piccalilli. A publication dedicated to providing high quality educational content to level up your front-end skills.</description>
      <language>en-GB</language>
      <copyright>Piccalilli - Everything 2026</copyright>
      <docs>https://www.rssboard.org/rss-specification</docs>
      <pubDate>Thu, 10 Sep 2026 14:04:09 GMT</pubDate>
      <lastBuildDate>Thu, 10 Sep 2026 14:04:09 GMT</lastBuildDate>

      
      <item>
        <title>Save 35% on all courses for two weeks only</title>
        <link>https://piccalil.li/links/pricefall-2026/?ref=main-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Wed, 09 Sep 2026 09:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/links/pricefall-2026/?ref=main-rss-feed</guid>
        <description><![CDATA[<p>The summer is over and the autumn (or fall, for our American friends) is here, so we're offering a <strong>huge 35% discount on all courses</strong> if you use the coupon code <code>PRICEFALL</code> at checkout.</p>
<p></p>
<p>This is the time of year where people, fresh off a summer break, like to skill up, so we're making that easier with this large discount.</p>
<p>We're running the discount for only two weeks — ending September 23 — so make sure you don't miss out.</p>
<h2>Purchasing Power Parity (PPP)</h2>
<p>Our PPP discounts are always based on the full price of the course, so our system will work out which deal is going to be the best for you.</p>
<p>If your PPP discount is cheaper than the <code>PRICEFALL</code> discount, we'll present that. If the <code>PRICEFALL</code> discount is cheaper, we'll present that.</p>
<p>We're all about people getting incredibly high quality education that's as affordable as possible.</p>
<p></p>
<h2>If you need to convince your boss</h2>
<p>We've got you covered with letter templates for every course:</p>
<ol>
<li><a href="https://piccalil.li/blog/we-made-an-email-template-to-help-convince-your-boss-to-pay-for-complete-css/">Complete CSS</a></li>
<li><a href="https://piccalil.li/blog/we-made-an-email-template-to-help-convince-your-boss-to-pay-for-javascript-for-everyone/">JavaScript for Everyone</a></li>
<li><a href="https://piccalil.li/blog/we-made-an-email-template-to-help-convince-your-boss-to-pay-for-mindful-design/">Mindful Design</a></li>
</ol>
        <p><a href="https://piccalil.li/courses/?utm_source=piccalilli-link&utm_campaign=price-fall-2026">Check it out!</a></p>
        ]]></description>
        
      </item>
    
      <item>
        <title>The Index: Issue #196</title>
        <link>https://piccalil.li/the-index/196/?ref=main-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Fri, 28 Aug 2026 09:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/the-index/196/?ref=main-rss-feed</guid>
        <description><![CDATA[<p>Before we get into this week's links, I just thought I'd let you know we're having a summer break next week, so the next issue won't be with you until September 11!</p>
<h2><a href="https://midnightvinylclub.com/?utm_source=the-index&amp;utm_medium=newsletter">Midnight Vinyl Club</a></h2>
<p>A handy service that helps you explore music and find the best prices for vinyl.</p>
<h2><a href="https://elliotjaystocks.com/blog/spotify?utm_source=the-index&amp;utm_medium=newsletter">The search for a Spotify alternative</a></h2>
<p>There are so many better options than Spotify for music and Elliot helpfully breaks plenty of options for you.</p>
<h2><a href="https://michellebarker.co.uk/writing/little-websites-everywhere/?utm_source=the-index&amp;utm_medium=newsletter">Little websites everywhere</a></h2>
<p>This is a great piece. As we see it, the web — at large — always  wins and those green shoots will come.</p>
<h2><a href="https://reelswap.net/?utm_source=the-index&amp;utm_medium=newsletter">ReelSwap</a></h2>
<p>A really cool service for cataloging physical media. The bookshelves are really nicely done too.</p>
<h2><a href="https://www.keithcirkel.co.uk/wire-loop/?utm_source=the-index&amp;utm_medium=newsletter">WireLoop</a></h2>
<p>An extremely satisfying web-based game.</p>
<h2><a href="https://piccalil.li/blog/nan-the-not-a-number-number-that-isnt-nan/?utm_source=the-index&amp;utm_medium=newsletter">NaN, the not-a-number number that isn’t NaN</a></h2>
<p>Here's one from the <a href="https://piccalil.li/blog/">Piccalilli archives</a> that you might have missed to wrap up this issue.</p>
<hr />
<p>P.S. <a href="https://www.ysabella.me/?utm_source=the-index&amp;utm_medium=newsletter">this is a good website</a> from <a href="https://personalsit.es/?utm_source=the-index&amp;utm_medium=newsletter">personalsit.es</a>.</p>
        
        <h2>Sponsor message</h2><p>There's no sponsor this week so I thought I'd use this slot to give a pep talk to those who are feeling incredibly beaten down by the industry right now (including myself).</p>
<p>We <em>will</em> win in the end. It might not feel like winning at first, but the current direction of travel is <em>not</em> inevitable.</p>
<p>We <em>do</em> have to fight back though, so don't let the bastards grind you down. We need you!</p>
]]></description>
        
      </item>
    
      <item>
        <title>Personal website redesign project post: A CLI for adding new music to the collection </title>
        <link>https://piccalil.li/projects/personal-site/10/?ref=main-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Thu, 27 Aug 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/projects/personal-site/10/?ref=main-rss-feed</guid>
        <description><![CDATA[<p>Right, we are at the end of <em>iteration one</em>.</p>
<p><img src="https://piccalil.b-cdn.net/images/projects/personal-site-core-features.jpg" alt="An Obsidian markdown file called &quot;core features and iterations.&quot; It lists a development roadmap across four iterations, including tasks like &quot;basic shell version of the site,&quot; &quot;look and feel design,&quot; &quot;AT protocol integration,&quot; and &quot;last.fm integration.&quot;" /></p>
<p>The last thing to do is to make my life a little easier. Markdown files work perfectly fine for the music collection, but they're a bit of a <em>faff</em>. Mostly because I <em>always</em> forget the front matter structure, so to fix that, I created myself a Command Line Interface (CLI) which is a series of questions, resulting in a new item being added to the collection.</p>
<p>I add <em>a lot</em> of music to my collection because I'm truly trying to get away from streaming platforms completely, so something that makes the process of keeping it up to date <em>simple</em> is very much needed.</p>
<p>Let's break down the tool I built, piece-by-piece.</p>
<pre><code>import * as p from '@clack/prompts';
import fs from 'node:fs';
import path from 'node:path';
import { Readable } from 'node:stream';
import { finished } from 'node:stream/promises';
import slugify from 'slugify';

// The root is the current working directory
const REPO_ROOT = process.cwd();

// Music collection content location
const MUSIC_COLLECTION_ROOT = path.join(
  REPO_ROOT,
  'apps',
  'web',
  'src',
  'content',
  'music-collection'
);

// For storing the album artwork
const ARTWORK_ROOT = path.join(REPO_ROOT, 'public', 'images', 'music-collection');
</code></pre>
<p>First up, I'm using <a href="https://github.com/bombshell-dev/clack">Clack</a> to do the heavy lifting for me. It's a fantastic tool that helps you to create a step-by-step CLI flow, which is exactly what I want. It'll keep all of the data up to date, so when I get to the end of the flow, I can generate the markdown file with front matter.</p>
<p>This first snippet is mostly me initialising the tools and setting some immovable constants — hence the all caps screaming naming convention.</p>
<pre><code>// Generates a nice unique filename for the artwork
function generateUniqueFilename(url) {
  const extension = path.extname(new URL(url).pathname);
  return `${Date.now()}-${Math.floor(Math.random() * 1000)}${extension}`;
}

// Downloads the remote artwork and places in ARTWORK_ROOT
async function downloadImage(url, filename) {
  const request = await fetch(url);

  const filePath = path.resolve(ARTWORK_ROOT, filename);

  // Flags: if file is already there, this will exit stage left because we're
  // in a pickle if a uniquely generated image name is duplicated
  const fileStream = fs.createWriteStream(filePath, { flags: 'wx' });

  await finished(Readable.fromWeb(request.body).pipe(fileStream));

  return filePath;
}
</code></pre>
<p>Let's look at album artwork now. What I want to be able to do is pop a URL as an answer to the artwork question so the system can download it and place a copy in my repository.</p>
<p>For all of that to work, I need to make sure each image file has a unique filename. That's where the <code>generateUniqueFilename()</code> function comes in. First, it extracts the image format from the passed <code>url</code> property. From there, I construct a new string, starting with the date and a random number. Lastly, I stitch the extension back on to the string and job done.</p>
<p></p>
<p>The <code>downloadImage()</code> function then grabs the original image and the desired filename. It grabs the image using <code>fetch</code>, renames it, moves it to the <code>ARTWORK_ROOT</code> and again, job done.</p>
<pre><code>// Takes the front matter and creates a markdown file
function createMusicItem(frontMatterTemplate, title) {
  const slug = slugify(title, {
    lower: true,
  });

  const filePath = path.join(MUSIC_COLLECTION_ROOT, `${slug}.md`);
  fs.writeFileSync(filePath, frontMatterTemplate);
  return filePath;
}
</code></pre>
<p>The function name here does a good job of explaining what's happening. Front matter data is passed in, along with the title of the album. Next, a new <code>slug</code> is generated from the <code>title</code>, then a new markdown file is created using the <code>frontMatterTemplate</code> string, which we'll cover in a moment.</p>
<pre><code>
async function main() {
  p.intro('Let’s add a new item to the music collection');

  const musicItem = await p.group(
    {
      album: () =&gt;
        p.text({
          message: 'What’s the album name?',
          placeholder: '',
          validate: (value) =&gt; {
            if (!value) return 'Album is required!';
          },
        }),
      artist: () =&gt;
        p.text({
          message: 'Who is it by?',
          placeholder: '',
          validate: (value) =&gt; {
            if (!value) return 'Album is required!';
          },
        }),
      artworkURL: () =&gt;
        p.text({
          message: 'What’s the artwork URL?',
          placeholder: '',
          validate: (value) =&gt; {
            if (!value) return 'Album is required!';
          },
        }),
      formats: () =&gt;
        p.multiselect({
          message: 'What format(s)?',
          options: [
            { text: 'Vinyl', value: 'Vinyl' },
            { text: 'CD', value: 'CD' },
            { text: 'Digital', value: 'Digital' },
          ],
        }),
      isMasterpiece: () =&gt;
        p.confirm({
          message: 'Is this a masterpiece?',
        }),
    },
    {
      onCancel: () =&gt; {
        p.cancel('Operation cancelled.');
        process.exit(0);
      },
    }
  );
  
  // For readers: the rest of the function follows shortly
}
</code></pre>
<p>A Big Block Of Code™ was unavoidable here unfortunately, so allow me to explain what's going on. The <code>p</code> variable is Clack, and the first thing I'm doing is popping a little message on the screen — "Let’s add a new item to the music collection". From there, I define a new <a href="https://bomb.sh/docs/clack/packages/prompts/#group">group of questions</a>.</p>
<p></p>
<p>For each of those questions in the group, I'll get back the response data. Because I'm defining <code>musicItem</code> as the group, I can get the data out like so: <code>musicItem.title</code>. That's very handy indeed! It's all very similar to the zod stuff we used for the Astro collection earlier in the series too.</p>
<p>With all of the questions in, it's time to respond to the data I get back.</p>
<pre><code>
// There's a 0 percent chance I'll add a top ten like this, so we're looking
// only for the masterpiece tag at this point
const tags = musicItem.isMasterpiece ? ['Masterpiece'] : [];

// Create a nice unique filename for the art image
const artworkFileName = generateUniqueFilename(musicItem.artworkURL);

// We've got all the data for markdown now, so create that front matter
const frontMatterTemplate = `---
title: '${musicItem.album}'
artist: '${musicItem.artist}'
cover: '${artworkFileName}'
formats: ['${musicItem.formats.join("', '")}']
tags: ['${tags.join("', '")}']
pubDate: ${new Date().toISOString()}
---
`;
</code></pre>
<p>As I say in the code comment, I'll never add a top 10 item like this, so my focus is day-to-day collection additions. I do however have an option to declare an album as a masterpiece, so if the clack answer is <code>true</code> for <code>isMasterpiece</code>, I assign <code>['Masterpiece']</code> as the value for <code>tags</code>.</p>
<p>Next, I use the function from earlier to determine an <code>artworkName</code> and that's all the data I need to generate a nice block of front matter data.</p>
<pre><code>// Wait until the image is ready
await downloadImage(musicItem.artworkURL, artworkFileName);

// Run the file creator
createMusicItem(frontMatterTemplate, musicItem.album);

p.outro('✅ Album added!');

return true;
</code></pre>
<p>Lastly, I use the <code>downloadImage</code> function from earlier and then the <code>createMusicItem</code> with all of that lovely data. I put a little success message on the screen and <code>return true</code>. The reason for that is I initialise the <code>main</code> function like so:</p>
<pre><code>main().catch(console.error);
</code></pre>
<p>By returning <code>true</code>, the process will just end, but by stitching <code>catch</code> to <code>main()</code>, if there are errors, I'll get a log.</p>
<h2>Rigging up the script</h2>
<p>From my command line, I want to be able to run <code>npm run music:add</code>, not <code>node packages/utils/new-music-collection-item.js</code>. That's easy enough to sort though.</p>
<p>I opened up the root <code>package.json</code> and added the following to the existing <code>"scripts"</code> property:</p>
<pre><code>"music:add": "node packages/utils/new-music-collection-item.js"
</code></pre>
<p>Job firmly done.</p>
<p></p>
<h2>Wrapping up</h2>
<p>That is iteration one <em>done</em>. Well it's been <em>done</em> for a while now, but me writing about iteration one is finally done. Like I mentioned a couple of posts ago, a very weird symptom of this project is that I've felt like I can't progress to iteration two — the actual design work — until iteration one is fully written up. I have no idea why either. Brains are weird, man.</p>
<p><em>Anyway</em>, the next post in this series will tackle exactly that: the creative process. This should all time well with the investments we've put into our own design system software too, so I'm looking forward to showing you all that. Because I do the production work, <em>then</em> write about it in this series, expect a bit of a delay now while I actually do the work.</p>
<p>For now, I've got a basic UI and a fully functional website. Sure, it's got a few rough edges, but it's a website. If you've also been in "pause mode" like I have, what I will say is get an ugly version live first — especially if you don't have a website already. It's better to have <em>something</em> than nothing.</p>
<p>Catch you in the next one.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>The Index: Issue #195</title>
        <link>https://piccalil.li/the-index/195/?ref=main-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Fri, 21 Aug 2026 10:50:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/the-index/195/?ref=main-rss-feed</guid>
        <description><![CDATA[<h2><a href="https://bulleted.app/?utm_source=the-index&amp;utm_medium=newsletter">Bulleted</a></h2>
<p>This is the stuff that's exciting about the AT protocol. Not the "new twitter" bullshit, but the endless possibilities that this technology opens up. It's using the <a href="https://atproto.com/blog/atproto-spaces-alpha">fancy new private data stuff</a> too.</p>
<h2><a href="https://www.bram.us/2026/08/20/the-future-of-css-target-multiple-classes-with-the-class-prefix-selector/?utm_source=the-index&amp;utm_medium=newsletter">The future of CSS: target multiple classes with the class prefix selector</a></h2>
<p>As Bramus says in the article, we can sort of do this already, but those substring selectors don't perform well. This new method is a very good improvement!</p>
<h2><a href="https://dbushell.com/2026/08/20/ruminations-on-notifications/?utm_source=the-index&amp;utm_medium=newsletter">Ruminations on notifications</a></h2>
<p>A good write-up on how annoying and harmful notifications are.</p>
<h2><a href="https://chrisburnell.com/html-can-do-that/?utm_source=the-index&amp;utm_medium=newsletter">HTML can do that</a></h2>
<p>So much good stuff has arrived in HTML that gives us rich functionality for free and Chris helpfully breaks that down for us.</p>
<h2><a href="https://daverupert.com/2026/08/microlighter/?utm_source=the-index&amp;utm_medium=newsletter">Introducing Microlighter</a></h2>
<p>An extremely lightweight syntax highlighter using the new <code>::highlight()</code> functionality? Yes please!</p>
<h2><a href="https://piccalil.li/blog/why-im-excited-about-text-box-trim-as-a-designer/?utm_source=the-index&amp;utm_medium=newsletter">Why I’m excited about text-box-trim as a designer</a></h2>
<p>Here's one from the <a href="https://piccalil.li/blog/">Piccalilli archives</a> that you might have missed to wrap up this issue.</p>
<hr />
<p>P.S. <a href="https://rwblickhan.org/?utm_source=the-index&amp;utm_medium=newsletter">this is a good website</a> from <a href="https://personalsit.es/?utm_source=the-index&amp;utm_medium=newsletter">personalsit.es</a>.</p>
        
        <h2>Sponsor message</h2><a href="https://piccalil.li/courses?utm_source=the-index&utm_campaign=next-level-2026"><img src="https://piccalil.b-cdn.net/images/ads/next-level-event-newsletter.png" alt="Save 20% on all courses" /></a><p><strong>Take your career to the next level by taking our premium courses and save 20%</strong>.</p>
<p>Use the code <code>NEXTLEVEL</code> at checkout to get our courses for only <strong>£199.20</strong>.</p>
<p>By taking our courses, you’re supporting independent publishing, rooted in doing right for workers in design, development and leadership.</p>
<p><a href="https://piccalil.li/courses?utm_source=the-index&utm_campaign=next-level-2026">Take your career to the next level</a></p>]]></description>
        
      </item>
    
      <item>
        <title>A look at the geolocation HTML element and how it works</title>
        <link>https://piccalil.li/blog/a-look-at-the-geolocation-html-element-and-how-it-works/?ref=main-rss-feed</link>
        <dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
        <pubDate>Thu, 20 Aug 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/a-look-at-the-geolocation-html-element-and-how-it-works/?ref=main-rss-feed</guid>
        <description><![CDATA[<p>The <code>&lt;geolocation&gt;</code> HTML element does exactly what you might think it does. It’s a dedicated element that gets the user’s location, either once or continuously. The options are set using HTML attributes instead of JavaScript, but JavaScript is still needed. However, <code>&lt;geolocation&gt;</code> requires <em>fewer lines</em> of JavaScript and also offers superior error and permission handling. In fact, the <code>&lt;geolocation&gt;</code> element started off as an all-purpose <code>&lt;permission&gt;</code> element, but is now dedicated to handling geolocation.</p>
<p>Fewer lines of code is always a good thing, but it’s the permission prompt that’s key here. When using the aging Geolocation JavaScript API, denying permission at any point can lock the user out with no way to recover unless they change the browser or operating system permissions manually. This can be a painful experience, especially for users that aren’t tech-savvy. Plus, as developers, we have to get the Permissions API involved.</p>
<p>Whereas, the <code>&lt;geolocation&gt;</code> element offers:</p>
<ul>
<li>Better error handling, and where applicable, recovery handholding</li>
<li>User-controlled permission prompting, even if the user denied permission previously</li>
<li>Auto-location if the user granted permission previously</li>
<li>A styleable granted state via the <code>:granted</code> CSS pseudo-class</li>
</ul>
<p><code>&lt;geolocation&gt;</code> is better in every way except browser support (it requires Chrome 144+), so in this article, I’ll explain how to use it alongside the older current Geolocation JavaScript API so that users get the better experience, if the new element is available.</p>
<p></p>
<h2>Requesting the user’s location using <code>&lt;geolocation&gt;</code></h2>
<p>The <code>&lt;geolocation&gt;</code> element accepts several attributes that largely correspond to the options of the <code>getCurrentPosition()</code> and <code>watchPosition()</code> methods of the aging Geolocation API.</p>
<p>Firstly, the <code>accuracymode</code> attribute accepts two values — <code>approximate</code> (which is the default value) and <code>precise</code>.</p>
<p><code>accuracymode=approximate</code> is equivalent to the <code>enableHighAccuracy: false</code> option (again, the default) from the Geolocation API, whereas <code>accuracymode=precise</code> is equivalent to <code>enableHighAccuracy: true</code>, which provides a more accurate location if the device is able to get one.</p>
<p>The <code>autolocate</code> boolean attribute attempts to get the user’s location automatically, assuming that they’ve granted the website permission previously.</p>
<p>The <code>watch</code> boolean attribute is equivalent to calling the <code>watchPosition()</code> method instead of the <code>getCurrentPosition()</code> method. <code>getCurrentPosition()</code> fetches the user’s location once, whereas <code>watchPosition()</code> tracks their location over time. Like <code>accuracymode=precise</code> and <code>enableHighAccuracy: true</code>, this drains the battery faster.</p>
<p>The <code>onlocation</code> event handler attribute can be used to execute JavaScript whenever location data or error information is passed to the browser.</p>
<p>What the <code>&lt;geolocation&gt;</code> element doesn’t offer, though, is the ability to specify the <code>timeout</code> (how long the browser should wait for a response) or <code>maximumAge</code> (how old a cached location can be). Instead the browser handles these as it sees fit, which is actually a good thing, because choosing the right values on a case-by-case basis is a complexity that we just don’t need.</p>
<p>In practice, this is how you might use <code>&lt;geolocation&gt;</code>:</p>
<pre><code>&lt;!-- Precisely autolocate and follow the user --&gt;
&lt;geolocation accuracymode="precise" autolocate watch&gt;
  &lt;!-- Render this when &lt;geolocation&gt; is unsupported --&gt;
  &lt;button id="fallbackButton"&gt;Use precise location&lt;/button&gt;
&lt;/geolocation&gt;
</code></pre>
<p>That is, of course, not including the JavaScript side of <code>&lt;geolocation&gt;</code> nor the Geolocation (JavaScript) API fallback. If you just want the full code then you’re looking for the <code>getCurrentPosition()</code> version and <code>watchPosition()</code> version. Note that geolocation doesn’t work in insecure contexts, so while the logic is sound, the CodePen demos won’t actually work. They’re raw-logic templates anyway, not fully working demonstrations.</p>
<p>Anyway, let’s get into the JavaScript of it all, starting with the JavaScript that sits on the other side of that <code>&lt;geolocation&gt;</code> markup.</p>
<h2>Handling the <code>&lt;geolocation&gt;</code> data with JavaScript</h2>
<p>First, we want to make sure that <code>&lt;geolocation&gt;</code> is supported with <code>if ("HTMLGeolocationElement" in window)</code>. If it is, then we select it with <code>const geoElement = document.querySelector("geolocation")</code>.</p>
<p>Unfortunately, we then have to dive into the most complex part of <code>&lt;geolocation&gt;</code> — it’s validity — where the <code>isValid</code> property returns <code>true</code> or <code>false</code> and the <code>invalidReason</code> property returns <code>""</code> (an empty string) or an enumerated value stating the reason.</p>
<p>These reasons mostly come down to developer oversight, so users <em>shouldn’t</em> encounter these ‘blockers’, but if they do, the <code>&lt;geolocation&gt;</code> button will be disabled. Here are the scenarios in which that can happen (note that the blockers are ordered by severity, and that <code>invalidReason</code> only returns the most severe one):</p>
<ul>
<li><code>illegal_subframe</code>: the <code>&lt;geolocation&gt;</code> element is nested within a <code>&lt;fencedframe&gt;</code> or insecure <code>&lt;iframe&gt;</code> (the latter of which is why the CodePen demos don’t work)</li>
<li><code>unsuccessful_registration</code>: the page has more than three <code>&lt;geolocation&gt;</code> elements</li>
<li><code>recently_attached</code>: the <code>&lt;geolocation&gt;</code> element has only recently been attached to the DOM (this blocker expires fairly quickly)</li>
<li><code>intersection_changed</code>: the <code>&lt;geolocation&gt;</code> element is moving</li>
<li><code>intersection_out_of_viewport_or_clipped</code> : the <code>&lt;geolocation&gt;</code> element isn’t within the viewport fully</li>
<li><code>intersection_occluded_or_distorted</code>: something is obscuring the <code>&lt;geolocation&gt;</code> element</li>
<li><code>style_invalid</code>: the <code>&lt;geolocation&gt;</code> element is styled in a way that isn’t allowed (more on this later)</li>
</ul>
<p>As you can see, these blockers are permanently avoidable as long as we catch and fix them. Let’s have a proper look at how <code>invalidReason</code> reports these blockers, though.</p>
<p>As the JavaScript comment below describes, <code>isValid</code> always returns <code>false</code> at first, while <code>invalidReason</code> returns <code>recently_attached</code>. This basically disables the <code>&lt;geolocation&gt;</code> button for a fraction of a second to prevent clickjacking. That is, unless a blocker of higher severity applies. We don’t actually need the line below, it’s there just to show you how <code>&lt;geolocation&gt;</code> will never be valid when the page loads.</p>
<pre><code>/* At first, isValid === false and invalidReason === "recently_attached"
unless isValid === false and invalidReason === "a reason of higher severity" */
console.warn(`isValid: ${geoElement.isValid}, invalidReason: ${geoElement.invalidReason}`);
</code></pre>
<p>If <code>&lt;geolocation&gt;</code> then becomes invalid for a more permanent reason, <code>isValid</code> will obviously remain <code>false</code>, so we can’t use the <code>validationstatuschange</code> event listener here. However, we can wrap the line in <code>setTimeout()</code> (as below). This will either log a new blocker into the console, or log that <code>isValid</code> is <code>true</code> (in which case <code>invalidReason</code> will be an empty string).</p>
<pre><code>/* After ~300ms, when &lt;geolocation&gt; is no longer ‘recently attached’ (to the DOM)
either isValid === true and invalidReason === "" (an empty string)
or isValid === false and invalidReason === "the reason with the highest severity" */
setTimeout(() =&gt; {
  console.log(`isValid: ${geoElement.isValid}, invalidReason: ${geoElement.invalidReason}`);
}, 300);
</code></pre>
<p>If the validation status changes later, <em>then</em> we can use the <code>validationstatuschange</code> event listener (again, as below). Note that if there are no persistent blockers, this will fire almost immediately as <code>invalidReason</code> switches from <code>recently_attached</code> to an empty string.</p>
<pre><code>/* If the validity of &lt;geolocation&gt; changes */
geoElement.addEventListener("validationstatuschange", () =&gt; {
  if (geoElement.isValid) {
    /* &lt;geolocation&gt; is valid (if there aren’t any blockers,
    it will become valid after it’s no longer ‘recently attached’ */
  } else {
    console.error(`&lt;geolocation&gt; invalid: ${geoElement.invalidReason}`);
  }
});
</code></pre>
<p>Now that you know how to debug the validation status and permanently fix any persistent blockers, let’s talk about the <em>permission</em> status.</p>
<p>We’re given a few properties and events to work with:</p>
<ul>
<li><code>initialPermissionStatus</code>: a property that returns <code>denied</code>, <code>granted</code>, or <code>prompt</code> (i.e., neither) based on the permission status when the page first loaded</li>
<li><code>permissionStatus</code>: the <em>current</em> permission status</li>
<li><code>promptaction</code>: an event that fires when the user denies or grants permission from the <code>&lt;geolocation&gt;</code> permission prompt dialog</li>
<li><code>promptdismiss</code>: fires when the user dismisses the dialog, in which case the <code>permissionStatus</code> remains unchanged</li>
</ul>
<pre><code>/* Determine the initial permission status */
if (geoElement.initialPermissionStatus === "denied") {
  /* The user previously denied permission */
} else if (geoElement.initialPermissionStatus === "granted") {
  /* The user previously granted permission */
} else if (geoElement.initialPermissionStatus === "prompt") {
  /* The user hasn’t made a choice */
}

/* If the user denies or grants permission */
geoElement.addEventListener("promptaction", () =&gt; {
  if (geoElement.permissionStatus === "denied") {
    /* The user denied permission */
  } else if (geoElement.permissionStatus === "granted") {
    /* The user granted permission */
  }
});

/* If the user dismisses the prompt */
geoElement.addEventListener("promptdismiss", () =&gt; {
  if (geoElement.permissionStatus === "denied") {
    /* The permission state remained denied */
  } else if (geoElement.permissionStatus === "granted") {
    /* The permission state remained granted */
  } else if (geoElement.permissionStatus === "prompt") {
    /* The permission state remained prompt */
  }
});
</code></pre>
<p>That being said, I honestly don’t know what we’d need any of that for. If the user previously denied permission, for example, the <code>&lt;geolocation&gt;</code> permission prompt dialog would enable the user to recover from that automatically:</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/geolocation-1.png" alt="Two confirmation boxes. One reads &quot;You previously didn't allow location for this site&quot; and the other reads &quot;To use your location on this site, give Chrome access&quot;" /></p>
<p>This is in contrast to the older Geolocation API, which is unlikely to help users recover. In this case we need to read the permission status, manage the state of the component accordingly, and provide recovery instructions so that users can grant access manually, but <code>&lt;geolocation&gt;</code> takes care of all of that.</p>
<p>What you <em>will</em> need is the new <code>location</code> event, which fires whenever the browser passes location data (<code>geoElement.position</code> in this case) or error information (<code>geoElement.error</code>) to us.</p>
<p>Then, in the event listener callback, assuming that <code>geoElement.position</code> is truthy, we can access <code>position.coords</code> and <code>position.timestamp</code>, synthesizing the location data like this:</p>
<pre><code>/* If the browser passes location data or error information */
geoElement.addEventListener("location", () =&gt; {
  /* If location data */
  if (geoElement.position) {
    /* Synthesize the data */
    const {
      latitude,
      longitude,
      altitude,
      accuracy,
      altitudeAccuracy,
      heading,
      speed
    } = geoElement.position.coords;

    const timestamp = geoElement.position.timestamp;
  } else if (geoElement.error) {
    /* If error information */
  }
});
</code></pre>
<p>However, if <code>geoElement.error</code> is truthy, we can access <code>error.message</code> (that’s for us to log into the console) and <code>error.code</code>, which is much more suitable for error handling.</p>
<p>In short, there are three possible error codes, each with an associated constant so that we don’t need to remember what each error code represents. So <code>1</code> represents <code>PERMISSION_DENIED</code>, <code>2</code> represents <code>POSITION_UNAVAILABLE</code>, and finally, <code>3</code> represents <code>TIMEOUT</code>, and then we just evaluate them like this:</p>
<pre><code>/* If the browser passes location data or error information */
geoElement.addEventListener("location", () =&gt; {
  if (geoElement.position) {
    /* If location data */
  } else if (geoElement.error) {
    /* If error information */
    console.error(`&lt;geolocation&gt; error: ${geoElement.error.message}`);

    if (geoElement.error.code === geoElement.error.PERMISSION_DENIED) {
      /* No HTTPS or server misconfiguration */
    } else if (geoElement.error.code === geoElement.error.POSITION_UNAVAILABLE) {
      /* No location source (GPS satellite or nearby Wi-Fi network/cellular tower) */
    } else if (geoElement.error.code === geoElement.error.TIMEOUT) {
      /* Location source detection or hardware took too long */
    }
  }
});
</code></pre>
<p><code>PERMISSION_DENIED</code> means that the website isn’t being served over HTTPS (the browser denied permission), or that there’s some kind of server misconfiguration (the server denied permission), but those two errors are permanently fixable and shouldn’t occur in production.</p>
<p>The only way to deny permission (as far as I’m aware) is to block location access from the browser settings, then click on the <code>&lt;geolocation&gt;</code> button, then choose to continue denying. In my opinion, that’s not likely to happen and doesn’t warrant an error message anyway. There isn’t a denial mechanism for OS-level blocks so as not to make the impression that the browser can enforce one. In short, I don’t think we need to do anything for <code>PERMISSION_DENIED</code>.</p>
<p>And to clarify, because <code>&lt;geolocation&gt;</code> is user-invoked, <code>&lt;geolocation&gt;</code> itself never sets the permission status to denied (again, as far as I know).</p>
<p><code>POSITION_UNAVAILABLE</code> means that the device can’t detect a location source (nearby Wi-Fi networks and cellular towers as well as GPS satellites) to determine the location. Using a VPN or visiting the website via an in-app browser could cause this error too, so this is the trickiest error to convey to users.</p>
<p><code>TIMEOUT</code> means that the location source detection or hardware took too long, and that users should try again.</p>
<p>How you communicate errors to users is totally up to you.</p>
<p></p>
<h2>Falling back to the Geolocation JavaScript API</h2>
<p>Ready for round two? Now we’re going to do the same thing but with the aging Geolocation API, which is supported in every browser, but kind of a headache.</p>
<p>This is where we’re at currently:</p>
<pre><code>/* If &lt;geolocation&gt; is supported */
if ("HTMLGeolocationElement" in window) {
  /* What we covered in the previous section */
} else {
  /* What we’re focusing on now (the fallback) */
}
</code></pre>
<p>Within that <code>else</code> block, which runs when <code>&lt;geolocation&gt;</code> isn’t supported, we start off by selecting the fallback button (<code>const fallbackButton = document.querySelector("#fallbackButton")</code>). If you recall, this is nested within <code>&lt;geolocation&gt;</code> so that it’s ignored when <code>&lt;geolocation&gt;</code> <em>is</em> supported.</p>
<p>After that we create a function (<code>updateState()</code>) that manages the component state and provides recovery instructions. As arguments we supply the <code>state</code> as a string (<code>‌granted</code>, <code>‌prompt</code>, or <code>‌denied</code>, corresponding with the <code>permissionStatus</code>), and optionally, <code>statusMessage</code>, which’ll be used to convey status messages to the user. I don’t want to make any assumptions about your component, so how you convey the <code>statusMessage</code> and expand upon <code>updateState()</code> is up to you.</p>
<p>If <code>state === "denied"</code>, we disable the button with <code>fallbackButton.disabled = true</code> and provide some kind of recovery instruction of which should be passed as the second argument of the function. The reason why we disable the button is that <em>this</em> Geolocation API isn’t user-invoked, so to protect the user from spam requests, the browser can suppress requests and send the API straight to jail without passing go, triggering <code>PERMISSION_DENIED</code>. Additionally, if we make the API user-invoked (as we have), users can spam the button themselves and basically shadowblock themselves, but disabling the button fixes that.</p>
<p>If <code>state === "granted"</code> or <code>state === "prompt"</code>, we can enable the button (<code>fallbackButton.disabled = false</code>).</p>
<p>Now is a good time to mention that the earlier JavaScript code for the <code>&lt;geolocation&gt;</code> element works regardless of whether the element has the <code>watch</code> attribute or not. However, when using this older Geolocation API, there are two additional things that we need to take care of when trying to keep track of the user’s location continuously. The first thing is the watcher ID, which is returned by the <code>watchPosition()</code> method. Knowing this ID enables us to clear the watcher before registering a new one, which is a must-do for performance reasons.</p>
<p>So we <code>let watcherID = null</code> for now, and then we create the function that attempts to get the location (<code>getLocation()</code>), and the first thing that we do within that function, assuming that <code>watcherID !== null</code> (meaning that it’s been set before), is clear the watcher using <code>navigator.geolocation.clearWatch(watcherID)</code> and make <code>watcherID = null</code> again:</p>
<pre><code>/* A function for getting the location */
const getLocation = () =&gt; {
  /* If a watcher has already been registered */
  if (watcherID !== null) {
    /* Unregister it */
    navigator.geolocation.clearWatch(watcherID);
    watcherID = null;
  }
}
</code></pre>
<p>Then we disable the button using <code>fallbackButton.disabled = true</code> to, again, prevent spam clicks. If you want to bake some kind of loading indicator in, feel free to, but <code>&lt;geolocation&gt;</code> doesn’t.</p>
<p>After that we call <code>navigator.geolocation.watchPosition()</code>, setting <code>watcherID</code> to the returned watcher ID. This method has three parameters — success, error, and options.</p>
<pre><code>watcherID = navigator.geolocation.watchPosition(
  (position) =&gt; {
    /* Success */
  },
  (error) =&gt; {
    /* Error */
  },
  {
    /* Options */
  }
);
</code></pre>
<p>For the success callback function we synthesize the location data similarly to last time, then call <code>updateState("granted")</code>.</p>
<p>For the error callback function (optional but highly recommended) we clear the watcher and, again, make <code>watcherID = null</code>, but otherwise run the same error handling logic that <code>&lt;geolocation&gt;</code> runs. However, there are more circumstances in which the errors can occur.</p>
<p>For example, because <code>watchPosition()</code> and <code>getCurrentPosition()</code> aren’t necessarily user-invoked, there are more scenarios in which users are able to deny access, triggering the <code>PERMISSION_DENIED</code> error. Accordingly, we should call <code>updateState("denied", "Permission denied (try this or that)")</code>, offering a useful status message and clear recovery instructions.</p>
<p>Similarly, <code>POSITION_UNAVAILABLE</code> can also be triggered by an OS-level block, since not all web browsers catch this during the permission prompt dialog. <code>updateState("prompt", "Position unavailable (try this or that)")</code> is what we’re looking for this time.</p>
<p>Finally, another scenario that doesn’t occur with <code>&lt;geolocation&gt;</code> but does with <em>this</em> Geolocation API, is that if the browser prompts the user to grant permission at the OS-level, but then the user cancels their request, that can trigger the <code>TIMEOUT</code> error. Either way, call <code>updateState("prompt", "Request timed out (try this or that)”)</code>, once again tweaking it to your liking.</p>
<p>As you can see, the error reporting isn’t the best. Sometimes the error isn’t identified correctly, and even when it is, the error can occur for various reasons, which makes it difficult for us to convey a useful status message and clear recovery instructions. <code>&lt;geolocation&gt;</code> handles this better — the errors are identified correctly, and the nature of <code>&lt;geolocation&gt;</code> ensures that certain errors never occur to begin with. But why the error categories? Why not tell us exactly what went wrong?</p>
<p>Well, the reason is to make fingerprinting more difficult, and while we can totally put in the extra work to pinpoint the exact problem, simply stating what happened and what the user should do next is perfectly fine. Of course, <code>error.message</code> tells us more than <code>error.code</code> does, but the messages can be a bit vague and differ in every web browser, so we can’t read them or even output them reliably.</p>
<p>Anyway, the optional third parameter expects an object where we can set:</p>
<ul>
<li><code>enableHighAccuracy</code>: <code>true</code> or <code>false</code></li>
<li><code>timeout</code>: <code>20000</code> (20 seconds) is reasonable if <code>enableHighAccuracy: true</code>, <code>3000</code> - <code>5000</code> otherwise</li>
<li><code>maximumAge</code>: <code>0</code> for turn-by-turn navigation, <code>5000</code> - <code>10000</code> (5-10 seconds) for live tracking, <code>300000</code> - <code>600000</code> (5-10 minutes) for frequent updates (e.g., weather)</li>
</ul>
<p>I’d rather that the web browser choose the <code>timeout</code> and <code>maximumAge</code> for us, especially considering the impact that they have on the <code>TIMEOUT</code> error and device battery, but of course, that’s exactly what <code>&lt;geolocation&gt;</code> does.</p>
<pre><code>/* Attempt to get the location and store the returned ID */
watcherID = navigator.geolocation.watchPosition(
  /* If location data is passed (like before) */
  (position) =&gt; {
    /* Synthesize the data (again, like before) */
    const {
      latitude,
      longitude,
      altitude,
      accuracy,
      altitudeAccuracy,
      heading,
      speed
    } = position.coords;

    const timestamp = position.timestamp;

    /* And update the state */
    updateState("granted");
  },

  /* If error information is passed (yep, like before) */
  (error) =&gt; {
    console.error(`Geolocation error: ${error.message}`);

    /* Again, unregister the watcher (if necessary) */
    if (watcherID !== null) {
      navigator.geolocation.clearWatch(watcherID);
      watcherID = null;
    }

    if (error.code === error.PERMISSION_DENIED) {
      /* No HTTPS, server misconfiguration, or the user denied access */
      updateState("denied", "Permission denied (try this or that)");
    } else if (error.code === error.POSITION_UNAVAILABLE) {
      /* No location source (GPS satellite or nearby Wi-Fi network/cellular tower) or OS-level access */
      updateState("prompt", "Position unavailable (try this or that)");
    } else if (error.code === error.TIMEOUT) {
      /* Location source detection or hardware took too long, or the user canceled their request */
      updateState("prompt", "Request timed out (try this or that)");
    }
  },
  {
    enableHighAccuracy: true,
    timeout: 20000 /* 20 seconds because enableHighAccuracy: true */,
    maximumAge: 0 /* 0 seconds because we’re demanding high accuracy */
  }
);
</code></pre>
<p>After that we need to query the permission status using the Permissions API (<code>navigator.permissions.query({ name: "geolocation" }).then((permissionStatus) =&gt; { /* ... */ })</code>), and then execute all of the aforementioned logic based on that.</p>
<p>If <code>permissionStatus.state === "granted"</code>, we call <code>getLocation()</code>, which is essentially what the <code>autolocate</code> attribute does. If you don’t want autolocation, simply delete this part. If it’s anything else, we basically determine the initial state by calling <code>updateState(permissionStatus.state)</code>.</p>
<pre><code>/* Query the permission status */
navigator.permissions.query({ name: "geolocation" }).then((permissionStatus) =&gt; {
  /* Equivalent to the autolocate attribute */
  if (permissionStatus.state === "granted") {
    getLocation();
  } else {
    /* React accordingly */
    updateState(permissionStatus.state);
  }
});
</code></pre>
<p>Finally, make the button respond to clicks:</p>
<pre><code>/* If the user clicks the fallback button */
fallbackButton.addEventListener("click", () =&gt; {
  getLocation();
});
</code></pre>
<p>If we only want to get the user’s location once (rather than watch it continuously), these are the modifications that we need to make:</p>
<ul>
<li>Remove everything related to the <code>watcherID</code></li>
<li>Swap <code>watchPosition()</code> for <code>getCurrentPosition()</code></li>
<li>Provide different settings for the options parameter</li>
</ul>
<p>Note that the <code>change</code> event, which could help us update the state whenever the user changes their browser-level permission status, doesn’t fire in Safari. Besides, the standard browser behavior is to ask users to refresh the page, so let’s stick with that.</p>
<p>One more thing — I wanted the button to say “Refresh precise location” and “Getting precise location…” at certain points, but <code>&lt;geolocation&gt;</code> doesn’t do this, so I didn’t either. That’s up to you, though.</p>
<p></p>
<h2>Styling the <code>&lt;geolocation&gt;</code> element</h2>
<p><code>&lt;geolocation&gt;</code> has a <code>:granted</code> pseudo-class. If we throw <code>:not(:granted)</code> into the mix, we can target the prompt/denied states too:</p>
<pre><code>geolocation {
  &amp;:granted {
    /* Permission granted */
  }

  &amp;:not(:granted) {
    /* Permission not granted */
  }
}
</code></pre>
<p>With the Geolocation API, we can toggle classes as needed to achieve the same effect, but honestly, I’ve never felt compeled to style such a button in this way. Personally, I’d like to be able to change the icon and text (maybe this is something that you can bake into the <code>updateState()</code> function), but this is one of the things that the <code>&lt;geolocation&gt;</code> element outright forbids.</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/geolocation-2.png" alt="A button element labelled &quot;use precise location&quot;" /></p>
<p>Before we dive into all <em>that</em>, here’s a rundown of <code>&lt;geolocation&gt;</code> rules stated by Google’s explainer and Mozilla’s explainer:</p>
<ul>
<li>There must be sufficient color contrast</li>
<li>The alpha channel must resolve to <code>1</code> (so no transparency)</li>
<li>The minimum and maximum width, height, and font size must be respected</li>
<li>Negative margins and outline offsets aren’t allowed either</li>
<li>Distortion effects — including linear gradients — are banned</li>
</ul>
<p>It’s also worth noting that some of these rules function like guardrails (for example, you physically can’t style the height above 50px), whereas others cause <code>invalidReason</code> to return <code>style_invalid</code> and the <code>&lt;geolocation&gt;</code> button to be disabled.</p>
<p>Some of the rules seem to have changed (or are currently bugged). <code>&lt;geolocation&gt;</code> doesn’t respect <code>prefers-color-scheme</code> either, but it’s a developing feature, so I’m not going to comment on all of that too much. I’ll just state what I do and don’t like:</p>
<ul>
<li>I don’t like that the button becomes disabled because of a style (guardrails are better, but I don’t really like either)</li>
<li>I don’t like the forced icon and text, and I’m also not too happy about not being able to use gradients or <code>corner-shape</code></li>
<li>I can make peace with everything else because I’d normally design within those guardrails anyway</li>
<li>The text being localized into different languages is very cool, but it’s a first for web standards and I don’t currently enjoy that it’s a thing for only these permission buttons</li>
</ul>
<p>While I obviously support preventing web authors from tricking users into giving away their location, I think it’d be better if <code>&lt;geolocation&gt;</code> were fully styleable and the permission prompt dialog be ultra clear about what the user is about to commit to.</p>
<p>Even though I can style the background, color, border, border radius, and box shadow how I’d want them (as in the image below, which is most of what I’d want), this feels like a step back towards unstyleable controls. Having said that, this approach means well (<em>extremely</em> well), so I can’t wait to see where this goes (and frankly, to be able to throw the aging Geolocation API in the bin).</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/geolocation-3.png" alt="A purple button element with white text, labelled &quot;use precise location&quot;" /></p>
<h2>What now? What else?</h2>
<p>When all web browsers support <code>&lt;geolocation&gt;</code>, the user experience and developer experience will improve dramatically.</p>
<p>But that’s not all!</p>
<p>What was once the <code>&lt;permission&gt;</code> element is now the <code>&lt;geolocation&gt;</code> element, <code>&lt;install&gt;</code> element (which facilitates the installation of Progressive Web Apps), <code>&lt;usermedia&gt;</code> element (which facilitates access to the user’s camera and/or microphone), and <code>&lt;camera&gt;</code> and <code>&lt;microphone&gt;</code> elements (which facilitates access to them individually).</p>
<p>All of these are being trialed in Chrome as part of a larger initiative to improve the process and experience of requesting permission, which is bloody great, in my opinion.</p>
        
        ]]></description>
        
      </item>
    
    </channel>
  </rss>
