Home >Web Front-end >CSS Tutorial >HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier!

HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier!

Jennifer Aniston
Jennifer AnistonOriginal
2025-03-08 09:44:12934browse

HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier!

Jeremy Keith's insightful article last year introduced me to HTML Web Components, a revelation that changed my approach to web development. His key point resonated deeply: While you could achieve the same results with DOM manipulation and event handling, web components offer a more robust, portable, and maintainable solution, adhering to the single responsibility principle.

My initial misconception was that all web components relied solely on JavaScript and Shadow DOM. While this is possible, a superior approach, especially for progressive enhancement advocates, leverages the inherent HTML capabilities of web components. They are, fundamentally, HTML. Andy Bell's recent work provides further context on progressive enhancement (though outside the scope of this article).

Let's explore three examples showcasing key features: CSS encapsulation and progressive enhancement possibilities, without mandatory JavaScript dependency for basic functionality. JavaScript will be used to enhance the experience, but core functionality remains intact even without it. These examples, along with their source code, are available in my Web UI Boilerplate component library (Storybook) on GitHub.

Example 1: <webui-disclosure></webui-disclosure>

Inspired by Chris Ferdinandi's tutorial on building web components from scratch using a disclosure pattern, this example expands on his demo.

The core is HTML. Web components allow custom element names; here, <webui-disclosure></webui-disclosure> contains a button to show/hide text within a <div>.</div>

<webui-disclosure data-bind-click-outside="" data-bind-escape-key="">
  Show / Hide
  <div data-content="">
    <p>Content to be shown/hidden.</p>
  </div>
</webui-disclosure>

With JavaScript disabled, the button (hidden via the hidden attribute) is invisible, and the content is displayed. This demonstrates simple progressive enhancement. The content is accessible regardless of JavaScript.

This extends Ferdinandi's demo by adding closure via ESC key or clicks outside the element (using data-attributes). The custom element is defined:

customElements.define('webui-disclosure', WebUIDisclosure);

Custom element names use dashed-ident (e.g., <my-component></my-component>). While the dash typically separates words, it's not strictly required.

Using TypeScript is beneficial for error prevention, but for simplicity, the JavaScript ES Module structure is:

class WebUIDisclosure extends HTMLElement {
  constructor() {
    super();
    this.trigger = this.querySelector('[data-trigger]');
    this.content = this.querySelector('[data-content]');
    this.bindEscapeKey = this.hasAttribute('data-bind-escape-key');
    this.bindClickOutside = this.hasAttribute('data-bind-click-outside');

    if (!this.trigger || !this.content) return;

    this.setupA11y();
    this.trigger?.addEventListener('click', this);
  }

  setupA11y() {
    // Add ARIA props/state to button.
  }

  handleEvent(e) {
    // 1. Toggle visibility of content.
    // 2. Toggle ARIA expanded state on button.
  }

  connectedCallback() {
    document.addEventListener('keyup', (e) => {
      // Handle ESC key.
    });

    document.addEventListener('click', (e) => {
      // Handle clicking outside.
    });
  }

  disconnectedCallback() {
    // Remove event listeners.
  }
}

Event listeners are handled in constructor() and connectedCallback() (as explained by Hawk Ticehurst). While not essential for basic functionality, the JavaScript enhances UX and accessibility (adding aria-expanded and aria-controls). This showcases progressive enhancement. No additional CSS is needed; styling is inherited.

Example 2: <webui-tabs></webui-tabs>

This example highlights CSS encapsulation and progressive enhancement in a tabbed component.

The HTML structure:

<webui-tabs>
  <div data-tablist="">
    <a data-tab="" href="https://www.php.cn/link/7426f79c9a7f5af0a6cc457b2a7fb195">Tab 1</a>
    <a data-tab="" href="https://www.php.cn/link/60430f4a984aa0a534e027339a7580a7">Tab 2</a>
    <a data-tab="" href="https://www.php.cn/link/9d4f684ba088d28ad1c2ae7d0aee496a">Tab 3</a>
  </div>

  <div data-tabpanel="">
    <p>1 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>

  <div data-tabpanel="">
    <p>2 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>

  <div data-tabpanel="">
    <p>3 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>
</webui-tabs>

CSS is encapsulated:

webui-tabs {
  [data-tablist] {
    /* Default styles without JavaScript */
  }

  [data-tab] {
    /* Default styles without JavaScript */
  }

  [role='tablist'] {
    /* Style role added by JavaScript */
  }

  [role='tab'] {
    /* Style role added by JavaScript */
  }

  [role='tabpanel'] {
    /* Style role added by JavaScript */
  }
}

Default styles ([data-tablist], [data-tab]) are applied regardless of JavaScript. Styles with role attributes are added only when JavaScript is enabled, providing progressive enhancement. Styles are scoped to <webui-tabs></webui-tabs>, preventing conflicts. Simple descendant selectors can replace complex methodologies like BEM.

Stylesheets can be imported via JavaScript (only consumed if JavaScript is available):

import styles from './styles.css';

class WebUITabs extends HTMLElement {
  constructor() {
    super();
    this.adoptedStyleSheets = [styles];
  }
}

customElements.define('webui-tabs', WebUITabs);

Alternatively, inline styles can be injected using Shadow DOM:

class WebUITabs extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
       <!-- styles go here -->
    `;
  }
}

customElements.define('webui-tabs', WebUITabs);

The "Light" DOM (content between component tags) inherits global styles. The Shadow DOM requires internal styles. Dave Rupert's article clarifies how external styles interact with the Shadow DOM.

Progressive enhancement is achieved with JavaScript:

class WebUITabs extends HTMLElement {
  constructor() {
    super();
    // ... (querySelector, etc.) ...
    this.createTabs();
    this.tabTriggers.forEach((tabTrigger, index) => {
      tabTrigger.addEventListener('click', (e) => { this.bindClickEvent(e); });
      tabTrigger.addEventListener('keydown', (e) => { this.bindKeyboardEvent(e, index); });
    });
  }

  createTabs() {
    // 1. Hide all tabpanels initially.
    // 2. Add ARIA props/state to tabs & tabpanels.
  }

  bindClickEvent(e) {
    e.preventDefault();
    // Show clicked tab and update ARIA props/state.
  }
  bindKeyboardEvent(e, index) {
    e.preventDefault();
    // Handle keyboard ARROW/HOME/END keys.
  }
}

customElements.define('webui-tabs', WebUITabs);

JavaScript adds ARIA roles and keyboard navigation, enhancing accessibility. Without JavaScript, the default behavior (links to panels) remains accessible.

Example 3: &lt;webui-ajax-loader&gt;&lt;/webui-ajax-loader&gt;

This example, entirely generated by JavaScript using Shadow DOM, is only rendered when JavaScript is enabled. It's used to indicate loading states for Ajax requests.

The HTML is simply:

&lt;webui-ajax-loader&gt;&lt;/webui-ajax-loader&gt;

The JavaScript:

class WebUIAjaxLoader extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <svg part="svg" role="img"><title>loading</title>
<circle cx="50" cy="50" r="47"></circle></svg>
    `;
  }
}

customElements.define('webui-ajax-loader', WebUIAjaxLoader);

The part attribute allows styling the SVG from outside the component using the ::part pseudo-selector:

webui-ajax-loader::part(svg) {
  // Shadow DOM styles for the SVG...
}

CSS custom properties can be used:

<webui-disclosure data-bind-click-outside="" data-bind-escape-key="">
  Show / Hide
  <div data-content="">
    <p>Content to be shown/hidden.</p>
  </div>
</webui-disclosure>

Progressive enhancement is inherent; the loader only appears when JavaScript is supported.

Conclusion

HTML Web Components prioritize HTML, using JavaScript only when needed for enhanced features and encapsulation. They provide a powerful approach to building robust, maintainable, and accessible web applications.

The above is the detailed content of HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier!. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn