Writing custom element metadata: Restricting element content

Looking back at our initial example we saw that the element accepted a <button> as content. If we want to allow only phrasing content (<span>, <strong>, etc) inside we can use the permittedContent property to limit. permittedContent is a list of allowed tags or content categories.

import { defineMetadata } from "html-validate";

export default defineMetadata({
  "my-component": {
    flow: true,
    permittedContent: ["span", "strong", "em"],
  },
});

<my-component>
  <button type="button">click me!</button>
</my-component>
error: <button> element is not permitted as content under <my-component> (element-permitted-content) at inline:2:4:
  1 | <my-component>
> 2 |   <button type="button">click me!</button>
    |    ^^^^^^
  3 | </my-component>


1 error found.

As it quickly get tedious to list all tag names we can refer to content categories directly:

 import { defineMetadata } from "html-validate";

 export default defineMetadata({
   "my-component": {
     flow: true,
-    permittedContent: ["span", "strong", "em"],
+    permittedContent: ["@phrasing"],
   },
 });

The list can also be turned to a blacklist by using the exclude keyword:

 import { defineMetadata } from "html-validate";

 export default defineMetadata({
   "my-component": {
     flow: true,
-    permittedContent: ["span", "strong", "em"],
+    permittedContent: [{ "exclude": "@heading" }],
   },
 });

<my-component>
  <div>allowed</div>
  <span>also allowed</span>
  <h1>not allowed</h1>
</my-component>
error: <h1> element is not permitted as content under <my-component> (element-permitted-content) at inline:4:4:
  2 |   <div>allowed</div>
  3 |   <span>also allowed</span>
> 4 |   <h1>not allowed</h1>
    |    ^^
  5 | </my-component>


1 error found.

Tips

exclude is also useful to prevent interactive elements from disallowing other interactive elements by excluding @interactive

Descendants

permittedContent validates direct children to the element but not deeper descendants. The related permittedDescendants property checks all descendants. Most of the time you should prefer permittedContent over permittedDescendants as each children should have its own metadata describing what should and should not be allowed and by allowing the element itself you should accept any descendants it may pull. However, it can be used in circumstances where this is not possible. The most common case is to prevent nesting of the component or limit usage of certain content categories such as sectioning or headings:

import { defineMetadata } from "html-validate";

export default defineMetadata({
  "my-component": {
    flow: true,
    permittedDescendants: [{ exclude: ["my-component", "@sectioning"] }],
  },
});

<my-component>
<!-- the div itself is allowed -->
  <div>
    <footer>
      sectioning element can no longer be used
    </footer>
    <my-component>
      nor can the component be nested
    </my-component>
  </div>
  <span>also allowed</span>
  <h1>not allowed</h1>
</my-component>
error: <footer> element is not permitted as a descendant of <my-component> (element-permitted-content) at inline:4:6:
  2 | <!-- the div itself is allowed -->
  3 |   <div>
> 4 |     <footer>
    |      ^^^^^^
  5 |       sectioning element can no longer be used
  6 |     </footer>
  7 |     <my-component>


error: <my-component> element is not permitted as a descendant of <my-component> (element-permitted-content) at inline:7:6:
   5 |       sectioning element can no longer be used
   6 |     </footer>
>  7 |     <my-component>
     |      ^^^^^^^^^^^^
   8 |       nor can the component be nested
   9 |     </my-component>
  10 |   </div>


2 errors found.

Rule of thumb
  • Use permittedContent to limit the elements you want to allow as content.
  • Use permittedDescendants to prevent nestling.
  • If the component wraps a <a> or <button> element use permittedDescendants exclude other interactive elements.

Other properties to limit content also exits, check the element metadata reference for details.

Case study: <html>

(simplified for brevity)

import { defineMetadata } from "html-validate";

export default defineMetadata({
  html: {
    permittedContent: ["head?", "body?"],
    permittedOrder: ["head", "body"],
    requiredContent: ["head", "body"],
  },
});

Like we seen before the permittedContent property is used to restrict to only accept the <head> and <body> elements. Note the usage of a trailing ?, this limits the allowed occurrences to 0 or 1 (2 or more is disallowed). Default is to allow any number of occurrences.

Next it uses permittedOrder to declare that <head> must come before <body>. permittedOrder doesn't have to list all the possible elements from permittedContent but for the items listed the order must be adhered to. Contents groups such as @flow is allowed and unlisted elements can be used in any anywhere (even between listed elements).

Lastly it uses requiredContent to declare that both <head> and <body> must be present.

To sum up, the <html> elements puts the following restrictions in place: