Tag: Control

React Authentication & Access Control

Authentication and access control are required for most applications, but they often distract us from building core features. In this article, I’ll cover a straightforward way to add auth and access control in React.

Instead of adding a static library that you have to keep up to date or re-research each time you build a project, we’ll use a service that stays up to date automatically and is a much simpler alternative to Auth0, Okta, and others.

React authentication

There are two main things your React application needs to do to sign on a user:

  1. Get an access token from an authentication server
  2. Send the access token to your backend server with each subsequent request

These steps are the same for pretty much all authentication, whether that’s standard email and password, magic links, or single sign on (SSO) providers like Google, Azure, or Facebook.

Ultimately, we want our React app to send an initial request to an authentication server and have that server generate an access token we can use.

JWT access tokens

There are different choices for what type of access token to use, and JSON Web Tokens (JWTs) are a great option. JWTs are compact, URL-safe tokens that your React application can use for authentication and access control.

Each JWT has a JSON object as its “payload” and is signed such that your backend server can verify that the payload is authentic. An example JWT looks like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9.f7iKN-xi24qrQ5NQtOe0jiriotT-rve3ru6sskbQXnA 

The payload for this JWT is the middle section (separated by periods):

eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9

The JWT payload can be decoded from base64 to yield the JSON object:

JSON.parse(atob("eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9"));  // => {   “userId”: 1,   “authorization”: “admin” }

It’s important to note that this payload is readable by anyone with the JWT, including your React application or a third party. Anyone that has the JWT can read its contents.

However, only the authentication server can generate valid JWTs. Your React application, your backend server, or a malicious third party cannot generate valid JWTs, only read and verify them.

When your backend server receives a request with a JWT, it should verify the JWT as authentic by checking it against the public key for that JWT. This allows your application server to verify incoming JWTs and reject any tokens that were not created by the authentication server (or that have expired).

The flow for using a JWT in your React application looks like this:

  1. Your React app requests a JWT from the authentication server whenever the user wants to sign on.
  2. The authentication server generates a JWT using a private key and then sends the JWT back to your React app.
  3. Your React app stores this JWT and sends it to your backend server whenever your user needs to make a request.
  4. Your backend server verifies the JWT using a public key and then reads the payload to determine which user is making the request.

Each of these steps is simple to write down, but each step has its own pitfalls when you actually want to implement it and keep it secure. Especially over time, as new threat vectors emerge and new platforms need to be patched or supported, the security overhead can add up quickly.

Userfront removes auth complexity in React apps

Userfront is a framework that abstracts away auth complexity. This makes it much easier for you to work with authentication in a React application and, perhaps most importantly, it keeps all the auth protocols updated for you automatically over time.

The underlying philosophy with Userfront is that world-class auth should not take effort – it should be easy to set up, and security updates should happen for you automatically. Userfront has all the bells and whistles of authentication, Single Sign On (SSO), access control, and multi-tenancy, with a production-ready free tier up to 10,000 monthly active users.

For most modern React applications, it’s a great solution.

Setting up authentication in React

Now we’ll go through building all the main aspects of authentication in a React application. The final code for this example is available here.

Set up your React application and get your build pipeline in order however you prefer. In this tutorial, we’ll use Create React App, which does a lot of the setup work for us, and we’ll also add React Router for client-side routing. Start by installing Create React App and React Router:

npx create-react-app my-app cd my-app npm install react-router-dom --save npm start

Now our React application is available at http://localhost:3000.

Like the page says, we can now edit the src/App.js file to start working.

Replace the contents of src/App.js with the following, based on the React Router quickstart:

// src/App.js  import React from "react"; import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";  export default function App() {   return (     <Router>       <div>         <nav>           <ul>             <li>               <Link to="/">Home</Link>             </li>             <li>               <Link to="/login">Login</Link>             </li>             <li>               <Link to="/reset">Reset</Link>             </li>             <li>               <Link to="/dashboard">Dashboard</Link>             </li>           </ul>         </nav>          <Switch>           <Route path="/login">             <Login />           </Route>           <Route path="/reset">             <PasswordReset />           </Route>           <Route path="/dashboard">             <Dashboard />           </Route>           <Route path="/">             <Home />           </Route>         </Switch>       </div>     </Router>   ); }  function Home() {   return <h2>Home</h2>; }  function Login() {   return <h2>Login</h2>; }  function PasswordReset() {   return <h2>Password Reset</h2>; }  function Dashboard() {   return <h2>Dashboard</h2>; }

Now we have ourselves a very simple app with routing:

Route Description
/ Home page
/login Login page
/reset Password reset page
/dashboard User dashboard, for logged in users only

This is all the structure we need to start adding authentication.

Signup, login, and password reset with Userfront

Go ahead and create a Userfront account. This will give you a signup form, login form, and password reset form you can use for the next steps.

In the Toolkit section of your Userfront dashboard, you can find the instructions for installing your signup form:

Follow the instructions by installing the Userfront React package with:

npm install @userfront/react --save npm start

Add the signup form to your home page by importing and initializing Userfront, and then updating the Home() function to render the signup form.

// src/App.js  import React from "react"; import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; import Userfront from "@userfront/react";  Userfront.init("demo1234");  const SignupForm = Userfront.build({   toolId: "nkmbbm", });  export default function App() {   return (     <Router>       <div>         <nav>           <ul>             <li>               <Link to="/">Home</Link>             </li>             <li>               <Link to="/login">Login</Link>             </li>             <li>               <Link to="/reset">Reset</Link>             </li>             <li>               <Link to="/dashboard">Dashboard</Link>             </li>           </ul>         </nav>          <Switch>           <Route path="/login">             <Login />           </Route>           <Route path="/reset">             <PasswordReset />           </Route>           <Route path="/dashboard">             <Dashboard />           </Route>           <Route path="/">             <Home />           </Route>         </Switch>       </div>     </Router>   ); }  function Home() {   return (     <div>       <h2>Home</h2>       <SignupForm />     </div>   ); }  function Login() {   return <h2>Login</h2>; }  function PasswordReset() {   return <h2>Password Reset</h2>; }  function Dashboard() {   return <h2>Dashboard</h2>; }

Now the home page has your signup form. Try signing up a user:

Your signup form is in “Test mode” by default, which will create user records in a test environment you can view separately in your Userfront dashboard:

Continue by adding your login and password reset forms in the same way that you added your signup form:

// src/App.js  import React from "react"; import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; import Userfront from "@userfront/react";  Userfront.init("demo1234");  const SignupForm = Userfront.build({   toolId: "nkmbbm", }); const LoginForm = Userfront.build({   toolId: "alnkkd", }); const PasswordResetForm = Userfront.build({   toolId: "dkbmmo", });  export default function App() {   return (     <Router>       <div>         <nav>           <ul>             <li>               <Link to="/">Home</Link>             </li>             <li>               <Link to="/login">Login</Link>             </li>             <li>               <Link to="/reset">Reset</Link>             </li>             <li>               <Link to="/dashboard">Dashboard</Link>             </li>           </ul>         </nav>          <Switch>           <Route path="/login">             <Login />           </Route>           <Route path="/reset">             <PasswordReset />           </Route>           <Route path="/dashboard">             <Dashboard />           </Route>           <Route path="/">             <Home />           </Route>         </Switch>       </div>     </Router>   ); }  function Home() {   return (     <div>       <h2>Home</h2>       <SignupForm />     </div>   ); }  function Login() {   return (     <div>       <h2>Login</h2>       <LoginForm />     </div>   ); }  function PasswordReset() {   return (     <div>       <h2>Password Reset</h2>       <PasswordResetForm />     </div>   ); }  function Dashboard() {   return <h2>Dashboard</h2>; }

At this point, your signup, login, and password reset should all be functional.

Your users can sign up, log in, and reset their password.

Access control in React

Usually, we don’t want users to be able to view the dashboard unless they are logged in. This is known as a protected route.

