WebHID Demo: Elgato Stream Deck Daft Punk Soundboard

Sparked by Pete LePage’s work on talking to a Elgato Stream Deck device from within the browser, I wanted to play with WebHID myself. First thing that came to my mind was to create a DrumPad.

What first started out as a simple/classic DrumPad …

… soon led to creating a Soundboard which uses samples from Daft Punk’s “Harder, Better, Faster, Stronger”.


Screenshot of the Soundboard I built.

Before linking to the final version of the Daft Punk Soundboard (which has evolved quite a bit when compared to the screenshot above), let’s take a look at how it works.

~

Building the Drumpad/Soundboard and responding to clicks

All audio samples are defined as a small object on an array, and consist of three properties:

  1. A label
  2. A link to an audio fragment
  3. A Keyboard keyCode to respond to

ℹ️ In the final version I added some extra features such as the ability to use an image instead of a label and to customize the action when the button is being pressed, but these are not the focus here.

Each fragment is rendered as a <button> element and a (non-visible) <audio> element. The <audio> element its id is set to the keyCode.

<button data-keycode="${keyCode}">
    <span>${label}</span>
    <audio id="${keyCode}" src=${url} preload="auto"></audio>
</button>

Upon pressing a button, its linked <audio> element (fetched using the button’s data-keycode attribute value, instead of relying on DOM traversal) is selected and a play action is triggered on the fragment.

const playSound = (keyCode) => {
	const $el = document.getElementById(keyCode);
	
	if (!$el) return;

	$el.currentTime = 0;
	$el.play();
}

~

Responding to Keyboard Key Presses

To capture key presses a listener on the keydown event of the document is added. Using the pressed key’s code the correct button is selected and a click on it is triggered.

document.addEventListener('keydown', (e) => {
	const $button = document.querySelector(`button[data-keycode="${e.code}"]`);
	if ($button) $button.click();
});

~

Attaching the Stream Deck

☝️ Do note that connecting a Stream Deck is entirely optional: using a Stream Deck is considered to be an enhancement.

The Stream Deck code itself was borrowed from Pete’s Google Meet Stream Deck Chrome Plug-in, and launched using similar logic. If a Stream Deck device is found and connected, it is attached to the DrumPad instance.

const go = async () => {
	const drumPad = new DrumPad(config, document.querySelector("#app"));
	await drumPad.init();

	if (navigator.hid) {
		const streamDeck = new StreamDeck();

		// Connect to previously connected device
		await streamDeck.connect();

		// A previously connected device was found
		if (streamDeck.isConnected) {
			drumPad.attachStreamDeck(streamDeck);
		}

		// No Previously connected device was found
		else {
			// Add button to connect new device
			const elem = document.createElement("button");
			elem.innerText = "Connect Stream Deck";
			elem.addEventListener("click", async () => {
				elem.remove();
				await streamDeck.connect(true);
				drumPad.attachStreamDeck(streamDeck);
			});
			document.body.appendChild(elem);
		}
	}
}
go();

💡 As not all browsers support top-level await, we wrap the whole logic in a async function

~

Responding to Stream Deck button presses

To also respond to button presses on the Stream Deck, a map that maps a Stream Deck button ID (0, 1, 2, …) to a certain keyCode is built.

const buttonIdToKeyCodeMap = {
  0: "KeyQ",
  1: "KeyW",
  2: "KeyE",
  …
}

Upon pressing a Stream Deck button it will — using the buttonIdToKeyCodeMap — fetch the corresponding HTML button and trigger a click on it, similar to how the keyboard key presses work.

This is set up in the call to drumPad.attachStreamDeck(streamDeck); (see above) and looks like this:

streamDeck.addEventListener('keydown', (e) => {
	const keyCode = buttonIdToKeyCodeMap[e.detail.buttonId] ?? '';
	const $button = document.querySelector(`button[data-keycode="${keyCode}"]`);
	if ($button) $button.click();
});

In that same attachStreamDeck method the buttons on the Stream Deck are also drawn.

~

Stretching it a bit more …

The switch to the Daft Punk board didn’t sit 100% well me with me though: there are 16 samples to use, but the Stream Deck “only” has 15 buttons available …

But then it hit me: what if I paginated the samples, and allowed you to switch between two sets of 8 samples each? In that idea the 1st row would be filled with buttons to switch between different sample sets, and the 2nd+3rd row would respond to that.

With that refactor being worked on, I also took the time to update the UI to closely reflect the layout of the Stream Deck device.

In the end, it ended up like this:

Here’s a video of it, so see how it works and behaves, including with a connected Stream Deck:

~

During lunch today I polished the code a bit further and pushed everything online. The Source Code can be found in GitHub, and the app is deployed on Netlify.

Elgato Stream Deck Daft Punk Soundboard Demo →
Elgato Stream Deck Daft Punk Soundboard Source Code →

👨‍🔬 The demo website is registered for the WebHID Origin Trial, and therefore WebHID should be enabled by default. If you however don’t see a connect button, go to chrome://flags/ and manually enable ”Experimental Web Platform Features”.

~

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

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

☕️ Buy me a Coffee (€3)

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

What’s new in ES2021

With the January meeting of TC39 behind us, the Stage-4 features that will make it into ES2021 have been selected.

💁‍♂️ Stage-4?

The Technical Committee which is concerned with the standardization of ECMAScript (e.g. TC39) has a 5 stage process in place, ranging from stage-0 to stage-4, by which it develops a new language feature.

Stage-4 is the Finished Stage and indicates that the proposal is ready to become part of the ECMAScript Specification.

The following features will be part of ES2021, which will be formalised mid-2021:

Apart from the linked-to posts above, Pawel Grzybek has done a writeup on all features. Additionally you can check all posts tagged es2021 on the V8 blog.

☝️ Note that you can use (most of) these features today already, as browsers start rolling them out (unflagged) when they hit Stage-3. So no, you don’t have to wait until mid-2021 to start using them.

Animating a CSS Gradient Border

Recently, Stephanie Eckles sent out a call to revive the use of CSS border-image. Not to use it with images — which requires a pretty nasty syntax — but to create Gradient Borders in CSS.

Curious to see what we can do with them, I whipped up a few demos using CodePen.

~

The most simple usage is to set some type of CSS gradient as the border-image:

div {
    border: 3em solid;
    border-image: linear-gradient(to right, green, yellow) 1;
}

See the Pen CSS Gradient Border by Bramus (@bramus) on CodePen.

Then I wondered if I could animate the border, so that it would rotate along the edge.

~

To animate the border gradient you’ll need to add an angle in there — using a Custom Property — which you animate using some @keyframes.

div {
  --angle: 0deg;
  /* … */
  border-image: linear-gradient(var(--angle), green, yellow) 1;
  animation: 10s rotate linear infinite;
}

@keyframes rotate {
  to {
    --angle: 360deg;
  }
}

By using a Custom Property we can have the browser properly automatically interpolate this value from 0deg to 360deg instead of needing to add individual keyframes for every 1 degree increase. For this to work we have to register the Custom Property using the amazing @property at-rule

@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

⚠️ As @property is only supported in Chromium, the demos from here on down will only work in browsers based upon it. For browsers that don’t support it we could add individual keyframes for each step, but for demonstration purposes I’m skipping out on that here.

See the Pen CSS Gradient Border (Animated) by Bramus (@bramus) on CodePen.

~

While the effect here looks quite nice it won’t play nice with more than two colors. Take this rainbow animated gradient border for example:

See the Pen CSS Rainbow Gradient Border (Animated, Attempt 1) by Bramus (@bramus) on CodePen.

🕵️‍♂️ You can see best what’s going on by toggling the fill option there … ugh, that’s not what we want!

To fix that I first thought of using a radial gradient but what I need is a conic gradient:

This way each color of the gradient will extend nicely into the border, resulting in a correct animation.

div {
    /* … */
    border-image: conic-gradient(from var(--angle), red, yellow, lime, aqua, blue, magenta, red) 1;
}

