30 Days of PWA

Microsoft recently did a “30 Days of PWA” blog series. The posts got grouped per week, each week with a shared topic:

  • Core Concepts: Learn the fundamental concepts and components of a PWA.
  • Advanced Capabilities: Explore web capabilities APIs, status, and examples of use.
  • Developer Tools: Learn about key authoring, debugging, auditing, testing and packaging tools for PWA.
  • Platforms & Practices: Learn good practices and platform-specific support for PWA.

30 Days of PWA →

Say Hello to <selectmenu>, a Fully Style-able <select> Element

Over at CSS-Tricks, Patrick Brosset dug into <selectmenu>, the customizable <select> we always wanted.

The new experimental <selectmenu> control offers a lot of flexibility when it comes to styling and even extending a traditional <select>. And it does this in all the right ways, because it’s built into the browser where accessibility and viewport-aware positioning are handled for you.

To test it out, you’ll need Chromium with the “Experimental Web Platform features” flag enabled.

Say Hello to <selectmenu>, a Fully Style-able <select> Element →
<selectmenu> Demos →

What’s new in Node.js core?

Simon Plenderleith gives a roundup of the new stuff that landed in Node since September 2021.

  • Deep clone values (using structuredClone)
  • Auto cancel async operations
  • Fetch API
  • Import JSON modules
  • Abort API additions
  • readline/promises API
  • Corepack

I’m especially excited about native fetch() support making it into Node 17. Note that you must enabled it using the --experimental-fetch flag, though.

Web Streams support — available with experimental support in 17.8 — also looks pretty sweet.

What’s new in Node.js core? (March 2022 Edition) →

Valet 3.0: Multiple/Parallel PHP Version Support

For my local PHP Development needs I use Laravel Valet. It’s easy to set up, provides HTTPS, and just works. The only downside of using it, is the fact that the selected PHP version is system-wide: switching PHP versions — using valet use php@7.4 for example — affects all sites. With the 3.0 release of Valet this is no longer the case, as it offers per-site PHP version isolation.

To isolate a site, use new isolate command:

cd path/to/app
# Isolate the current project
valet isolate php@7.4

# Isolate a site by name
valet isolate php@8.0 --site=laravel9x

If a certain PHP version is missing, Valet will install it automagically (using Homebrew)

⬆️ If you’re already running Valet, you can’t update to 3.x by invoking good ’ole composer global update. Instead, you must re-require Valet again, with the required version set to ^3.0

composer global require "laravel/valet:^3.0"


Note that when isolating sites using valet isolate, it only affects the PHP version that’s used by Nginx. On the CLI your php will always use the one you’re globally using. For dependencies, you could lock down your PHP version via composer.json, but that won’t prevent you from possibly incompatible PHP syntax.

To cater for both issue, version 3.1 (which got released just few days after 3.0) comes with three new commands that allow you to run PHP and Composer commands using the isolated site’s PHP version on the CLI.

  1. valet php ... will proxy PHP Commands with isolated PHP version
  2. valet composer ... will proxy Composer Commands with isolated PHP version
  3. valet which-php outputs the PHP executable path for a site. For isolated site it would output the isolated PHP executable path. But non-isolated site will just output the linked default PHP path. The other two commands are dependent on this one to find the PHP executable.

To not have to type valet php every time, this tip by Jacob Delcroix is pure gold: make php an alias for valet php


If you’re rocking the aforementioned PHP Monitor, you’ll be glad to read that version 5.2 has isolate support built-in.

To update PHPMon, do so using Homebrew:

brew upgrade phpmon


🔥 Like what you see? Want to stay in the loop? Here's how:

What’s new in ECMAScript 2022

Pawel Grzybek gives us a good overview of the ES2022 features

The ECMAScript 2022 Language Specification candidate is now available. Even though the final version will be approved in June, the list of new features coming to the language this year is already defined. Let’s look at what’s coming to ECMAScript specification in 2022.

What’s new in ECMAScript 2022 →

What’s new in @bramus/specificity v2

Back in February I created @bramus/specificity, an NPM package to calculate the Specificity of CSS Selectors.

As that version was more of a thought experiment/POC, there was a lot of room for improvement. Yesterday, after 11 betas, version 2.0.0 of @bramus/specificity was released. Let’s take a look …


Quick Example

To give you an idea of what it’s all about, here’s a quick demo:

See the Pen
Calculate Specificity
by Bramus (@bramus)
on CodePen.

The input accepts a string that contains one or more CSS Selector(s) — a Selector List. @bramus/specificity will calculate the specificity for each Selector that it detects (powered by csstree).


Notable Changes since v1

✨ Introduce and use a Specificity class

Where v1 exposed a standalone calculate function which returned simple Objects, v2 now exposes a Specificity class which represents a calculated specificity. The calculate function is now a static method of that class.

import Specificity from '@bramus/specificity';

const selectors = 'header:where(#top) nav li:nth-child(2n), #doormat';
const specificities = Specificity.calculate(selectors);

specificities.map((s) => s.toString());
// ~> ["(0,1,3)", "(1,0,0)"]

The class also has many instance methods for you to use.

  • Read the specificity value using one of its accessors:

    const s = specificities[0];
    s.value; // { a: 0, b: 1, c: 3 }
    s.a; // 0
    s.b; // 1
    s.c; // 3
  • Convert the calculated value to various formats using one of the toXXX() instance methods:

    s.toString(); // "(0,1,3)"
    s.toArray(); // [0, 1, 3]
    s.toObject(); // { a: 0, b: 1, c: 3 }
  • Extract the matched selector string:

    s.selectorString(); // "header:where(#top) nav li:nth-child(2n)"
  • Use one of its instance comparison methods to compare it to another Specificity instance:

    s.isEqualTo(specificities[1]); // false
    s.isGreaterThan(specificities[1]); // false
    s.isLessThan(specificities[1]); // true
  • Don’t worry about using JSON.stringify():

    // {
    //    "selector": 'header:where(#top) nav li:nth-child(2n)',
    //    "asObject": { "a": 0, "b": 1, "c": 3 },
    //    "asArray": [0, 1, 3],
    //    "asString": "(0,1,3)",
    // }


👨‍👩‍👧‍👦 Support Selector Lists

v1 only accepted single selectors to calculate. v2 accepts Selector Lists. Because of that, Specificity.calculate(…) will always return an array, with each entry being a Specificity instance — one per found selector.

If you know you’re passing only a single Selector into Specificity.calculate(), you can use JavaScript’s built-in destructuring to keep your variable names clean.

const [s] = Specificity.calculate('header:where(#top) nav li:nth-child(2n)');
s.value; // { a: 0, b: 1, c: 3 }


🗜 Reduced Bundle Size

By only importing the selector-parser from css-tree, the bundle size was greatly reduced. Thanks to a code contribution to css-tree, some of the code in @bramus/specificity could also be removed.


🔀 Utility functions for comparing, sorting, and filtering

On the Specificity class, several static methods are exposed for comparing, sorting, and filtering.

  • Comparing:

    • Specificity.compare(s1, s2): Compares s1 to s2. Returns a value that can be:
      • > 0 = Sort s2 before s1 (i.e. s1 is more specific than s2)
      • 0 = Keep original order of s1 and s2 (i.e. s1 and s2 are equally specific)
      • < 0 = Sort s1 before s2 (i.e. s1 is less specific than s2)
    • Specificity.equals(s1, s2): Returns true if s1 and s2 have the same specificity. If not, false is returned.
    • Specificity.greaterThan(s1, s2): Returns true if s1 has a higher specificity than s2. If not, false is returned.
    • Specificity.lessThan(s1, s2): Returns true if s1 has a lower specificity than s2. If not, false is returned.
  • Sorting:

    • Specificity.sortAsc(s1, s2, …, sN): Sorts the given specificities in ascending order (low specificity to high specificity)
    • Specificity.sortDesc(s1, s2, …, sN): Sorts the given specificities in descending order (high specificity to low specificity)
  • Filtering:

    • Specificity.min(s1, s2, …, sN): Filters out the value with the lowest specificity
    • Specificity.max(s1, s2, …, sN): Filters out the value with the highest specificity

A specificity passed into any of these utility functions can be any of:

  • An instance of the included Specificity class
  • A simple Object such as {'a': 1, 'b': 0, 'c': 2}

These helper functions can also be imported as standalone functions, thanks to the use of SubPath Exports.

import { compare, equals, greaterThan, lessThan } from '@bramus/specificity/compare';
import { min, max } from '@bramus/specificity/filter';
import { sortAsc, sortDesc } from '@bramus/specificity/sort';


🤖 Type Definitions

Although @bramus/specificity is written in Vanilla JavaScript, it does include Type Definitions which are exposed via its package.json.


💻 CLI script

@bramus/specificity exposes a binary named specificity to calculate the specificity of a given selector list on the CLI. For each selector that it finds, it’ll print out the calculated Specificity as a string on a new line.

$ specificity "header:where(#top) nav li:nth-child(2n), #doormat"


Getting @bramus/specificity

@bramus/specificity’s source is available on GitHub and is distributed through NPM:

npm i @bramus/specificity

If you encounter any issues, you can leave them in the Issue Tracker.


Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small donation would surely put a smile on my face. Thanks!

Sponsor on GitHub

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Laravel Zero – Micro-framework for console applications

Laravel Zero is a lightweight and modular micro-framework for developing fast and powerful console applications. Built on top of the Laravel components.

Think of it as a stripped down Laravel, without the public folder.

Recently used it to create a Command that runs in Docker Container upon boot and then exits. Instead of using Laravel’s built-in Task Scheduling, we manage the scheduling through a Kubernetes CronJob,. That way we can use Laravel, but don’t need to keep the container up to handle 24/7. Keeping it up would be useless, as the job only runs once per day and there’s no website to serve.

Laravel Zero →
Laravel Zero Source (GitHub) →

Optimising Largest Contentful Paint

Harry Roberts takes a look at some more technical and non-obvious aspects of optimising Largest Contentful Paint:

Largest Contentful Paint (LCP) is my favourite Core Web Vital. It’s the easiest to optimise, and it’s the only one of the three that works the exact same in the lab as it does in the field (don’t even get me started on this…). Yet, surprisingly, it’s the least optimised CWV in CrUX—at the time of writing, only half of origins in the dataset had a Good LCP

Optimising Largest Contentful Paint →

WASD Controls on the Web: Don’t use KeyboardEvent.key but use KeyboardEvent.code

A while ago I was talking to the author of the aforementioned Jamir that the WASD controls they implemented weren’t that practical for me due to the AZERTY keyboard layout which I use.

Thankfully, the fix is pretty small and easy to roll out …



To control the position of characters in games, the gaming industry has settled on using the WASD keys to do so. These keys are laid out similar fashion to the arrow keys, can be controlled with the left hand, and keep your right hand free to control the mouse to turn and look around.

On QWERTY, using WASD works fine, as these keys are placed in line with a rather natural position of your left hand’s fingers:

But on AZERTY — the default keyboard layout in Belgium — it would look like this:

To not have to flex our fingers in some very weird ways, us Belgians have adopted the use of ZQSD, which maps to the same physical keys as WASD on QWERTY.

In most games this difference in keyboard layout is automatically handled, and pressing ZQSD works fine for me and my fellow AZERTY users. In Jamir, however, that’s not the case, and we’re required to press the key that has the label W in order to move forward.


The problem with KeyboardEvent.key

