Building tabs in Web Components

 

Using Web Components as part of a Design System

Dave 🧱

Developer Advocate at zeroheight

more…

"We build products that redefine healthcare"

Nordhealth

Nord Design System

  • Tokens
  • Framework
  • Fonts
  • Themes
  • Components
  • Icons
  • Toolkit
  • Documentation
  • Current landscape
  • Semantics
  • Accessibility
  • Design and presentation
  • Pitfalls
  • Ideal structure

Research & experimentation

Inclusive Components – Tabbed Interfaces

inclusive-components.design/tabbed-interfaces/

CSS Tricks – Spicy Sections (ft. Dave Rupert)

css-tricks.com/spicy-sections/

Zach Leatherman – Seven minute tabs

github.com/zachleat/seven-minute-tabs

Open UI – Tab Component Specification

open-ui.org/components/tabs

  • The use of tab and tablist roles
  • Labelling tablist using aria-label
  • The use of aria-controls and aria-labelledby
  • tabindex for guiding focus between elements

Research takeaways

<div class="tabs">
  <div role="tablist" aria-label="Tabs">
    <button id="tab-1" role="tab" aria-selected="true" aria-controls="tabpanel-1" tabindex="0">
      Tab 1
    </button>
    <button id="tab-2" role="tab" aria-selected="false" aria-controls="tabpanel-2" tabindex="-1">
      Tab 2
    </button>
    <button id="tab-3" role="tab" aria-selected="false" aria-controls="tabpanel-3" tabindex="-1">
      Tab 3
    </button>
    <button id="tab-4" role="tab" aria-selected="false" aria-controls="tabpanel-4" tabindex="-1">
      Tab 4
    </button>
  </div>

  <div id="tabpanel-1" role="tabpanel" aria-hidden="false" aria-labelledby="tab-1" tabindex="0">
    Tab panel 1
  </div>
  <div id="tabpanel-2" role="tabpanel" aria-hidden="true" aria-labelledby="tab-2" tabindex="0">
    Tab panel 2
  </div>
  <div id="tabpanel-3" role="tabpanel" aria-hidden="true" aria-labelledby="tab-3" tabindex="0">
    Tab panel 3
  </div>
  <div id="tabpanel-4" role="tabpanel" aria-hidden="true" aria-labelledby="tab-4" tabindex="0">
    Tab panel 4
  </div>
</div>
<nord-tab-group label="Title">
  <nord-tab slot="tab">Tab 1</nord-tab>
  <nord-tab-panel>
    Tab panel 1
  </nord-tab-panel>
  <nord-tab slot="tab">Tab 2</nord-tab>
  <nord-tab-panel>
    Tab panel 2
  </nord-tab-panel>
  <nord-tab slot="tab">Tab 3</nord-tab>
  <nord-tab-panel>
    Tab panel 3
  </nord-tab-panel>
  <nord-tab slot="tab">Tab 4</nord-tab>
  <nord-tab-panel>
    Tab panel 4
  </nord-tab-panel>
</nord-tab-group>
import {html, css, LitElement} from 'lit';

export class SimpleGreeting extends LitElement {
  static styles = css`p { color: blue }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'Somebody';
  }

  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}
customElements.define('simple-greeting', SimpleGreeting);
import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  static styles = css`p { color: blue }`;

  @property()
  name = 'Somebody';

  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}
  • Mouse & touch control
  • Keyboard control
  • Programmatic control

Controls & interaction

private updateSelectedTab(selectedTab: Tab) {
  const selectedPanel = this.querySelector(`#${selectedTab.getAttribute("aria-controls")}`)

  if (selectedTab === this.selectedTab) return

  /**
   * Reset all the selected state of the tabs, and select the clicked tab
   */
  this.querySelectorAll("nord-tab").forEach(tab => {
    tab.removeAttribute("selected")
    if (tab === selectedTab) {
      tab.setAttribute("selected", "")
      tab.focus()
      tab.scrollIntoView({ block: "nearest", inline: "nearest" })
      this.selectedTab = tab
    }
  })

  /**
   * Reset all the visibility of the panels,
   * and show the panel related to the selected tab
   */
  this.querySelectorAll("nord-tab-panel").forEach(panel => {
    panel.setAttribute("aria-hidden", `${panel !== selectedPanel}`)
  })
}

Mouse control

switch (event.key) {
    case "ArrowLeft":
        updateTab(this.direction.isLTR ? previousTab : nextTab, event)
        break

    case "ArrowRight":
        updateTab(this.direction.isLTR ? nextTab : previousTab, event)
        break

    case "Home":
        updateTab(firstTab, event)
        break

    case "End":
        updateTab(lastTab, event)
        break

    default:
        break
}

Keyboard control

<nord-tab-group label="Title">
  <nord-tab slot="tab">Tab 1</nord-tab>
  <nord-tab-panel>
    Tab panel 1
  </nord-tab-panel>
  <nord-tab slot="tab" selected>Tab 2</nord-tab>
  <nord-tab-panel>
    Tab panel 2
  </nord-tab-panel>
</nord-tab-group>

Programmatic control

mutations.forEach(mutation => {
    if (mutation.attributeName === "selected" && mutation.oldValue === null) {
        const selectedTab = mutation.target
        this.observer.disconnect()
        this.updateSelectedTab(selectedTab)
        this.observer.observe(this, TabGroup.observerOptions)
    }
})

Louis Lazaris​ – Getting To Know The MutationObserver API

smashingmagazine.com/2019/04/mutationobserver-api-guide/

CSS and UI details

Adjusting for font weights

<div class="n-tab" data-text="The tab label">
    The tab label
</div>
.n-tab::before {
  content: attr(data-text);
  font-weight: var(--n-font-weight-active);
  display: block;
  block-size: 0;
  visibility: hidden;
}

Tab overflow shadows

David Darnes – Building tabs in Web Components

darn.es/building-tabs-in-web-components/

Lea Verou – Pure CSS scrolling shadows​

lea.verou.me/2012/04/background-attachment-local/

Thank You!

darn.es

mastodon.design/@daviddarnes

Made with Slides.com