You know about Baseline, right? And you may have heard that the Chrome team made a web component for it.

Here it is!

Of course, we could simply drop the HTML component into the page. But I never know where we’re going to use something like this. The Almanac, obs. But I’m sure there are times where embedded it in other pages and posts makes sense.

That’s exactly what WordPress blocks are good for. We can take an already reusable component and make it repeatable when working in the WordPress editor. So that’s what I did! That component you see up there is the <baseline-status> web component formatted as a WordPress block. Let’s drop another one in just for kicks.

Pretty neat! I saw that Pawel Grzybek made an equivalent for Hugo. There’s an Astro equivalent, too. Because I’m fairly green with WordPress block development I thought I’d write a bit up on how it’s put together. There are still rough edges that I’d like to smooth out later, but this is a good enough point to share the basic idea.

Scaffolding the project

I used the @wordpress/create-block package to bootstrap and initialize the project. All that means is I cd‘d into the /wp-content/plugins directory from the command line and ran the install command to plop it all in there.

npm install @wordpress/create-block
Mac Finder window with the WordPress plugins directory open and showing the baseline-status plugin folder.
The command prompts you through the setup process to name the project and all that.

The baseline-status.php file is where the plugin is registered. And yes, it’s looks completely the same as it’s been for years, just not in a style.css file like it is for themes. The difference is that the create-block package does some lifting to register the widget so I don’t have to:

<?php
/**
 * Plugin Name:       Baseline Status
 * Plugin URI:        https://css-tricks.com
 * Description:       Displays current Baseline availability for web platform features.
 * Requires at least: 6.6
 * Requires PHP:      7.2
 * Version:           0.1.0
 * Author:            geoffgraham
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       baseline-status
 *
 * @package CssTricks
 */

if ( ! defined( 'ABSPATH' ) ) {
  exit; // Exit if accessed directly.
}

function csstricks_baseline_status_block_init() {
  register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'csstricks_baseline_status_block_init' );

?>

The real meat is in src directory.

Mac Finder window with the WordPress project's src folder open with seven files, two are highlighted in orange: edit.js and render.php.

The create-block package also did some filling of the blanks in the block-json file based on the onboarding process:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 2,
  "name": "css-tricks/baseline-status",
  "version": "0.1.0",
  "title": "Baseline Status",
  "category": "widgets",
  "icon": "chart-pie",
  "description": "Displays current Baseline availability for web platform features.",
  "example": {},
  "supports": {
    "html": false
  },
  "textdomain": "baseline-status",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "render": "file:./render.php",
  "viewScript": "file:./view.js"
}

Going off some tutorials published right here on CSS-Tricks, I knew that WordPress blocks render twice — once on the front end and once on the back end — and there’s a file for each one in the src folder:

  • render.php: Handles the front-end view
  • edit.js: Handles the back-end view

The front-end and back-end markup

Cool. I started with the <baseline-status> web component’s markup:

<script src="https://cdn.jsdelivr.net/npm/baseline-status@1.0.8/baseline-status.min.js" type="module"></script>
<baseline-status featureId="anchor-positioning"></baseline-status>

I’d hate to inject that <script> every time the block pops up, so I decided to enqueue the file conditionally based on the block being displayed on the page. This is happening in the main baseline-status.php file which I treated sorta the same way as a theme’s functions.php file. It’s just where helper functions go.

// ... same code as before

// Enqueue the minified script
function csstricks_enqueue_block_assets() {
  wp_enqueue_script(
    'baseline-status-widget-script',
    'https://cdn.jsdelivr.net/npm/baseline-status@1.0.4/baseline-status.min.js',
    array(),
    '1.0.4',
    true
  );
}
add_action( 'enqueue_block_assets', 'csstricks_enqueue_block_assets' );

// Adds the 'type="module"' attribute to the script
function csstricks_add_type_attribute($tag, $handle, $src) {
  if ( 'baseline-status-widget-script' === $handle ) {
    $tag = '<script type="module" src="' . esc_url( $src ) . '"></script>';
  }
  return $tag;
}
add_filter( 'script_loader_tag', 'csstricks_add_type_attribute', 10, 3 );

// Enqueues the scripts and styles for the back end
function csstricks_enqueue_block_editor_assets() {
  // Enqueues the scripts
  wp_enqueue_script(
    'baseline-status-widget-block',
    plugins_url( 'block.js', __FILE__ ),
    array( 'wp-blocks', 'wp-element', 'wp-editor' ),
    false,
  );

  // Enqueues the styles
  wp_enqueue_style(
    'baseline-status-widget-block-editor',
    plugins_url( 'style.css', __FILE__ ),
    array( 'wp-edit-blocks' ),
    false,
  );
}
add_action( 'enqueue_block_editor_assets', 'csstricks_enqueue_block_editor_assets' );

The final result bakes the script directly into the plugin so that it adheres to the WordPress Plugin Directory guidelines. If that wasn’t the case, I’d probably keep the hosted script intact because I’m completely uninterested in maintaining it. Oh, and that csstricks_add_type_attribute() function is to help import the file as an ES module. There’s a wp_enqueue_script_module() action available to hook into that should handle that, but I couldn’t get it to do the trick.

With that in hand, I can put the component’s markup into a template. The render.php file is where all the front-end goodness resides, so that’s where I dropped the markup:

<baseline-status
  <?php echo get_block_wrapper_attributes(); ?> 
  featureId="[FEATURE]">
</baseline-status>

That get_block_wrapper_attibutes() thing is recommended by the WordPress docs as a way to output all of a block’s information for debugging things, such as which features it ought to support.

[FEATURE]is a placeholder that will eventually tell the component which web platform to render information about. We may as well work on that now. I can register attributes for the component in block.json:

"attributes": { "showBaselineStatus": {
  "featureID": {
  "type": "string"
  }
},

Now we can update the markup in render.php to echo the featureID when it’s been established.

<baseline-status
  <?php echo get_block_wrapper_attributes(); ?> 
  featureId="<?php echo esc_html( $featureID ); ?>">
</baseline-status>

There will be more edits to that markup a little later. But first, I need to put the markup in the edit.js file so that the component renders in the WordPress editor when adding it to the page.

<baseline-status { ...useBlockProps() } featureId={ featureID }></baseline-status>

useBlockProps is the JavaScript equivalent of get_block_wrapper_attibutes() and can be good for debugging on the back end.

At this point, the block is fully rendered on the page when dropped in! The problems are:

  • It’s not passing in the feature I want to display.
  • It’s not editable.

I’ll work on the latter first. That way, I can simply plug the right variable in there once everything’s been hooked up.

Block settings

One of the nicer aspects of WordPress DX is that we have direct access to the same controls that WordPress uses for its own blocks. We import them and extend them where needed.

I started by importing the stuff in edit.js:

import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import './editor.scss';

This gives me a few handy things:

  • InspectorControls are good for debugging.
  • useBlockProps are what can be debugged.
  • PanelBody is the main wrapper for the block settings.
  • TextControl is the field I want to pass into the markup where [FEATURE] currently is.
  • editor.scss provides styles for the controls.

Before I get to the controls, there’s an Edit function needed to use as a wrapper for all the work:

export default function Edit( { attributes, setAttributes } ) {
  // Controls
}

First is InspectorTools and the PanelBody:

export default function Edit( { attributes, setAttributes } ) {
  // React components need a parent element
  <>
    <InspectorControls>
      <PanelBody title={ __( 'Settings', 'baseline-status' ) }>
      // Controls
      </PanelBody>
    </InspectorControls>
  </>
}

Then it’s time for the actual text input control. I really had to lean on this introductory tutorial on block development for the following code, notably this section.

export default function Edit( { attributes, setAttributes } ) {
  <>
    <InspectorControls>
      <PanelBody title={ __( 'Settings', 'baseline-status' ) }>
        // Controls
        <TextControl
          label={ __(
            'Feature', // Input label
            'baseline-status'
          ) }
          value={ featureID || '' }
          onChange={ ( value ) =>
            setAttributes( { featureID: value } )
          }
        />
     </PanelBody>
    </InspectorControls>
  </>
}

Tie it all together

At this point, I have:

  • The front-end view
  • The back-end view
  • Block settings with a text input
  • All the logic for handling state

Oh yeah! Can’t forget to define the featureID variable because that’s what populates in the component’s markup. Back in edit.js:

const { featureID } = attributes;

In short: The feature’s ID is what constitutes the block’s attributes. Now I need to register that attribute so the block recognizes it. Back in block.json in a new section:

"attributes": {
  "featureID": {
    "type": "string"
  }
},

Pretty straightforward, I think. Just a single text field that’s a string. It’s at this time that I can finally wire it up to the front-end markup in render.php:

<baseline-status
  <?php echo get_block_wrapper_attributes(); ?>
  featureId="<?php echo esc_html( $featureID ); ?>">
</baseline-status>

Styling the component

I struggled with this more than I care to admit. I’ve dabbled with styling the Shadow DOM but only academically, so to speak. This is the first time I’ve attempted to style a web component with Shadow DOM parts on something being used in production.

If you’re new to Shadow DOM, the basic idea is that it prevents styles and scripts from “leaking” in or out of the component. This is a big selling point of web components because it’s so darn easy to drop them into any project and have them “just” work.

But how do you style a third-party web component? It depends on how the developer sets things up because there are ways to allow styles to “pierce” through the Shadow DOM. Ollie Williams wrote “Styling in the Shadow DOM With CSS Shadow Parts” for us a while back and it was super helpful in pointing me in the right direction. Chris has one, too.

A few other more articles I used:

First off, I knew I could select the <baseline-status> element directly without any classes, IDs, or other attributes:

baseline-status {
  /* Styles! */
}

I peeked at the script’s source code to see what I was working with. I had a few light styles I could use right away on the type selector:

baseline-status {
  background: #000;
  border: solid 5px #f8a100;
  border-radius: 8px;
  color: #fff;
  display: block;
  margin-block-end: 1.5em;
  padding: .5em;
}

I noticed a CSS color variable in the source code that I could use in place of hard-coded values, so I redefined them and set them where needed:

baseline-status {
  --color-text: #fff;
  --color-outline: var(--orange);

  border: solid 5px var(--color-outline);
  border-radius: 8px;
  color: var(--color-text);
  display: block;
  margin-block-end: var(--gap);
  padding: calc(var(--gap) / 4);
}

Now for a tricky part. The component’s markup looks close to this in the DOM when fully rendered:

<baseline-status class="wp-block-css-tricks-baseline-status" featureid="anchor-positioning"></baseline-status>
<h1>Anchor positioning</h1>
<details>
  <summary aria-label="Baseline: Limited availability. Supported in Chrome: yes. Supported in Edge: yes. Supported in Firefox: no. Supported in Safari: no.">
    <baseline-icon aria-hidden="true" support="limited"></baseline-icon>
    <div class="baseline-status-title" aria-hidden="true">
      <div>Limited availability</div>
        <div class="baseline-status-browsers">
        <!-- Browser icons -->
        </div>
    </div>
  </summary><p>This feature is not Baseline because it does not work in some of the most widely-used browsers.</p><p><a href="https://github.com/web-platform-dx/web-features/blob/main/features/anchor-positioning.yml">Learn more</a></p></details>
<baseline-status class="wp-block-css-tricks-baseline-status" featureid="anchor-positioning"></baseline-status>

I wanted to play with the idea of hiding the <h1> element in some contexts but thought twice about it because not displaying the title only really works for Almanac content when you’re on the page for the same feature as what’s rendered in the component. Any other context and the heading is a “need” for providing context as far as what feature we’re looking at. Maybe that can be a future enhancement where the heading can be toggled on and off.

Voilà

Get the plugin!

This is freely available in the WordPress Plugin Directory as of today! This is my very first plugin I’ve submitted to WordPress on my own behalf, so this is really exciting for me!

Future improvements

This is far from fully baked but definitely gets the job done for now. In the future it’d be nice if this thing could do a few more things:

  • Live update: The widget does not update on the back end until the page refreshes. I’d love to see the final rendering before hitting Publish on something. I got it where typing into the text input is instantly reflected on the back end. It’s just that the component doesn’t re-render to show the update.
  • Variations: As in “large” and “small”.
  • Heading: Toggle to hide or show, depending on where the block is used.

Baseline Status in a WordPress Block originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.