Writing custom element metadata: A simple component

HTML-Validate is fully data-driven and has no hardcoded rules for which elements exists and how they should work. This data is called element metadata and the validator bundles metadata for all HTML5 elements but makes no distinction between the bundled and user-provided metadata.

The bundled metadata is available in html5.json.

See also:

The component

Lets assume we have a custom element called <my-component>. If this element has no metadata anything goes for this element and the validator cannot help much. Lets start off with some examples:

<!-- this is probably legal? -->
<div>
  <my-component>lorem ipsum</my-component>
</div>

<!-- but should it work inside a span? -->
<span>
  <my-component>lorem ipsum</my-component>
</span>
0 errors found

Depending on what the element consists of it might not be appropriate to use inside a <span> but there is not yet any metadata available to tell if <my-component> is allowed to be used in that context.

<!-- can it contain an interactive button? who knows? -->
<my-component>
  <button type="button">click me!</button>
</my-component>

<!-- or is it allowed inside a button? -->
<button type="button">
  <my-component>click me!</my-component>
</button>
0 errors found

Similarly there is not yet a way to tell if a button is allowed inside the component or if it may be used inside one. Consider what would happen if <my-component> wraps the content in an <a>.

<!-- lets nest the component for fun and profit! -->
<my-component>
  <my-component>
    <my-component>
      Sup dawg I heard you like components so I put components inside your components.
    </my-component>
  </my-component>
</my-component>
0 errors found

Most of the time it would make little sense to nest components but sometimes it isn't as obvious when it happens. Perhaps the nesting isn't direct but happens way down in the DOM tree.

All of above considered valid unless the metadata gives instructions how the element should be used. The first step is creating a new file, e.g. elements.js and configure the validator to read it.

elements.js:

const { defineMetadata } = require("html-validate");

module.exports = defineMetadata({
  "my-component": {},
});

Configure with:

.htmlvalidate.json
{
  "extends": ["html-validate:recommended"],
  "elements": ["html5", "./elements.js"]
}

Rerunning the validation will now have the complete opposite effect, the element will not be allowed anywhere:

<div>
  <my-component>lorem ipsum</my-component>
</div>
error: <my-component> element is not permitted as content under <div> (element-permitted-content) at inline:2:4:
  1 | <div>
> 2 |   <my-component>lorem ipsum</my-component>
    |    ^^^^^^^^^^^^
  3 | </div>


1 error found.

This happens because we have not yet told the validator what kind of element it is, we only provided it with empty metadata. Most properties default to false or [].

Tips

Use no-unknown-elements rule to find elements you have not yet provided metadata for.

Content categories

The first step to writing metadata is to decide which content category the element belongs to. It can be one or many but most elements will probably only use flow and/or phrasing. A bit simplified but flow elements can be thought as block-level <div> and phrasing as inline <span> (but all phrasing elements are also flow elements).

For instance, if our <my-component> element were to work similar to a <div> we can set the flow property to true.

 const { defineMetadata } = require("html-validate");

 module.exports = defineMetadata({
   "my-component": {
+    flow: true,
   },
 });

The element will now be accepted inside another <div> as flow elements can be nested inside each other.

<div>
  <my-component>lorem ipsum</my-component>
</div>
0 errors found

It can not be nested inside a <span> as a <span> does not accept flow content (only other phrasing elements):

<span>
  <my-component>lorem ipsum</my-component>
</span>
error: <my-component> element is not permitted as content under <span> (element-permitted-content) at inline:2:4:
  1 | <span>
> 2 |   <my-component>lorem ipsum</my-component>
    |    ^^^^^^^^^^^^
  3 | </span>


1 error found.

If we set the phrasing property as well the element will be allowed inside a <span> too:

 const { defineMetadata } = require("html-validate");

 module.exports = defineMetadata({
   "my-component": {
     flow: true,
+    phrasing: true,
   },
 });

<span>
  <my-component>lorem ipsum</my-component>
</span>
0 errors found

Some elements might want only one or the other and some want both. Most of the time a phrasing elements belongs to both the flow and phrasing categories but the opposite is not true.

Rule of thumb
  • If the component is meant to be used where a <span> would go set both flow and phrasing to true.
  • If the component is meant to be used where a <div> would go set only flow to true.
  • If the component wraps a <a> or <button> element set interactive to true.

There are other content categories as well, check the element metadata reference and the official HTML5 specification for details of each one.

Case study: <div>

const { defineMetadata } = require("html-validate");

module.exports = defineMetadata({
  div: {
    flow: true,
    permittedContent: ["@flow"],
  },
});

As a generic flow content container the <div> element simply sets the flow property and uses the permittedContent property to restrict its content to only allow other flow content (in practice this means almost all other elements). In the next part, restricting element content, you will learn how to restrict both content and context of your components.