Whenever a user is not logged in but tries to visit /dashboard, we can redirect them to the login screen.

We can accomplish this by updating the Dashboard component in src/App.js to handle the conditional logic.

When a user is logged in with Userfront, they will have an access token available as Userfront.accessToken(). We can check for this token to determine if the user is logged in. If the user is logged in, we can show the dashboard page, and if the user is not logged in, we can redirect to the login page.

Add the Redirect component to the import statement for React Router, and then update the Dashboard component to redirect if no access token is present.

// src/App.js  import React from "react"; import {   BrowserRouter as Router,   Switch,   Route,   Link,   Redirect, // Be sure to add this import } from "react-router-dom";  // ...  function Dashboard() {   function renderFn({ location }) {     // If the user is not logged in, redirect to login     if (!Userfront.accessToken()) {       return (         <Redirect           to={{             pathname: "/login",             state: { from: location },           }}         />       );     }      // If the user is logged in, show the dashboard     const userData = JSON.stringify(Userfront.user, null, 2);     return (       <div>         <h2>Dashboard</h2>         <pre>{userData}</pre>         <button onClick={Userfront.logout}>Logout</button>       </div>     );   }    return <Route render={renderFn} />; }

Notice also that we’ve added a logout button by calling Userfront.logout() directly:

<button onClick={Userfront.logout}>Logout</button>

Now, when a user is logged in, they can view the dashboard. If the user is not logged in, they will be redirected to the login page.

React authentication with an API

You’ll probably want to retrieve user-specific information from your backend. In order to protect your API endpoints, your backend server should check that incoming JWTs are valid.

There are many libraries available to read and verify JWTs across various languages; here are a few popular libraries for handling JWTs:

Node.js .NET Python Java

Your access token

While the user is logged in, their access token is available in your React application as Userfront.accessToken().

Your React application can send this as a Bearer token inside the Authorization header to your backend server. For example:

// Example of calling an endpoint with a JWT  import Userfront from "@userfront/react"; Userfront.init("demo1234");  async function getInfo() {   const res = await window.fetch("/your-endpoint", {     method: "GET",     headers: {       "Content-Type": "application/json",       Authorization: `Bearer $ {Userfront.accessToken()}`,     },   });    console.log(res); }  getInfo();

To handle a request like this, your backend should read the JWT from the Authorization header and verify that it is valid using the public key found in your Userfront dashboard.

Here is an example of Node.js middleware to read and verify the JWT access token:

// Node.js example (Express.js)  const jwt = require("jsonwebtoken");  function authenticateToken(req, res, next) {   // Read the JWT access token from the request header   const authHeader = req.headers["authorization"];   const token = authHeader && authHeader.split(" ")[1];   if (token == null) return res.sendStatus(401); // Return 401 if no token    // Verify the token using the Userfront public key   jwt.verify(token, process.env.USERFRONT_PUBLIC_KEY, (err, auth) => {     if (err) return res.sendStatus(403); // Return 403 if there is an error verifying     req.auth = auth;     next();   }); }

Using this approach, any invalid or missing tokens would be rejected by your server. You can also reference the contents of the token later in the route handlers using the req.auth object:

console.log(req.auth);  // => {   mode: 'test',   tenantId: 'demo1234',   userId: 5,   userUuid: 'ab53dbdc-bb1a-4d4d-9edf-683a6ca3f609',   isConfirmed: false,   authorization: {     demo1234: {       tenantId: 'demo1234',       name: 'Demo project',       roles: ["admin"],       permissions: []     },   },   sessionId: '35d0bf4a-912c-4429-9886-cd65a4844a4f',   iat: 1614114057,   exp: 1616706057 }

With this information, you can perform further checks as desired, or use the userId or userUuid to look up user information to return.

For example, if you wanted to limit a route to admin users only, you could check against the authorization object from the verified access token, and reject any tokens that don’t have an admin role:

// Node.js example (Express.js)  app.get("/users", (req, res) => {   const authorization = req.auth.authorization["demo1234"] || {};    if (authorization.roles.includes("admin")) {     // Allow access   } else {     // Deny access   } });

React SSO (Single Sign On)

With your Toolkit forms in place, you can add social identity providers like Google, Facebook, and LinkedIn to your React application, or business identity providers like Azure AD, Office365, and more.

You do this by creating an application with the identity provider (e.g. Google), and then adding that application’s credentials to the Userfront dashboard. The result is a modified sign on experience:

No additional code is needed to implement Single Sign On using this approach: you can add and remove providers without updating your forms or the way you handle JWTs.

Final notes

React authentication and access control can be complex to do yourself, or it can be simple when using a service.

Both the setup step and, more importantly, the maintenance over time, are handled with modern platforms like Userfront.

JSON Web Tokens allow you to cleanly separate your auth token generation layer from the rest of your application, making it easier to reason about and more modular for future needs. This architecture also allows you to focus your efforts on your core application, where you are likely to create much more value for yourself or your clients.

For more details on adding auth to your React application, visit the Userfront guide, which covers everything from setting up your auth forms to API documentation, example repositories, working with different languages and frameworks, and more.


The post React Authentication & Access Control appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,

Weekly Platform News: Focus Rings, Donut Scope, Ditching em Units, and Global Privacy Control

In this week’s news, Chrome tackles focus rings, we learn how to get “donut” scope, Global Privacy Control gets big-name adoption, it’s time to ditch pixels in media queries, and a snippet that prevents annoying form validation styling.

Chrome will stop displaying focus rings when clicking buttons

Chrome, Edge, and other Chromium-based browsers display a focus indicator (a.k.a. focus ring) when the user clicks or taps a (styled) button. For comparison, Safari and Firefox don’t display a focus indicator when a button is clicked or tapped, but do only when the button is focused via the keyboard.

The focus ring will stay on the button until the user clicks somewhere else on the page.

Some developers find this behavior annoying and are using various workarounds to prevent the focus ring from appearing when a button is clicked or tapped. For example, the popular what-input library continuously tracks the user’s input method (mouse, keyboard or touch), allowing the page to suppress focus rings specifically for mouse clicks.

[data-whatintent="mouse"] :focus {   outline: none; }

A more recent workaround was enabled by the addition of the CSS :focus-visible pseudo-class to Chromium a few months ago. In the current version of Chrome, clicking or tapping a button invokes the button’s :focus state but not its :focus-visible state. that way, the page can use a suitable selector to suppress focus rings for clicks and taps without affecting keyboard users.

:focus:not(:focus-visible) {   outline: none; }

Fortunately, these workarounds will soon become unnecessary. Chromium’s user agent stylesheet recently switched from :focus to :focus-visible, and as a result of this change, button clicks and taps no longer invoke focus rings. The new behavior will first ship in Chrome 90 next month.

The enhanced CSS :not() selector enables “donut scope”

I recently wrote about the A:not(B *) selector pattern that allows authors to select all A elements that are not descendants of a B element. This pattern can be expanded to A B:not(C *) to create a “donut scope.”

For example, the selector article p:not(blockquote *) matches all <p> elements that are descendants of an <article> element but not descendants of a <blockquote> element. In other words, it selects all paragraphs in an article except the ones that are in a block quotation.

The donut shape that gives this scope its name

The New York Times now honors Global Privacy Control

Announced last October, Global Privacy Control (GPC) is a new privacy signal for the web that is designed to be legally enforceable. Essentially, it’s an HTTP Sec-GPC: 1 request header that tells websites that the user does not want their personal data to be shared or sold.

The DuckDuckGo Privacy Essentials extension enables GPC by default in the browser

The New York Times has become the first major publisher to honor GPC. A number of other publishers, including The Washington Post and Automattic (WordPress.com), have committed to honoring it “this coming quarter.”

From NYT’s privacy page:

Does The Times support the Global Privacy Control (GPC)?

Yes. When we detect a GPC signal from a reader’s browser where GDPR, CCPA or a similar privacy law applies, we stop sharing the reader’s personal data online with other companies (except with our service providers).

The case for em-based media queries

Some browsers allow the user to increase the default font size in the browser’s settings. Unfortunately, this user preference has no effect on websites that set their font sizes in pixels (e.g., font-size: 20px). In part for this reason, some websites (including CSS-Tricks) instead use font-relative units, such as em and rem, which do respond to the user’s font size preference.

Ideally, a website that uses font-relative units for font-size should also use em values in media queries (e.g., min-width: 80em instead of min-width: 1280px). Otherwise, the site’s responsive layout may not always work as expected.

For example, CSS-Tricks switches from a two-column to a one-column layout on narrow viewports to prevent the article’s lines from becoming too short. However, if the user increases the default font size in the browser to 24px, the text on the page will become larger (as it should) but the page layout will not change, resulting in extremely short lines at certain viewport widths.

If you’d like to try out em-based media queries on your website, there is a PostCSS plugin that automatically converts min-width, max-width, min-height, and max-height media queries from px to em.

(via Nick Gard)

A new push to bring CSS :user-invalid to browsers

In 2017, Peter-Paul Koch published a series of three articles about native form validation on the web. Part 1 points out the problems with the widely supported CSS :invalid pseudo-class:

  • The validity of <input> elements is re-evaluated on every key stroke, so a form field can become :invalid while the user is still typing the value.
  • If a form field is required (<input required>), it will become :invalid immediately on page load.

Both of these behaviors are potentially confusing (and annoying), so websites cannot rely solely on the :invalid selector to indicate that a value entered by the user is not valid. However, there is the option to combine :invalid with :not(:focus) and even :not(:placeholder-shown) to ensure that the page’s “invalid” styles do not apply to the <input> until the user has finished entering the value and moved focus to another element.

The CSS Selectors module defines a :user-invalid pseudo-class that avoids the problems of :invalid by only matching an <input> “after the user has significantly interacted with it.”

Firefox already supports this functionality via the :-moz-ui-invalid pseudo-class (see it in action). Mozilla now intends to un-prefix this pseudo-class and ship it under the standard :user-invalid name. There are still no signals from other browser vendors, but the Chromium and WebKit bugs for this feature have been filed.


The post Weekly Platform News: Focus Rings, Donut Scope, Ditching em Units, and Global Privacy Control appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , , , , , ,
[Top]

Give Users Control: The Media Session API

Here’s a scenario. You start a banging Kendrick Lamar track in one of your many open browser tabs. You’re loving it, but someone walks into your space and you need to pause it. Which tab is it? Browsers try to help with that a little bit. You can probably mute the entire system audio. But wouldn’t it be nice to actually have control over the audio playback without necessarily needing to find your way back to that tab?

The Media Session API makes this possible. It gives media playback access to the user outside of the browser tab where it is playing. If implemented, it will be available in various places on the device, including:

  • the notifications area on many mobile devices,
  • on other wearables, and
  • the media hub area of many desktop devices.

In addition, the Media Session API allows us to control media playback with media keys and voice assistants like Siri, Google Assistant, Bixby, or Alexa.

The Media Session API

The Media Session API mainly consists of the two following interfaces:

  • MediaMetadata
  • MediaSession

The MediaMetadata interface is what provides data about the playing media. It is responsible for letting us know the media’s title, album, artwork and artist (which is Kendrick Lamar in this example). The MediaSession interface is what is responsible for the media playback functionality.

Before we take a deep dive into the topic, we would have to take note of feature detection. It is good practice to check if a browser supports a feature before implementing it. To check if a browser supports the Media Session API, we would have to include the following in our JavaScript file:

if ('mediaSession' in navigator) {   // Our media session api that lets us seek to the beginning of Kendrick Lamar's &quot;Alright&quot; }

The MediaMetadata interface

The constructor, MediaMetadata.MediaMetadata() creates a new MediaMetadata object. After creating it, we can add the following properties:

  • MediaMetadata.title sets or gets the title of the media playing.
  • MediaMetadata.artist sets or gets the name of the artist or group of the media playing.
  • MediaMetadata.album sets or gets the name of the album containing the media playing.
  • MediaMetadata.artwork sets or gets the array of images related with the media playing.

The value of the artwork property of the MediaMetadata object is an array of MediaImage objects. A MediaImage object contains details describing an image associated with the media. The objects have the three following properties:

  • src: the URL of the image
  • sizes: indicates the size of the image so one image does not have to be scaled
  • type: the MIME type of the image

Let’s create a MediaMetadata object for Kendrick Lamar’s “Alright” off his To Pimp a Butterfly album.

if ('mediaSession' in navigator) {   navigator.mediaSession.metadata = new MediaMetadata({     title: 'Alright',     artist: 'Kendrick Lamar',     album: 'To Pimp A Butterfly',     artwork: [       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },       // More sizes, like 192x192, 256x256, 384x384, and 512x512     ]   }); }

The MediaSession interface

As stated earlier, this is what lets the user control the playback of the media. We can perform the following actions on the playing media through this interface:

  • play: play the media
  • pause: pause the media
  • previoustrack: switch to the previous track
  • nexttrack: switch to the next track
  • seekbackward: seek backward from the current position, by a few seconds
  • seekforward: seek forward from the current position, by a few seconds
  • seekto: seek to a specified time from the current position
  • stop: stop media playback
  • skipad: skip past the advertisement playing, if any

The MediaSessionAction enumerated type makes these actions available as string types. To support any of these actions, we have to use the MediaSession’s setActionHandler() method to define a handler for that action. The method takes the action, and a callback that is called when the user invokes the action. Let us take a not-too-deep dive to understand it better.

To set handlers for the play and pause actions, we include the following in our JavaScript file:

let alright = new HTMLAudioElement();  if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('play', () => {     alright.play();   });   navigator.mediaSession.setActionHandler('pause', () => {     alright.pause();   }); }

Here we set the track to play when the user plays it and pause when the user pauses it through the media interface.

For the previoustrack and nexttrack actions, we include the following:

let u = new HTMLAudioElement(); let forSaleInterlude = new HTMLAudioElement();  if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('previoustrack', () => {     u.play();   });   navigator.mediaSession.setActionHandler('nexttrack', () => {     forSaleInterlude.play();   }); }

This might not completely be self-explanatory if you are not much of a Kendrick Lamar fan but hopefully, you get the gist. When the user wants to play the previous track, we set the previous track to play. When it is the next track, it is the next track.

To implement the seekbackward and seekforward actions, we include the following:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('seekbackward', (details) => {     alright.currentTime = alright.currentTime - (details.seekOffset || 10);   });   navigator.mediaSession.setActionHandler('seekforward', (details) => {     alright.currentTime = alright.currentTime + (details.seekOffset || 10);   }); }

Given that I don’t consider any of this self-explanatory, I would like to give a concise explanation about the seekbackward and seekforward actions. The handlers for both actions, seekbackward and seekforward, are fired, as their names imply, when the user wants to seek backward or forward by a few number of seconds. The MediaSessionActionDetails dictionary provides us the “few number of seconds” in a property, seekOffset. However, the seekOffset property is not always present because not all user agents act the same way. When it is not present, we should set the track to seek backward or forward by a “few number of seconds” that makes sense to us. Hence, we use 10 seconds because it is quite a few. In a nutshell, we set the track to seek by seekOffset seconds if it is provided. If it is not provided, we seek by 10 seconds.

To add the seekto functionality to our Media Session API, we include the following snippet:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('seekto', (details) => {     if (details.fastSeek && 'fastSeek' in alright) {       alright.fastSeek(details.seekTime);       return;     }     alright.currentTime = details.seekTime;   }); }

Here, the MediaSessionActionDetails dictionary provides the fastSeek and seekTime properties. fastSeek is basically seek performed rapidly (like fast-forwarding or rewinding) while seekTime is the time the track should seek to. While fastSeek is an optional property, the MediaSessionActionDetails dictionary always provides the seekTime property for the seekto action handler. So fundamentally, we set the track to fastSeek to the seekTime when the property is available and the user fast seeks, while we just set it to the seekTime when the user just seeks to a specified time.

Although I wouldn’t know why one would want to stop a Kendrick song, it won’t hurt to describe the stop action handler of the MediaSession interface:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('stop', () => {     alright.pause();     alright.currentTime = 0;   }); } 

The user invokes the skipad (as in, “skip ad” rather than “ski pad”) action handler when an advertisement is playing and they want to skip it so they can continue listening to Kendrick Lamar’s “Alright track. If I’m being honest, the complete details of the skipad action handler is out of the scope of my “Media Session API” understanding. Hence, you should probably look that up on your own after reading this article, if you actually want to implement it.

Wrapping up

We should take note of something. Whenever the user plays the track, seeks, or changes the playback rate, we are supposed to update the position state on the interface provided by the Media Session API. What we use to implement this is the setPositionState() method of the mediaSession object, as in the following:

if ('mediaSession' in navigator) {   navigator.mediaSession.setPositionState({     duration: alright.duration,     playbackRate: alright.playbackRate,     position: alright.currentTime   }); }

In addition, I would like to remind you that not all browsers of the users would support all the actions. Therefore, it is recommended to set the action handlers in a try...catch block, as in the following:

const actionsAndHandlers = [   ['play', () => { /*...*/ }],   ['pause', () => { /*...*/ }],   ['previoustrack', () => { /*...*/ }],   ['nexttrack', () => { /*...*/ }],   ['seekbackward', (details) => { /*...*/ }],   ['seekforward', (details) => { /*...*/ }],   ['seekto', (details) => { /*...*/ }],   ['stop', () => { /*...*/ }] ]   for (const [action, handler] of actionsAndHandlers) {   try {     navigator.mediaSession.setActionHandler(action, handler);   } catch (error) {     console.log(`The media session action, $ {action}, is not supported`);   } }

Putting everything we have done, we would have the following:

let alright = new HTMLAudioElement(); let u = new HTMLAudioElement(); let forSaleInterlude = new HTMLAudioElement();  const updatePositionState = () => {   navigator.mediaSession.setPositionState({     duration: alright.duration,     playbackRate: alright.playbackRate,     position: alright.currentTime   }); }   const actionsAndHandlers = [   ['play', () => {     alright.play();     updatePositionState();   }],   ['pause', () => { alright.pause(); }],   ['previoustrack', () => { u.play(); }],   ['nexttrack', () => { forSaleInterlude.play(); }],   ['seekbackward', (details) => {     alright.currentTime = alright.currentTime - (details.seekOffset || 10);     updatePositionState();   }],   ['seekforward', (details) => {     alright.currentTime = alright.currentTime + (details.seekOffset || 10);     updatePositionState();   }],   ['seekto', (details) => {     if (details.fastSeek && 'fastSeek' in alright) {       alright.fastSeek(details.seekTime);       updatePositionState();       return;     }     alright.currentTime = details.seekTime;     updatePositionState();   }],   ['stop', () => {     alright.pause();     alright.currentTime = 0;   }], ]   if ( 'mediaSession' in navigator ) {   navigator.mediaSession.metadata = new MediaMetadata({     title: 'Alright',     artist: 'Kendrick Lamar',     album: 'To Pimp A Butterfly',     artwork: [       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },       // More sizes, like 192x192, 256x256, 384x384, and 512x512     ]   });     for (const [action, handler] of actionsAndHandlers) {     try {       navigator.mediaSession.setActionHandler(action, handler);     } catch (error) {       console.log(`The media session action, $ {action}, is not supported`);     }   } }

Here’s a demo of the API:

I implemented six of the actions. Feel free to try the rest during your leisure.

If you view the Pen on your mobile device, notice how it appears on your notification area.

If your smart watch is paired to your device, take a sneak peek at it.

If you view the Pen on Chrome on desktop, navigate to the media hub and play with the media buttons there. The demo even has multiple tracks, so you experiment moving forward/back through tracks.

If you made it this far (or not), thanks for reading and please, on the next app you create with media functionality, implement this API.


The post Give Users Control: The Media Session API appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

More Control Over CSS Borders With background-image

You can make a typical CSS border dashed or dotted. For example:

.box {    border: 1px dashed black;    border: 3px dotted red; }

You don’t have all that much control over how big or long the dashes or gaps are. And you certainly can’t give the dashes slants, fading, or animation! You can do those things with some trickery though.

Amit Sheen build this really neat Dashed Border Generator:

The trick is using four multiple backgrounds. The background property takes comma-separated values, so by setting four backgrounds (one along the top, right, bottom, and left) and sizing them to look like a border, it unlocks all this control.

So like:

.box {   background-image: repeating-linear-gradient(0deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(90deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(180deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(270deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px);   background-size: 3px 100%, 100% 3px, 3px 100% , 100% 3px;   background-position: 0 0, 0 0, 100% 0, 0 100%;   background-repeat: no-repeat; }

I like gumdrops.


The post More Control Over CSS Borders With background-image appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Get Programmatic Control of your Builds with Netlify Build Plugins

Today at Jamstack_Conf, Netlify announced Build Plugins. What it does is allow you to have particular hooks for events within your build, like when the build starts or ends. What’s nice about them is that they’re just a plain ‘ol JavaScript object, so you can insert some logic or kick off a library just the way you typically would within your application.

A “Build” is when you give your site to Netlify either via GitHub/GitLab/etc., or by literally just dropping the directory into the interface, Netlify will process all the assets, download and install packages, and generate a static version of the site to deploy to CDNs all around the world.

What the Build Plugin does is give you access to key points in time during that process, for instance, onPreBuild, onPostBuild, onSuccess, and so forth. You can execute some logic at those specific points in time, like this:

module.exports = {   onPreBuild: () => {     console.log('Hello world from onPreBuild event!')   }, }

You don’t only have to build them yourself, either! You can use build plugins that have been made by the community. There are very interesting ones, such as a11y, Cypress for testing, Inline Critical CSS, and my personal favorite, Subfont, which optimizes fonts for you in a really incredible way (you can watch a video about that).

Enable them through the dashboard through a few button clicks:

If you’d like to learn more, check out the announcement post here! Happy building!

The post Get Programmatic Control of your Builds with Netlify Build Plugins appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Building an accessible autocomplete control

Here’s a great in-depth post from Adam Silver about his journey to create an autocomplete field that’s as accessible as possible. There are so many edge cases to consider! There are old browsers and their peculiar quirks, there are accessibility best practices for screen readers, and not to mention dealing with the component design when there’s no JavaScript, etc.

Adam offers a warning before he begins:

[…] I’ve been looking at ways to let users enter a destination country. Unfortunately, native HTML form controls just aren’t good enough for this type of interaction. And so we need to build a custom autocomplete control from scratch. A word of warning though: this is one of the hardest UI components I’ve ever had to make—they’re just way harder than they look.

I also just bought Adam’s book, Form Design Patterns, and this post now makes me extra excited to read it.

Direct Link to ArticlePermalink

The post Building an accessible autocomplete control appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

Control Icons with Font Size

Here’s a nifty trick from Andy Bell that now seems a little obvious in hindsight: if you set an SVG to have a width and height of 1em then you can change the size of it with the font-size property.

Try and change the font-size on the body element below to see the icon scale with the text:

See the Pen
Font size controlled icon
by Andy Bell (@andybelldesign)
on CodePen.

You pretty much always want your icons to be aligned with text so this trick helps by creating that inherent relationship in your code. Pretty nifty, eh?

Direct Link to ArticlePermalink

The post Control Icons with Font Size appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]