Customize header styles page

Learn how to use CSS properties to customize the appearance within Shadow DOM.

Overview

In this part we will:

  • Use CSS custom properties to allow users of our component to modify the component’s encapsulated styles.

Problem

We want the users of our component to be able to customize the styling. For example we think they should be able to change the header’s background-color and the heading's text-transform.

Open up the styling for this component by providing supported CSS custom properties. In the end a custom header should look something like this:

How to Solve This Problem

  1. Style the bus-tracker using custom properties in the global CSS.
  2. Change the bus-tracker component’s own CSS to use those properties in the header and header h1 elements.

Technical requirements

Use the following CSS property names and values:

  • --header-background - Pick any color you want. I’ll use salmon.
  • --header-text-transform - Use capitalize.

What you need to know

  • How to define CSS custom properties.
  • How to use CSS custom properties to set the value of styles.
  • How to pass CSS custom properties into custom elements.

Defining custom properties

Custom properties start with two hyphens --. Otherwise they can have any name, but usually use hyphens to separate each word, ex: --this-class-name. Defining custom properties works the same way as other style properties, use a colon to delimit the name and the value.

This example sets a default border style on the root (html) element:

:root {
  --border: 1px solid DarkBlue;
}

Using var()

The var() syntax is a way to get a value from a variable (custom property). Using var(--color, blue) says: Use the --color variable if it is defined, otherwise fallback to blue. This gives us what we need to both have a custom background-color / text-transform, and also allow the consumer to override these styles.

:root {
  --theme-alt: MidnightBlue;
}

.button {
  color: var(--theme-alt, DarkBlue);
}

Passing custom properties

You can pass custom properties to custom elements by defining the property on a CSS selector matching the element.

bus-tracker {
  --header-background: salmon;
}

Solution

✏️ Add a bus-tracker { } section to the CSS and set --header-background and --header-text-transform CSS properties. Using var(), apply these styles in the appropriate place within the Shadow DOM CSS.

Click to see the solution

<style>
  html,
  body {
    height: 100%;
  }
  body {
    font-family: "Catamaran", sans-serif;
    background-color: #f2f2f2;
    display: flex;
    flex-direction: column;
    margin: 0;
  }
  bus-tracker {
    --header-background: salmon;
    --header-text-transform: capitalize;
  }
</style>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyD7POAQA-i16Vws48h4yRFVGBZzIExOAJI"></script>

<bus-tracker></bus-tracker>

<template id="bt-template">
  <style>
    :host {
      display: flex;
      flex-direction: column;
    }

    .top {
      flex-grow: 1;
      overflow-y: auto;
      height: 10%;
      display: flex;
      flex-direction: column;
    }

    footer {
      height: 250px;
      position: relative;
    }
    .gmap {
      width: 100%;
      height: 250px;
      background-color: grey;
    }

    header {
      box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.1);
      background-color: var(--header-background, #313131);
      color: white;
      min-height: 60px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      line-height: 1.2;
    }

    header h1 {
      text-align: center;
      font-size: 18px;
      text-transform: var(--header-text-transform, uppercase);
      letter-spacing: 1px;
      margin: 0;
    }
    #selected-route:not(.route-selected) {
      display: none;
    }
    .route-selected {
      line-height: 1;
      position: absolute;
      z-index: 1;
      text-align: right;
      background: rgba(6, 6, 6, 0.6);
      top: 10px;
      right: 10px;
      padding: 6px 10px;
      color: white;
      border-radius: 2px;
      cursor: pointer;
    }
    .route-selected small {
      display: block;
      font-size: 14px;
      color: #ddd;
    }
    .route-selected .error-message {
      font-size: 14px;
      background-color: #ff5722;
      border-radius: 10px;
      padding: 4px 8px 1px;
      margin-top: 5px;
    }
    .routes-list {
      padding: 20px 0;
      margin: 0;
      overflow-y: auto;
    }
    .routes-list li {
      list-style: none;
      cursor: pointer;
      background: white;
      border: 1px solid #dedede;
      margin: 1% 2%;
      border-radius: 25px;
      color: #2196f3;
      width: 41%;
      display: inline-flex;
      font-size: 14px;
      line-height: 1.2;
    }
    .routes-list li:hover {
      border-color: transparent;
      background-color: #008eff;
      color: white;
      box-shadow: 0px 5px 20px 0px rgba(0, 0, 0, 0.2);
    }
    .routes-list li .check {
      display: none;
    }
    .routes-list li.active {
      color: #666;
      background-color: #e8e8e8;
    }
    .routes-list li.active .check {
      display: inline-block;
      margin-left: 5px;
      color: #2cc532;
    }
    .routes-list li.active:hover {
      border-color: #dedede;
      box-shadow: none;
    }
    .routes-list button {
      width: 100%;
      padding: 8px 8px 6px;
      border: none;
      border-radius: 25px;
      background: transparent;
      text-align: left;
      font: inherit;
      color: inherit;
    }
    .route-number {
      display: inline-block;
      border-right: 1px solid #dedede;
      padding-right: 5px;
      margin-right: 5px;
      min-width: 18px;
      text-align: right;
    }
    p {
      text-align: center;
      margin: 0;
      color: #ccc;
      font-size: 14px;
    }
  </style>
  <div class="top">
    <header>
      <h1>Chicago CTA Bus Tracker</h1>
      <p id="loading-routes">Loading routes…</p>
    </header>

    <ul class="routes-list"></ul>
  </div>
  <footer>
    <button id="selected-route" type="button">
    </button>

    <google-map-view></google-map-view>
  </footer>
</template>
<template id="error-template">
  <div class="error-message">
    No vehicles available for this route
  </div>
</template>
<template id="gmap-template">
  <style>
    .gmap {
      width: 100%;
      height: 250px;
      background-color: grey;
    }
  </style>
  <div class="gmap"></div>
</template>
<template id="route-template">
  <li>
    <button type="button">
      <span class="route-number"></span>
      <span class="route-name"></span>
      <span class="check">✔</span>
    </button>
  </li>
</template>
<script type="module">
const template = document.querySelector('#gmap-template');

class GoogleMapView extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    let nodes = document.importNode(template.content, true);
    this.shadowRoot.append(nodes);

    this.markers = null;
    this._vehicles = null;
  }

  connectedCallback() {
    let gmap = this.shadowRoot.querySelector('.gmap');
    this.map = new google.maps.Map(gmap, {
      zoom: 10,
      center: {
        lat: 41.881,
        lng: -87.623
      }
    });
  }

  get vehicles() {
    return this._vehicles;
  }

  set vehicles(newVehicles) {
    this._vehicles = newVehicles;
    if (this.markers) {
      for(let marker of this.markers) {
        marker.setMap(null);
      }
      this.markers = null;
    }
    if (newVehicles) {
      this.markers = newVehicles.map(vehicle => {
        return new google.maps.Marker({
          position: {
            lat: parseFloat(vehicle.lat),
            lng: parseFloat(vehicle.lon)
          },
          map: this.map
        });
      });
    }
  }
}

customElements.define('google-map-view', GoogleMapView);

const apiRoot = "https://cta-bustracker.vercel.app/api/";
const getRoutesEndpoint = apiRoot + "routes";
const getVehiclesEndpoint = apiRoot + "vehicles";

const btTemplate = document.querySelector('#bt-template');
const routeTemplate = document.querySelector('#route-template');
const errorTemplate = document.querySelector('#error-template');

class BusTracker extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    let frag = document.importNode(btTemplate.content, true);
    this.shadowRoot.append(frag);

    this.routesList = this.shadowRoot.querySelector('.routes-list');
    this.selectedRouteBtn = this.shadowRoot.querySelector('#selected-route');
    this.googleMapView = this.shadowRoot.querySelector('google-map-view');
  }

  connectedCallback() {
    this.getRoutes();
  }

  async getRoutes() {
    let response = await fetch(getRoutesEndpoint);
    let data = await response.json();
    let routes = data["bustime-response"].routes;

    for(let route of routes) {
      let frag = document.importNode(routeTemplate.content, true);

      frag.querySelector('.route-number').textContent = route.rt;
      frag.querySelector('.route-name').textContent = route.rtnm;

      frag.querySelector('button').addEventListener('click', ev => {
        this.pickRoute(route, ev.currentTarget.parentNode);
      });

      this.routesList.append(frag);
    }

    this.shadowRoot.querySelector('#loading-routes').remove();
  }

  async getVehicles(route) {
    let response = await fetch(getVehiclesEndpoint + '?rt=' + route.rt);
    let data = await response.json();

    this.selectedRouteBtn.innerHTML = `
      <small>Route ${this.route.rt}:</small> ${this.route.rtnm}
    `;

    if (data['bustime-response'].error) {
      let frag = document.importNode(errorTemplate.content, true);
      this.selectedRouteBtn.append(frag);
      this.googleMapView.vehicles = [];
    } else {
      let vehicles = data['bustime-response'].vehicle;
      this.googleMapView.vehicles = vehicles;
    }

    this.selectedRouteBtn.classList.add('route-selected');
  }

  pickRoute(route, li) {
    this.route = route;
    this.getVehicles(route);

    if(this.activeRoute) {
      this.activeRoute.classList.remove('active');
    }
    this.activeRoute = li;
    this.activeRoute.classList.add('active');
  }
}

customElements.define("bus-tracker", BusTracker);
</script>