Underline can look and work better, can they?

In the beginning: CSS and CSS 2

Links have been on the web since the beginning as a representation of a connection to another document. In the beginning you could only change the color of the link itself and its underline. By default, text links are blue and underlined. This underlining strikes through descenders – letters that dip below the baseline, such as a lowercase j – making some links more difficult to read.

with CSS level 2 we got the text-decoration keyword that allowed to change the way underlines in links and other elements looked like. With it you could do something like this to create lines above and below a matching element:

.over {
  text-decoration: overline;
}

.regular {
  text-decoration: underline;
}

This was good but limited. The values available were:

  • none to remove any underline
  • underline
  • overline to create a line above the content
  • line-through to strike through the text
  • blink
  • inherit from the parent element

you could change the color of the link (and the text) using the color keyword and the position of the link using text-decoration but that was the extent of it. The underline would still run through the descenders of letters and number

CSS 3

CSS has nove developed an entire module dedicated to text decorations. CSS Text Decoration Module Level 3 has been sitting at the candidate recommendation stage for almost 4 years yet it provides very interesting properties concerning underlines and other text decorations. We will only concentrate on the decorations part of the spec.

text-decoration-color

This rule defines the color of the underline. It can be any of the CSS color formats: color names, hexadecimal, rgb, rgba, hsl or the transparent and currentColor keywords. The example below shows some possibilities for the text-decoration-color keyword.

.demo {
  /* <color> values */
  text-decoration-color: currentColor;
  text-decoration-color: red;
  text-decoration-color: #00ff00;
  text-decoration-color: rgba(255, 128, 128, 0.5);
  text-decoration-color: transparent;
}

text-decoration-line

This rule defines the type of line that will be used with text decoration. It is the successor of the CSS 2 property with a different name. The rule also allows two or more selectors but be careful not to go overboard like the last example of the CSS block below shows.

.demo2 {
  /* Possible text-decoration-line values */
  text-decoration-line: none;
  text-decoration-line: underline;
  text-decoration-line: overline;
  text-decoration-line: line-through;
  text-decoration-line: blink;
   /* Two decoration lines */
  text-decoration-line: underline overline;               
  /* Multiple decoration lines */
  text-decoration-line: overline underline line-through;   
}

The table below explains the different values for the text-decoration-line keyword.

Explanation of the values for text-decoration-style
Value Description
underline Each line of text is underlined.
overline Each line of text has a line above it.
line-through Each line of text has a line through the middle.
blink The text blinks (alternates between visible and invisible). This value is deprecated in favor of Animations.

text-decoration-style

The text-decoration-style keyword defines what type of underline we use.

.demo3 {
  /* Possible text-decoration-style values */
  text-decoration-style: solid;
  text-decoration-style: double;
  text-decoration-style: dotted;
  text-decoration-style: dashed;
  text-decoration-style: wavy;
}
Definition of the values for text-decoration-style
Keyword Description
solid Draws a single line
double Draws a double line
dotted Draws a dotted line
dashed Draws a dashed line
wavy Draws a wavy line

text-decoration

text-decoration is a shorthand for text-decoration-color, text-decoration-line, and text-decoration-style CSS properties. Like for any other shorthand property, it means that it resets their value to their default if not explicitly set in the shorthand.

text-decoration-skip

The text-decoration-skip CSS property specifies what parts of the element’s content any text decoration affecting the element must skip over. It controls all text decoration lines drawn by the element and also any text decoration lines drawn by its ancestors.

.demo4 {
/* Possible values for text-decoration-skip */
text-decoration-skip: none;
text-decoration-skip: objects;
text-decoration-skip: spaces;
text-decoration-skip: ink;
text-decoration-skip: edges;
text-decoration-skip: box-decoration;

/* We can also combine values */
text-decoration-skip: ink spaces;
text-decoration-skip: ink edges box-decoration;
}
Description of the values for text-decoration-skip
Value Description
none Nothing is skipped, i.e. text decoration is drawn for all text content and across atomic inline-level boxes.
objects The entire margin box of the element is skipped if it is an atomic inline such as an image or inline-block.
spaces All spacing is skipped, i.e. all Unicode white space characters and all word separators, plus any adjacent letter-spacing or word-spacing.
ink The text decoration is only drawn where it does not touch or closely approach a glyph. I.e. it is interrupted where it would otherwise cross over a glyph.

example of decoration-skipink

edges

The start and end of the text decoration is placed slightly inward (e.g. by half of the line thickness) from the content edge of the decorating box. E.g. two underlined elements side-by-side do not appear to have a single underline. (This is important in Chinese, where underlining is a form of punctuation).
example of decoration-skip-edges

box-decoration The text decoration is skipped over the box’s margin, border and padding areas. This only has an effect on decorations imposed by an ancestor; a decorating box never draws over its own box decoration.

Further ideas

There are other features that we can use to further refine links.

If you’re working with languages other than English you can use text-underline-position to control where the underline will appear on the text.

Managing the CSS Cascade

There are times when the CSS cascade becomes a pain in the ass. When we want to revert the value of a property to its default (before we added any other value for the property in the current element or any ancestor up the chain) we have to remember the property but also what the default value is.

CSS provides several mechanisms to handle inheritance. We’ll discuss several ways to work with the cascade to make it do what we need.

initial

The newest (to me) way to reset is the initial value.

The initial CSS keyword sets and element to its initial value. It is allowed on every CSS property and causes the element for which it is specified to use the initial value of the property.

On inherited properties, the initial value may be surprising and you should consider using the inherit, unset, or revert keywords instead.

Test the hell out of this keyword.

<p style="color:red"> 
    this text is red 
       <em style="color:initial"> 
          this text is in the initial color (e.g. black)
       </em>                                          
    this is red again
 </p> 

One thing to be aware is that the initial value may not be the same across browsers. This is specially important when working with fonts as the default font is not necessarily the same in all browsers.

inherit

The inherit CSS value uses the value of the parent element’s property (defining the parent as the parent element in the document tree, even if it’s not the containing block). It is allowed on every CSS property.

In the following CSS fragment the h2 elements that are children of #sidebar will inherit the color of its parent. The stylesheet defines another container (div#current) with a different color.

/* make second-level headers green */
 h2 { color: green; }

 /* leave those in the sidebar alone so they use the parent's color */
 #sidebar h2 { color: inherit; }

div#current { color: blue; }

In the following HTML fragment the sidebar’s h2 element will be blue, the color of the parent.

<div id="current">
  <sidebar id="#sidebar">
    <h2>Sidebar Title</h2>
</sidebar>
</div>

For inherited properties, this reinforces the default behavior, and is only needed to override another rule. For non-inherited properties, the results may be unexpected and you may find that using initial, or unset on the all property works better.

unset

The unset CSS keyword is the combination of the initial and inherit keywords. This keyword resets the property to its inherited value if it inherits from its parent or to its initial value if not. In other words, it behaves like the inherit keyword in the first case and like the initial keyword in the second case.

Color is an inherited property so the following CSS:

.foo {
  color: blue;
}
.bar {
  color: green;
}

p {
  color: red;
}
.bar p {
  color: unset;
}

Produces the following colors in the HTML file below.

<p>This text is red</p>
<div class="foo">
  <p>This text is also red</p>
</div>
<div class="bar">
  <p>This text is green (default inherited value)</p>
</div>

revert

This property is only supported in Safari.

a

The revert keyword resets the cascade as if no stylesheets of the specified type were present. For example, if the revert keyword is used in an author stylesheet (the one we write as designers) then it’ll assume that no stylesheets are present and will take the value from the default user agent style sheet or any user stylesheet present.

The order of how the cascade works is show below in order of importance from left to right:

  user agent > user > author

There’s no perfect way to handle cascade idiosyncrasies reliably across browsers. The CSS Cascading and Inheritance Level 4 is an editor’s draft and, my guess, is that there will be significant work in the spec before it’s finalized.

For now these are the tools we have so let’s make the best use of them.

Digital books on the web

Rather than trying to come up with new strategies for taking ebooks forward (strategies we know are not going to work in the long run) I’ve been exploring processes and technologies to turn web content into publishable content. This will definitely rehash some older posts but, I hope, will provide a fresher perspective to the technologies since it’s been a few months since I last looked at them.

The web is getting close to native and that might not be a bad thing

In terms of performance and functionality the web has come closer and closer to native applications without plugins or having to install plugins or full blown applications. If you’re in Android the integration is full and very tight. Some of the things your web applications can do when properly configured:

  1. Add an icon to the homescreen after the user has interacted with the web app for a certain period of time
  2. Cache specific data for reliable and offline use
  3. Once the user accepts, send her push notifications
  4. Background updates

In the context of a book-like experience we’ll concentrate in #1, #2 and #4. And illustrate how they may be used to create a reading experience.

Enter the PWA technologies

Progressive Web Applications (progressive web apps or PWAs) are a set of Web APIS and conventions to make web applications work more like native applications in Android and iOS.

Add to homescreen

We have been able to add applications to the device’s homepage since early versions of the iPhone. What has changed is the automation of the process and what it takes for the browser to decide you’ve engaged with the application. Apps on the homescreen provide a good user experience and to do that they:

  • Should load instantly, regardless of network state. This isn’t to say that they need to function fully offline, but they must put their own UI on screen without requiring a network round trip.
  • Should be tied in the user’s mind to where they came from. The brand or site behind the app shouldn’t be a mystery.
  • Can run without extra browser chrome (e.g., the URL bar). This is a potentially dangerous permission. To prevent hijacking by captive portals (and worse), apps must be loaded over TLS connections.

These concerns give rise to today’s Baseline Criteria. To be a Progressive Web App, a site must:

  • Originate from a Secure Origin. Served over TLS and green padlock displays (no active mixed content).
  • Load while offline (even if only a custom offline page). By implication, this means that Progressive Web Apps require Service Workers.
  • Reference a Web App Manifest with at least the following properties:
    • name
    • short_name
    • start_url
    • display with a value of standalone or fullscreen
    • An icon at least 144×144 large in png format. E.g.: "icons": [ { "src": "/images/icon-144.png", "sizes": "144x144", "type": "image/png" } ]

Criteria taken from Alex Russell’s What, Exactly, Makes Something A Progressive Web App?

So if the user interacts with your site it will eventually prompt him to add the site to the homescreen and, depending on the OS you’re working on, be able to interact with it as a full fledged application.

Web Application Manifest

The Web Application Manifest is a JSON file that provides additional information for your application including names, different resolutions for the homescreen icon, a splash screen and a lot of the elements that make for an application. This will also work with Microsoft and iOS devices, not just Android.

The Web Application manifest uses a link like the one below to link to the manifest file.

<link rel="manifest" href="/manifest.json">

An example manifest from Paul Kinlan’s Air Horner looks like this.

{
  "name": "The Air Horner",
  "short_name": "Air Horner",
  "icons": [{
        "src": "images/touch/Airhorner_128.png",
        "type": "image/png",
        "sizes": "128x128"
      }, {
        "src": "images/touch/Airhorner_152.png",
        "type": "image/png",
        "sizes": "152x152"
      }, {
        "src": "images/touch/Airhorner_144.png",
        "type": "image/png",
        "sizes": "144x144"
      }, {
        "src": "images/touch/Airhorner_192.png",
        "type": "image/png",
        "sizes": "192x192"
      },
      {
        "src": "images/touch/Airhorner_256.png",
        "type": "image/png",
        "sizes": "256x256"
      },
      {
        "src": "images/touch/Airhorner_512.png",
        "type": "image/png",
        "sizes": "512x512"
      }],
  "start_url": "/?homescreen=1",
  "scope": "/",
  "display": "standalone",
  "orientation": "portrait",
  "background_color": "#2196F3",
  "theme_color": "#2196F3"
}

Service Worker

Before we jump into theory and code it’s a good idea to remember that service workers are not the first tool created to make content available offline. Google Gears attempted to do this first and failed. Some of the people who worked in the Gears project at Google attempted to create a web version of Gears’ offline storage and standardize it as Application Cache.

Application Cache failed to get traction because of its reliance in implicit behavior. Writing the App Cache manifest itself is straight forward. Using it becomes harder when ou have to wade over the many implicit rules that make App CAche work… as a result many people gave up because the pages would never display or would never update the content, regardless if you were online or not.

Jake Archibald wrote Application Cache is a Douchebag to document the problems he experience in a successful App Cache Deployment at Lynrd. It is illustrative of the problems developers experienced when deploying the API and the workarounds they had to make so that users had a moderately successful experience.

With the lessons of Application Cache fresh in mind the brains at the W3C started working on the next iteration of offline caching and performance improvement APIs. It is the Service Worker.

Where App. Cache works with a lot of implicit behavior and assumptions Service Worker forces you to be explicit in what you want to accomplish, whether it’s is to designate the resources to cache or intercepting a network request and providing an alternative resource if the user is offline and the resource not cached.

Having to explicitly code the behavior you want gives you a lot of flexibility when choosing what resources you want to cache and how you want to handle individual requests. In the index page of your website put the code below and replace the /sw.js with the name of your service worker that must be located in the root directory of your site or application.

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
      // registration failed :(
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

sw.js is the actual service worker script. The first part sets up the caches and the list of URLs to cache when the user first access the Service Worker controlled site. These resources are ones used in the index page and may include images, style sheets, scripts, fonts and others. Just make sure you don’t go overboard or you’ll defeat the purpose of precaching resources.

This is the place where you update the names of your caches to trigger the automatic update process. We’ll discuss this in more detail later.

// Names of the two caches used in this version of the service worker.
// Change to v2, etc. when you update any of the local resources, which will
// in turn trigger the install event again.
const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime-v1';

// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
  'index.html',
  './', // Alias for index.html
  'styles.css',
  '../../styles/main.css',
  'demo.js'
];

The first event we want to set up is the install event. In this event we precache the resources we defined at the top of the script. We then make the Service Worker take over the page and site immediately and not use the default behavior of waiting until the browser reloads the content.

// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(PRECACHE)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(self.skipWaiting())
  );
});

The activate handler is the maintenance and cleanup handler. Whenever we update the name of the cache constants at the top of the script, the activation process will delete those caches that are no longer need because the material has been updated.

// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', event => {
  const currentCaches = [PRECACHE, RUNTIME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
    }).then(cachesToDelete => {
      return Promise.all(cachesToDelete.map(cacheToDelete => {
        return caches.delete(cacheToDelete);
      }));
    }).then(() => self.clients.claim())
  );
});

The fetch event is the heart of the Service Worker. This is where we fetch resources for the application and interact with the user. In essence the fetch event does the following

  • If the request comes from a different domain skip it
  • If the item is in the cache, then return it
  • If the item is not in the cache, fetch it from the network and:
    • Store a copy of the object in the cache
    • Return the item to the use
self.addEventListener('fetch', event => {
  // Skip cross-origin requests, like those for Google Analytics.
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches.match(event.request).then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }

        return caches.open(RUNTIME).then(cache => {
          return fetch(event.request)
          .then(response => {
            // Put a copy of the response in the runtime cache.
            return cache.put(event.request, response.clone())
            .then(() => {
              return response;
            });
          });
        });
      })
    );
  }
});

This service worker is simple and provides a core set of functionality to work with Service Worker. We can do other things like provide an offline page if the content is not in the cache and the network is down and many other things that we explicitly code.

Push Messaging and Notification

These two APIs are used on top of a service worker to provide push notifications and one-to-many communication services for your app. Say, for example, that you want to notify users when new content is added to your publication or content is updated; you can use push messages to notify the user of these changes.

The code for Push messaging and notifications is very dependent on the backedn you choose for your project. A couple examples:

Why bother?

I know what you’re thinking: “This sounds like a lot of work, why should I bother?

The quickest answer is: because it helps users. It provides easier access to web content and it gives them a way to view the content while they have spotty or no internet connectivity.

Creatively it gives you, the developer, a way to create better and original content. Serialized content, check. Content that requires specific web APIs likeWebGL, check. The choices are limited by your imagination.

Links and resources

Custom HTML5 video playlist

Another idea is how to create playlists like those on Youtube but without having to code the entire interface from scratch. Dudley Storey, again, proposed a solution using CSS display: table and a little Javascript magic.

I’ve taken the layout from Storey’s article as is (I’m still learning about display: table and related CSS) and enhanced the Javascript with some of my working ideas. The two main constraints:

  • It must work without Javascript; The user must be able to view the videos when there Javascript is not available
  • It must work without the mouse using only keyboard

THe HTML uses a figure as the container for the playlist. The two children are a video element with the traditional sources. We make sure to leave the controls visible so people who choose to work with the standard video controls.

In the figcaption element we add links and images for the other videos available in the playlist.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Video Playlist</title>
    <link rel="stylesheet" href="styles/styles.css">

</head>
<body>
<figure id="video_player">
    <video controls poster="images/SAO-Ordinal-Scale-Trailer1.jpg" id="video">
        <source src="video/SAO-Ordinal-Scale-Trailer1.mp4" type="video/mp4">
        <source src="video/SAO-Ordinal-Scale-Trailer1.webm" type="video/webm">
    </video>
    <figcaption>
        <a href="video/SAO-Ordinal-Scale-Trailer2.mp4">
          <img src="images/SAO-Ordinal-Scale-Trailer2.jpg" alt="SAO Ordinal Scale Trailer 2"></a>
        <a href="video/SAO-Ordinal-Scale-Trailer3.mp4">
          <img src="images/SAO-Ordinal-Scale-Trailer3.jpg" alt="SAO Ordinal Scale Trailer 3"></a>
        <a href="video/SAO-Ordinal-Scale-Trailer4.mp4">
          <img src="images/SAO-Ordinal-Scale-Trailer4.jpg" alt="SAO Ordinal Scale Trailer 4"></a>
    </figcaption>
</figure>
<script src="scripts/script.js"></script>
</body>
</html>

Javascript

The first portion of the script is a shortcut. Rather than attach the same event to multiple elements manually we define the elements we want to attach the event to, in this case all the a elements inside figcaption and loop through them attacking the handler function to the onclick event for each link.

let anchors = document.querySelectorAll('figcaption a');
let links = [...anchors];

for (let i=0; i<links.length; i++) {
    links[i].onclick = handler;
}

Next we define the handler function that we’ve attached to the anchors in the page.

For each element we:

  1. Prevent the default click. We want this function to handle the click rather than the browser’s default link handling mechanism.
  2. Capture a reference to the link’s href attribute
  3. Extract the file name by creating a substring of the href attribute we generated in the prvious step
  4. Create a reference to the video element and remove the poster attribute. We don’t want the poster from the previous movie to overlap the new video
  5. We create a node list (not an array) of the source children elements
  6. Assign the mp4 version of the video to the first child and the webm version of the video to the second child source element
  7. Load the video
  8. Play
function handler(e) {
    e.preventDefault(); // 1
    let videotarget = this.getAttribute("href"); // 2
    let filename = videotarget.substr(0, videotarget.lastIndexOf('.')); // 3
    let video = document.getElementById("video"); // 4
    video.removeAttribute("poster"); // 4
    let source = document.querySelectorAll("#video_player video source"); // 5
    source[0].src = filename + ".mp4"; // 6
    source[1].src = filename + ".webm"; // 6
    video.load(); // 7
    video.play(); // 8
}

I’ve copied the keyboard event handler from another project to make sure we meet the keyboard accessibility requirement.

  1. If the user presses either the space bar or enter key then toggle playback, if the video is paused then play it and if it’s playing then pause it.
  2. If the user presses the left key then seek 5 seconds backwards on the video
  3. If the user presses the right key then seek 5 seconds forward on the video

For the arrow keys we use a utility function to seek the video.

// Event handler for keyboard navigation
window.addEventListener('keydown', (e) => {
    switch (e.which) {
        case 32:  // 1
        case 13:  // 1
            e.preventDefault();
            if (video.paused) {
                video.play();
            } else {
                video.pause();
            }
            break;

        case 37:  // 2
            skip(-5);
            break;

        case 39:  // 3
            skip(5);
            break;
    }

});

function skip(value) {
    video.currentTime += value;
}

CSS

The CSS uses display: table to layout the content in a way that is backwards compatible. The video (the #video_player element )is the main component of our ‘table’ layout and will take 2/3rd of the space while each of the video thumbnails (figcaption a elements) are stacked in the remaining width of the element.

This is similar to using a table but not quite the same: The table element in HTML is a semantic structure. The table value for display is an indication of how the content should be displayed and has nothing to do with the structure of the content like the table tag does

#video_player {
    display: table;
    line-height: 0;
    font-size: 0;
    background: #fff;
}
#video_player video,
#video_player figcaption {
    display: table-cell;
    vertical-align: top;
}
#video_player figcaption {
    width: 25%;
}
#video_player figcaption a {
    display: block;
    opacity: .5;
    transition: 1s opacity;
}
#video_player figcaption a img,
figure video {
    width: 100%;
    height: auto;
}
#video_player figcaption a:hover {
    opacity: 1;
}

One further refinement

Right now there is no way to navigate between videos and now way to play the first video again after you navigate to the thumbnails. It’ll require some additional Javascript like the one in this post by Dudley Storey.

Custom controls for HTML5 video

One of the cool things about HTML5 video is that you can fully customize it to suit your needs and preferences. Dudley Storey has an interesting demo and tutorial on how to add custom controls for a video. I will take his idea and take it in a different direction… Rather than attach the controls to the video I’ll create a control-panel like interface for a video.

When working in projects like this I create a minimal set of requirements. In this case the requirements are:

  • Video still works if Javascript is not available
  • Video is captioned and the captions can be toggled on and off
  • Video can be played through keyboard commands

Some additional enhancements and ideas are at the end of the post. Now let’s dive into code 🙂

HTML

The HTML code is pretty straightforward. The root element for the demo is a section element with a class of display-wrap; it’s purpose is to serve as the root of our flexbox layout.

The videoPlayer class div holds our video element. This is the core of the experiment with mp3 and webm versions of the video and an Eglish version of our vtt captions. These are the elements that we’ll manipulate with Javascript.

<section class='display-wrap'>
  <div class='videoPlayer'>
    <video id='video' controls poster='video/hololens.jpg'>

        <source
                src='video/hololens.mp4'
                type='video/mp4'>
        <source
                src='video/hololens.webm'
                type='video/webm'>

        <track kind='captions' src='video/hololens.en.vtt' srclang='en'
               label='English' id='english'>
    </video>
</div>

The last portion, the control-panel div holds the icons for the player controls. I wasn’t able to find an icon for both captions on and off so I left them as text links. This is also a flexbox layout.

<div class='control-panel'>
    <h2>Video Control Panel</h2>

    <div class='player-icons'>
        <a href='#' id='rewind'><img class='icon' src='images/icons/rewind.svg' alt='back'></a>
        <a href='#' id='play'><img class='icon' id='playIcon' src='images/icons/play-button.svg' alt='play'></a>
        <a href='#' id='myStop'><img class='icon' src='images/icons/stop.svg' alt='stop'></a>
        <a href='#' id='forward'><img class='icon' src='images/icons/fast-forward.svg' alt='forward'></a>
    </div>
    <div class='player-icons'>
        <!--<a href='#' id='reset'-->
        <a href='#' id='showCaptions'>Enable Captions</a>
        <a href='#' id='disableCaptions'>Disable Captions</a>
    </div>
  </div>
</section>

Javascript

The Javascript is very event driven and works by reacting to events that you’ve triggered. As usual, I’ll break the Javascript in sections and describe what each one does or any relevant thing about the code.

The first section holds place holders for all the objects that will afect the video player.

// Event Listeners For Play/Pause Button
let video = document.getElementById('video');

let play = document.getElementById('play');
let playIcon = document.getElementById('playIcon');

let myStop = document.getElementById('myStop');

let rewind = document.getElementById('rewind');
let forward = document.getElementById('forward');

let showCaptions = document.getElementById('showCaptions');
let hideCaptions = document.getElementById('hideCaptions');

// Remove native controls
video.removeAttribute('controls');

The second part is our keyboard navigation. Using a keydown event listener we intercept multiple keys using a switch statement.

If the key code is 32 (space bar) or 13 (enter) then we trigger the play sequence: if the video is not playing (represented by the pause state) then we play the video and changge the icon to the pause icon. if the video is playing then we pause it and change the icon to play.

If the key code is 37 (left arrow) then we seek back 30 second in the video.

If the key code is 39 (right arrow) then we seek forward 30 seconds in the video.

In my original code I was using keypress instead of keydown. For some reason the code for left and right arrows was not working. Trying to figure out why.

// Event handler for keyboard navigation
window.addEventListener('keydown', (e) => {
  switch (e.which) {
    case 32:
    case 13:
      e.preventDefault();
      if (video.paused) {
          video.play();
          playIcon.src = 'images/icons/pause.svg';
      } else {
          video.pause();
          playIcon.src = 'images/icons/play-button.svg';
      }
      break;

    case 37:
      skip(-30);
      break;

    case 39:
      skip(30);
      break;
  }

});

What happens when the user click the play button is similar to what happens when they press the space bar or enter key:

  • If the video is not playing (represented by the pause state) then we play the video and changge the icon to the pause icon
  • If the video is playing then we pause it and change the icon to play
// play handler
play.addEventListener('click', (e) => {
  // Prevent Default Click Action
  e.preventDefault();
  if (video.paused) {
      video.play();
      playIcon.src = 'images/icons/pause.svg';
  } else {
      video.pause();
      playIcon.src = 'images/icons/play-button.svg';
  }
});

Working with captions proved very difficult. At first I had done something like the commented code below. The first part worked properly but not the second one… so I had to break it into separate items

//  captions.addEventListener('click', (e) => {
//
//      e.preventDefault();
//      console.log(video.textTracks[0].mode);
//      if (video.textTracks[0].mode = 'showing') {
//          video.textTracks[0].mode = 'hidden';
//          console.log(video.textTracks[0].mode);
//      } else {
//          video.textTracks[0].mode = 'showing';
//          console.log(video.textTracks[0].mode);
//      }
//
//  })

Each of the links uses attributes from the VTT Text Track to show or hide the video where appropriate. I’m still looking at merging these two into a single event listener like I did for play but haven’t been able to find an easy way to do it.

// Show captions
showCaptions.addEventListener('click', (e) => {
 e.preventDefault();
 video.textTracks[0].mode = 'showing';
});

// Hide captions
disableCaptions.addEventListener('click', (e) => {
  e.preventDefault();
  video.textTracks[0].mode = 'hidden';
});

In this demo I’ve made a very important difference between play/pause and stop. Pause will keep the play head at the current location so clicking the button again will resume play without interruption.

The stop button will stop playback, change the play button icon to play (regardless of its previous status) and reset the playback to the beginning of the video.

// Stop and reset
myStop.addEventListener('click', (e) => {
  video.pause();
  playIcon.src = 'images/icons/play-button.svg';
  video.currentTime = 0;
  video.load();
});

The seeking functions both forwards and backwards using a small convenience function, skip to adjust the timeline forward or backwards by the specifiec ammount.

// Back 30
rewind.addEventListener('click', (e) => {
  skip(-30);
});

// Forward 30
forward.addEventListener('click', (e) => {
  skip(30);
});

function skip(value) {
  video.currentTime += value;
}

Most of the CSS deals with layout and making the video a responsive one. We first define our outer display as a flex container and give some basic styles to look like a separate unit… we also make the control panel take 1 portion of the flex layout.

.display-wrap {
    display: flex;
    flex-flow: row wrap;
}

.control-panel {
    border: 1px solid black;
    height: 20vh;
    flex: 1;
    padding: 1em;
}

The .videoPlayer classes make the video responsive and size it to be 16:9. This also takes 4 units in the flex layout.

.videoPlayer {
    position: relative;
    padding-bottom: 56.23%;
    /* Use 75% for 4:3 videos */
    height: 0;
    overflow: hidden;
    max-width: 100%;
    background: #000;
    margin: 5px;
    flex: 4;
}

.videoPlayer #video {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 100;
    background: transparent;
}

The last piece of CSS we use is to lyout the icons using flexbox and size each individual icon appropriate. Initially I used SVG icons but I could not get them to size like I wanted inside a flexbox grid so I reverted back to using PNG. I may revisit this using the icons as background images.

.player-icons {
    display: flex;
    flex-flow: row;
    justify-content: space-between;
}

.icon {
    border: 0;
    flex: 1;
    height: auto;
    width: 32px;
}

Changes, refinements and future ideas

The code in the page works fine for a single video in a page. In future iterations of the project I’d like to do a few things:

Convert this into a class for better reusability. One thing I’d like to change is moving the code into a class so I can instantiate one per video in a page. Some of the challenges are learning more about classes and how to instantiate event handlers from inside a class (if it’s possible at all).

Naming conventions for multiple videos. If using more than one video for the page then I need to come up with a convention for the IDs, ideally onne that would allow me to use string literal templates when triggering the events. This goes together with using classes.

Using background images instead of just regular icons. I really want to use SVG for the icons but was unable to use it. As part of a later iteration I want to explore using background images (inserted in the CSS code) instead of regular icons. This will make the code harder to read and may cause accessibility problems but it’s worth researching.