DEV Community

Cover image for HTML Custom Element as a Feature API
Taylor Beseda for Begin

Posted on • Originally published at

HTML Custom Element as a Feature API

Say you've found a helpful front-end library you'd like to use for a feature in your web app, but you aren't thrilled with the interface.
It requires an awkward data structure or tedious markup.
Let's use an Enhance custom element definition to use as an adapter.

JavaScript-less Charts with charts.css

For this guide, I'll make use of a slick library called “charts.css” — it's a pure CSS way to turn tabular data into really nice charts.

Let's start with the end in mind. Here's the result we'd like to target using just HTML + charts.css:

Be sure to open and scroll through the HTML code here.
That's a lot of markup! And a lot of repetition.
But thankfully, it's just a standard HTML <table> with a few special classes and some CSS variable declarations mixed inline.

Data structure

Our 2016 Olympics medal data isn't likely in an HTML-ready format, so let's assume it comes from a database or API as an array of objects.
We can return that set from our Enhance API route:

export function get(req) {
  const medals = [
      label: 'USA',
      values: [46, 37, 38],
      label: 'GBR',
      values: [27, 23, 17],
      label: 'CHN',
      values: [26, 18, 26],

  return { json: { medals } }
Enter fullscreen mode Exit fullscreen mode


Again, we'll start with the goal in mind and author the desired code we want to write for the above chart (CodePen example):

  heading="2016 Summer Olympics Medal Table"
Enter fullscreen mode Exit fullscreen mode

Super simple interface and even follows charts.css conventions without all the markup.
Here is the Enhance element definition where I've implemented the above interface:

export default function MyChart({ html, state }) {
  const { attrs, store } = state

  const config = {
    dataKey: attrs['data-key'],
    type: attrs.type || 'bar',
    heading: attrs.heading || null,
    valueKey: attrs['value-key'],
    valueNames: attrs['value-names']?.split(',') || [],
    multiple: typeof attrs.multiple === 'string',
    showLabels: typeof attrs['show-labels'] === 'string',

  const data = store.chartData[config.dataKey]
  const allClasses = [
    config.multiple ? 'multiple' : null,
    config.showLabels ? 'show-labels' : null,

  return html`
    <table class="${allClasses.join(' ')}">
          <th scope="col">${config.valueKey}</th>
          ${ =>
            `<th scope="col">${v}</th>`
        ${ => `
            <th scope="row"> ${d.label} </th>
            ${, i) => {
              const style = [
                `--start: calc(${v}/100);`,
                `--size: calc(${v}/100);`,
                d.colors?.at(i) ? `--color: ${}` : null,
              return `<td style="${style.join(' ')}">${v}</td>`
Enter fullscreen mode Exit fullscreen mode

At its core, it's a function to loop over the medals data structure to generate <tr> elements for charts.css.
The applied classes are informed by the element attributes added to the <my-chart> element.

Whenever we use the <my-chart> element in our app, this definition will be used by Enhance to render our custom element.
Bonus: this custom element ships zero browser JavaScript!

Not only have we vastly improved the experience of creating new charts in our application, but we've centralized this adapter.
We can iterate on it over time when we want to add features or when the shape of the data inevitably changes.

Top comments (0)