💁‍♂️ To make the end of the gradient blend nicely into its begin color, we have to repeat the said color — e.g. red — as the last entry.

See the Pen CSS Rainbow Gradient Border (Animated) by Bramus (@bramus) on CodePen.

When toggling the fill you can see that border-image here stretches out the color perpendicular to its edge, instead of letting the gradient “pass”. This might not be 100% what you want, but for me it was exactly what I aimed for 🙂

~

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

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

☕️ Buy me a Coffee (€3)

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

Form Validation: You want :not(:focus):invalid, not :invalid

Update 2021-01-28: In case your form controls have the required attribute set, you’ll even want to use the more extensive :not(:focus):not(:placeholder-shown):invalid selector.

See the update at the end of this post for more info on this.

We’ve all been in this situation, where the built-in form validation of the browser starts complaining while you’re still entering data into the form:

Highly annoying, but thankfully there’s an easy way to fix this.

~

# The problem

The problem is caused by a piece of CSS similar to this snippet:

.error-message {
    display: none;
}

input:invalid {
  border-color: var(--color-invalid);
}

input:invalid ~ .error-message {
  display: block; 
}

input:valid {
  border-color: var(--color-valid);
}

When entering an e-mail address — as Ryan is doing above — this is extremely annoying as your e-mail address is only valid when you’re done entering it. Try it in the demo below.

See the Pen
Form Validation on Blur (1/4)
by Bramus (@bramus)
on CodePen.

Ugh! 🤬

💁‍♂️ For a slight moment you’ll notice that an e-mail address in the form of [email protected] (e.g. without a .tld suffix) is also considered valid. As per RFC 822 the [email protected] format — used mainly in local networks — indeed is allowed.

~

# Making things better

It would be nice to only perform the validation when the field is not being edited anymore. In CSS we don’t have a blur event, but what we do have is a pseudo-class selector to indicate whether an input has the focus: :focus. Combine that with :not() and we have a way to target the “not being focussed” state, which also indicates that the field is not being edited.

Putting it all together, our CSS becomes this:

.error-message {
    display: none;
}

input:not(:focus):invalid {
  border-color: var(--color-invalid);
}

input:not(:focus):invalid ~ .error-message {
  display: block; 
}

input:not(:focus):valid {
  border-color: var(--color-valid);
}

This way the validations only happen when you’re blurred out of the form.

See the Pen
Form Validation on Blur (2/4)
by Bramus (@bramus)
on CodePen.

Ah, that’s better! 😊

~

# Making things even more better

In the demo above you’ll see one small side-effect though: the border is green by default, even though we didn’t enter any value. This is not exactly what we want Ideally we only want to validate in case the field is both not focussed and not empty.

In CSS we can’t use :empty for this though, as :empty targets elements that have no children/innerHTML content. What we can do however is abuse the :placeholder-shown pseudo-class.

  • If there’s no text entered, the placeholder is shown
  • If there is text entered, the placeholder is not shown

With this in mind, our code now becomes this:

.error-message {
    display: none;
}

input:not(:focus):invalid {
  border-color: var(--color-invalid);
}

input:not(:focus):invalid ~ .error-message {
  display: block; 
}

input:not(:focus):not(:placeholder-shown):valid {
  border-color: var(--color-valid);
}

⚠️ Do note that this requires a value for the input‘s placeholder.

<input type="email" placeholder="you​@​example​.​org" />

If you don’t want any placeholder to show, set its value to   (space)

Here’s an adjusted demo:

See the Pen
Form Validation on Blur (3/4)
by Bramus (@bramus)
on CodePen.

Yes, exactly what we want! 🤩

Note: In the future we will be able to use the :user-invalid pseudo class for exactly this use-case.

The :user-invalid pseudo-class represents an element with incorrect input, but only after the user has significantly interacted with it.

This feature is still in the works and not supported yet. Firefox supports it using the non-standard ::-moz-ui-invalid name. Thanks for the tip, Schepp!

~

# Update 2021-01-28: Playing nice with required

As reader Corey pointed out in the comments below the code above does not play nice with the required attribute. When the attribute is added, the error message will be shown when the form loads.

To work around this we also need to include the :not(:placeholder-shown) pseudo-class in our :invalid selectors.

.error-message {
    display: none;
}

input:not(:focus):not(:placeholder-shown):invalid {
  border-color: var(--color-invalid);
}

input:not(:focus):not(:placeholder-shown):invalid ~ .error-message {
  display: block; 
}

input:not(:focus):not(:placeholder-shown):valid {
  border-color: var(--color-valid);
}

Putting it all together, here’s our final demo:

See the Pen
Form Validation on Blur (4/4)
by Bramus (@bramus)
on CodePen.

Phew! 😅

~

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

Note: The title of this post is definitely a reference to this post on CSS-Tricks and this post by Kilian.

Cancel JavaScript Event Listeners with AbortController

One of the new features that was added in Chrome 88 is the ability to pass an AbortController‘s signal into addEventListener.

const controller = new AbortController();
const { signal } = controller;

document.querySelector('…').addEventListener('foo', (e) => {
  // …
}, {signal});

By calling controller.abort();, you can remove the listener from the event target, just like when calling element.removeEventListener.

💁‍♂️ Did you know you can use the AbortController to abort promises? It was its first intended use case when it was introduced back in 2017.

~

An excellent use-case here is that you can cancel multiple Event Listeners at once using it, as shared by Jake Archibald:

In the example above there are two competing event listeners. Whenever one of them finishes controller.abort(); is called, thus also removing the other one. Cool!

~

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

CSS Properties Memory Test

Remember that HTML Tags Memory Test from before? Šime Vidas recently joked that a similar Memory Test but for CSS Properties should exist.

Of course I couldn’t resist, so here is the CSS Properties Memory Test 🤪


See the Pen CSS Properties Memory Test by Bramus (@bramus) on CodePen.

In total there are 653 properties for you to guess. Good luck! 😅

~

# Behind the scenes

The demo itself is a straight up fork from the aforementioned HTML Tags Memory Test with an adjusted list of items to guess and a small style change to tweak the appearance of the already guessed results.

The list of itself properties was generated from the “Properties and Descriptors” CSS Index. The index lists 705 properties, but I’ve filtered out -webkit-prefixed properties that also exist without such a prefix. The filtered array was eventually converted to a string and base64 encoded to discourage cheating.

// Get all listed properties
const allProps = $$('#properties + div > ul.index > li').map(($li) => $li.textContent.trim().split("\n")[0]); // ~> 705 props

// Filter out -webkit prefixed props that also exist without a prefix
const filteredProps = allProps.filter(prop => !(prop.startsWith('-webkit-') && allProps.includes(prop.replace('-webkit-', '')))); // ~> 650 props

// Base64 encode the whole lot string to discourage cheating ;)
const encodedProps = btoa(filteredProps.join(','));

Note that when decoded again there are 653 properties to guess. That’s because one of the entries lists 4 properties separated by a comma.

~

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

Using Emoji as the Mouse Cursor on a Webpage

Recently I saw this tweet by Marco Denic fly by:

To use an emoji as the cursor you can’t simply type in the emoji though.

/* ❌ This won't work */
html {
	cursor: 👻, auto;
}

What you’ll have to do instead is embed the emoji inside an SVG and then successively embed that SVG in the CSS file by passing it as a Data URL into the url() function.

/* ✅ This will work */
html {
	cursor: url('data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" height="64" width="64"><text y="28" font-size="32">👻</text><path d="M0,2 L0,0 L2,0" fill="red" /></svg>'), auto;
}

See the Pen
Emoji Cursor
by Bramus (@bramus)
on CodePen.

If you’re on a device that does not show a pointer, here’s a recording of what the demo looks like:

In the code above I’ve also added a little triangle in the top left corner of the SVG, as that’s where the actual tip of the pointer is. Omitting it makes up for a really weird experience.

To customize the color of the tip you can change its fill value to any color you like. Although not recommended you can remove the entire <path> if you don’t want it.

Update 2020-01-27: As reader Louis Houbregts points out it’s also possible to set the X/Y coordinates to indicate where the tip of the pointer is.

html {
	cursor: url(…) 10 0, auto;
}

For emoji this isn’t an ideal option though, as emoji differ per platform/vendor.

To change the overall size of the emoji cursor, change the height and width attributes of the SVG. Best is to leave the other attributes (such as viewBox and font-size) alone, as those have been carefully tweaked.

🔥 Using this same technique you can set an emoji as the favicon.

~

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

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

☕️ Buy me a Coffee (€3)

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

CSS mix-blend-mode not working? Set a background-color!

💡 If you find your CSS mix-blend-mode not working as expected (on a white background), you need to explicitly set a background-color on the underlying element. The easiest way to do so is to apply background-color: white; on the html and body elements.

html, body {
    background-color: #fff;
}

~

Demos + Explanation

Without a background-color set

You’ll notice here that for the “white” sections, the set mix-blend-mode: difference does not seem to work. The navigation will stay white and visually blend into the white background. Note that the navigation is not actually gone, as you can still see it shine through whenever a number of any of the sections crosses it.

See the Pen CSS mix-blend-mode not working? (1/2) by Bramus (@bramus) on CodePen.

The reason why it doesn’t work is that the white sections don’t really have a white background. They have no background-color set, so they fall back to the default value of transparent. Visually this is manifested as a white color, but to the compositor it will still be transparent. As the compositor can’t calculate the difference of the white text against the transparent background, the text will remain white.

~

With a background-color set

With background-color: #fff; set on the body/html the compositor does know how to calc the difference, and the demo will behave correctly.


See the Pen CSS mix-blend-mode not working? (2/2) by Bramus (@bramus) on CodePen.

Alternatively we could set this declaration on the sections themselves:

section {
    background-color: #fff;
}

~

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

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

☕️ Buy me a Coffee (€3)

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

Accept several email addresses in a form with the multiple attribute

A popular tweet of mine that’s been doing rounds again (thanks to an RT by Stefan, whom you should definitely follow) is this little tip:

By setting the multiple attribute on an input[type="email"] you can have it accept multiple entries. Each entry is separated by a comma and is validated individually.

Here’s a little demo video of how that works:

💁‍♂️ For a slight moment there you can see that [email protected] is considered valid. As per RFC 822 the [email protected] format — used mainly in local networks — indeed is allowed.

~

However, it was pointed out to me that on iOS this isn’t usable by default:

On iOS, the “email keyboard” looks like this, with no comma to be found (not even when switching to numbers/symbols):

To work around this limitation you can manually override the input to use the regular keyboard by setting the inputmode attribute to text.

That way we still have the built-in browser validation rules (triggered by [type="email"]) and a means to type in a comma (triggered by [inputmode="text"]). Double win!

💁‍♂️ Sidenote: With this inputmode attribute you can create better number inputs.

~

Combining what we know, here’s a full demo for you to play with:

See the Pen
Accepting multiple e-mail addresses in one input
by Bramus (@bramus)
on CodePen.

~

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

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

☕️ Buy me a Coffee (€3)

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

Nested Media Queries

I can’t seem to find any mention of this in the Media Queries Module specification, but apparently it’s allowed to nest media queries, as shared by Šime Vidas:

That’s … awesome! 🤯

Fiddling with it a bit more, turns out this snippet also works as expected:

@media not print {
  @media (min-width: 0) {
    p {
      font-weight: bold;
    }
    @media (max-width: 750px) {
      p {
        background: yellow;
      }
    }
  }
}

You can play with this CodePen demo to try it yourself.

💁‍♂️ Don’t confuse Nested Media Queries with CSS Nesting, an upcoming feature of CSS, which allows you to nest selectors.

UPDATE: Thanks to reader Vadim Makeev for pointing out that support for nested @media blocks was added to Opera 12.50 back in 2012! Its syntax is defined in the CSS Conditional Rules Module specification.

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