The culprit is that Jamir’s code is basing itself on KeyboardEvent.key to determine which key was pressed. Since KeyboardEvent.key represents the label that appears on the key, that will map to different physical positions per keyboard layout. You can see this in the images above: the W key has moved between QWERTY and AZERTY

Pressing the key with the label w on your keyboard — or any keyboard — will always show a value of w for KeyboardEvent.key, independent that key’s physical location on the keyboard.

To play nice with AZERTY, Jamir would have to maintain separate list of KeyboardEvent.key values to respond to ZQSD. Same for DVORAK, Maltron, QWERTZ, Colemak, JCUKEN, or any other keyboard layout …

  • User has QWERTY?
    → Listen for KeyboardEvent.key W to move forward.
  • User has AZERTY?
    → Listen for KeyboardEvent.key Z to move forward.
  • User has …?
    → Listen for KeyboardEvent.key ??? to move forward.

Seems like a tedious job to do and maintain 🫤


Keyboard Layout-Independent WASD

Thankfully there’s an easier solution than keeping track of multiple keyboard mappings: instead of evaluating KeyboardEvent.key, evaluate KeyboardEvent.code, as that represents the physical key that’s being pressed.

See the Pen
Keyboard Event Debugger
by Bramus (@bramus)
on CodePen.

Testing with the CodePen above, using several keyboard layouts, you can see that the value for KeyboardEvent.code remains the same when pressing the same physical key:

  • QWERTY keyboard, pressing W:

    {"keyCode":87, "key":"w", "which":87, "code":"KeyW"}
  • AZERTY keyboard, pressing Z:

    {"keyCode":90, "key":"z", "which":90, "code":"KeyW"}

If you have your code check the value of KeyboardEvent.code, the WASD controls will automatically adapt themselves to become ZQSD on AZERTY, ,AOE on DVORAK, etc. — yay! 🎉

  • User has QWERTY?
    → Listen for KeyboardEvent.code KeyW to move forward.
  • User has AZERTY?
    → Listen for KeyboardEvent.code KeyW to move forward.
  • User has any keyboard layout?
    → Listen for KeyboardEvent.code KeyW to move forward.

☝️ The thing that has to click here is that there’s a difference between the physical position of a key (exposed through KeyboardEvent.code) and the label on the key (exposed through KeyboardEvent.key). E.g. Z on AZERTY and W on QWERTY are the same physical key ("KeyW"), but they have a different label (z or w, depending on the layout).


Press W/Z/, to move forward

With KeyboardEvent.code in place, our controls will now play nice with all keyboard layouts, but it can now become a bit difficult to tell the user which key they need to press:

  • Using QWERTY?
    → Press W to move forward.
  • Using AZERTY?
    → Press Z to move forward.
  • Using DVORAK?
    → Press , to move forward.

👨‍🏫 Again, note that these all map to the same physical key ("KeyW") being pressed.

Thankfully there’s the experimental Keyboard.getLayoutMap() that can help us out here. Using it, we can translate a KeyboardEvent.code to it’s accompanying label:

const keyboardLayoutMap = await navigator.keyboard.getLayoutMap();
const forwardKey = keyboardLayoutMap.get('KeyW');

The code above will return w on QWERTY, z on AZERTY.

👨‍🔬 Keyboard.getLayoutMap() is only supported in Chromium based browsers at the time of writing.


When not to use KeyboardEvent.code

Note that it’s not required everywhere to use KeyboardEvent.code. If you have an undo feature in your app that’s triggered by pressing the z key, then you do need to listen to KeyboardEvent.key.

It’s only in situations where the physical keys matter — such as WASD controls in games, a piano, etc. — that you need to resort to KeyboardEvent.code.


Want more?

If you want more: in the latest episode of HTTP 203, Jake and Ada tap into keyboard events and this use-case. Besides covering non-QWERTY keyboard layouts, they also cover virtual keyboards, compositions, swipey keyboards, etc.


🔥 Like what you see? Want to stay in the loop? Here's how: