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():

    JSON.stringify(specificities[0]);
    
    // {
    //    "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"
(0,1,3)
(1,0,0)

~

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 …

~

WASD vs. AZERTY

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');
console.log(forwardKey);

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:

The Hidden Fundamentals of CSS

In this CSS Café talk, Josh W. Comeau digs into several Layout Modes and a few common misunderstandings about CSS (such as Stacking Contexts)

Also see the Understanding Layout Algorithms post Josh recently published.

Those HTML Attributes You Never Use

Over at Smashing Magazine, Louis Lazaris covers a few of the lesser known HTML attributes:

  • The enterkeyhint Attribute For Virtual Keyboards
  • The title Attribute On Stylesheets
  • The cite Attribute For The <blockquote> And <q> Elements
  • Attributes For Custom Ordered Lists
  • The download Attribute For The <a> Element
  • The decoding Attribute For The <img> Element
  • The loading Attribute For The <iframe> Element
  • The form Attribute For Form Fields
  • The cite And datetime Attributes For Deletions/Insertions
  • The label Attribute For The <optgroup> Element
  • The imagesizes And imagesrcset Attributes For Preloading Responsive Images

Those HTML Attributes You Never Use →

~

Create animated GIFs of your code with Recoded

Like Carbon or Ray, but the output is an animated GIF.

Beware though: the generated GIFs are HUUUUUGE. The embedded animation above originally was a 14MB GIF. Converted to an MP4, it’s only 163kB.

Recoded →
Recoded Source (GitHub) →

CSS color-contrast() – Target Contrast Ratio Demo

Nice demo by Daniel Yuschick, showing how color-contrast() does its thing. Using the controls you can change the target contrast using a keyword or a custom value.

Good use of Custom Properties there as well!

👨‍🔬 To check this demo you’ll need Safari Technology Preview 122+ with the CSS color-contrast() Experimental Feature enabled.