npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

custom-behavior-registry

v1.1.3

Published

Attach one or more custom element-like behavior classes to any element in the DOM

Readme

CustomBehaviorRegistry

Attach one or more custom element-like behavior classes to any element in the DOM and attached shadow DOMs. Create a custom registry that maps named behavior classes to CSS selectors, creates behavior instances for matching elements, and uses a single Mutation Observer to keep them manage the behavior life-cycle as the DOM changes.

This project was inspired by other (now-abandoned) packages that had similar functionality:

  • WebReflection / wicked-elements Attaches one or more behavior objects to any element that match associated query selectors. CustomBehaviorRegistry is a spiritual successor of sorts to this package.
  • matthewp / custom-attributes Attaches one or more behavior classes to any element that have an associated custom attribute defined.
  • lume / element-behaviors Attaches one or more behavior classes to any element with an assocated name in a space deliminated list within the non-standard 'has' attribute.

Install and Create a Registry

import CustomBehaviorRegistry from "custom-behavior-registry";

const registry = new CustomBehaviorRegistry();

Registry Options

Pass options to the constructor to customize how behavior definitions work.

| Option | Description | | --- | --- | | queryPrefix | String prepended to every definition name when generating its selector. | | querySuffix | String appended to every definition name when generating its selector. | | queryGenerator(name, behavior, options) | Returns the CSS selector for a definition. Overrides prefix and suffix. | | nameValidator(name) | Returns true to accept a name, false to reject it, or a string to normalize it. | | attributeFilter | Iterable of document attributes whose changes can trigger behavior matching checks. | | attributeChangedCallback(element, attributeName, oldValue, newValue) | Called when an attribute listed in the attributeFilter changes. Returns false to cancel the action, true to continue. May also return an element, or an iterable of elements to update instead of the target element. | | definedCallback(name, behavior, options) | Called when defining a behavior. May return updated registry options to apply. | | definitionConstructorCallback(name, element, behavior, options) | Called before a behavior is constructed. May return options to merge into the existing definition options. | | definitionConnectedCallback(name, element, options) | Called before a behavior's connectedCallback. | | definitionDisconnectedCallback(name, element, options) | Called after a behavior's disconnectedCallback. | | definitionConnectedMoveCallback(name, element, options) | Called before a behavior's connectedMoveCallback. | | definitionAttributeChangedCallback(name, element, attributeName, oldValue, newValue, options) | Called before a behavior's attributeChangedCallback. | | definitionOptionDefaults | Default options merged with options passed to define. |

Instance Methods

define(name, behavior[, options])

Adds a definition, connects it to matching elements, and resolves any matching whenDefined promise. Throws for invalid names, duplicate names, or non-constructors.

whenDefined(name)

Returns a promise that resolves with the behavior class after the name is defined.

await registry.whenDefined("my-tooltip");

update([root])

Rechecks root element and all of its descendants against the registered definitions. Without an argument, checks the current DOM and attached Shadow DOMs.

registry.update(document.querySelector("main"));

get(name[, element])

Returns the behavior class for name, or the behavior instance connected to element when an element is supplied. Returns null when absent.

const behaviorClass = registry.get('my-tooltip'); 
const behaviorInstance = registry.get('my-tooltip',document.querySelector("main")); 

getElements(nameOrBehavior)

Returns a Map of connected elements to behavior instances for a definition name or behavior class. Returns null when none are connected.

getElementBehaviors(element)

Returns a frozen object containing the behavior instances connected to element, keyed by definition name. Returns null when there are none.

getName(behavior)

Returns the definition name registered for a behavior class, or null.

Static Registry Methods

CustomBehaviorRegistry.disconnect(registry);

// do instense DOM manipulation

registry.update();
CustomBehaviorRegistry.observe(registry);

CustomBehaviorRegistry.disconnect(registry)

Disables DOM observation. Existing definitions and connections remain available, and registry.update() will still manually recan for changes.

CustomBehaviorRegistry.observe(registry)

Re-enables DOM observation for the registry.

CustomBehaviorRegistry.getSettings(registry)

Returns the registry's active settings.

CustomBehaviorRegistry.replaceSettings(registry, settings)

Replaces active registy settings with valid values from settings.

CustomBehaviorRegistry.clearSettings(registry)

Restores a registry to default settings.

CustomBehaviorRegistry.undefineBehavior(registry, nameOrBehavior)

Removes one definition and disconnects its behavior instances. |

CustomBehaviorRegistry.undefineAllBehaviors(registry)

Removes every definition and disconnects all behavior instances.

Usage Examples

The inspirational packages listed above can be roughly replicated using the CustomElementRegistry constructor

Create a wicked-elements like attachBehaviorByQuery registry

// default 'optionless' registry, define method takes in a query selector and behavior class
window.attachBehaviorByQuery = new CustomBehaviorRegistry();

attachBehaviorByQuery.define('table[role="treegrid"] > * > tr[aria-level]', AriaTreegridExpander)
// matches the <tr> in <table role="treegrid" ...> <thead|tfoot|tbody> <tr aria-level="..." ...>

check out presets/attach-behavior-by-query for a more robust version

It could be modified to just look for class names

window.attachBehaviorByClass = new CustomBehaviorRegistry({
  // wrap the name to generate a class query selector
  queryPrefix: '.',
  querySuffix: '',
  // ensure name doesn't have whitespace
  nameValidator: (name) => !/\s/.test(name),
  // scan the DOM for updates to the class attribute
  attributeFilter: ["class"]
});

attachBehaviorByClass.define('treegrid', AriaTreegridBehavior);
// matches <[tagname] class="treegrid ..." ...>

check out presets/attach-behavior-by-class for a more robust version

Create a element-behaviors like elementHasBehavior registry

window.elementHasBehavior = new CustomBehaviorRegistry({
  // wrap the name to generate a `has` attribute query selector
  queryPrefix: '[has~="',
  querySuffix: '"]',
  // ensure name generally matches the custom ident structure
  nameValidator: (name) => /^[a-z]([^A-Z]*-)+[^A-Z]+$/.test(name),
  // scan the DOM for updates to the has attribute
  attributeFilter: ["has"]
});

elementHasBehavior.define('treegrid-expander', AriaTreegridExpander)
// matches <[tagname] has="treegrid-expander ..." ...>

check out presets/element-has-behavior for a more robust version

It could be modified to check an existing attribute value (like role)

window.ariaRoleBehaviors = new CustomBehaviorRegistry({
  // wrap the name to generate a `role` attribute query selector
  queryPrefix: '[role="',
  querySuffix: '"]',
  // ensure name doesn't have whitespace and force it to lower case
  nameValidator: (name) => !/\s/.test(name) && name.toLowerCase(),
  // scan the DOM for updates to the role attribute
  attributeFilter: ["role"]
});

ariaRoleBehaviors.define('treegrid', AriaTreegridBehavior);
// matches <[tagname] role="treegrid" ...>

Create a custom-attributes like customAttributes registry

//keep an external array to hold attribute names
const attributeNames = [];
window.customAttributes = new CustomBehaviorRegistry({
  // wrap the name to generate an attribute query selector
  queryPrefix: '[',
  querySuffix: ']',
  // ensure name doesn't start with aria- or data-
  // and generally matches the custom ident structure
  nameValidator: (name) => (
    !(name.startsWith('aria-') || name.startsWith('data-'))
    && /^[a-z]([^A-Z]*-)+[^A-Z]+$/.test(name)
  ),
  // no attributes to filter initially
  attributeFilter: [],
  // for each new behavior defined, return a new filter list to scan for
  definedCallback: (name) => (
    attributeNames.push(name) &&
    {attributeFilter: attributeNames}
  );

customAttributes.define('treegrid-expander', AriaTreegridExpander)
// matches <[tagname] treegrid-expander[="..."] ...>

check out presets/custom-attributes for a more robust version

It could be modified to check for existing attributes (like aria-*)

//keep an external array to hold attribute names
const attributeNames = [];
window.ariaAttributeBehaviors = new CustomBehaviorRegistry({
  // wrap the name to generate a an `aria-*` attribute query selector
  queryPrefix: '[aria-',
  querySuffix: ']',
  // ensure name doesn't have whitespace and force it to lower case
  nameValidator: (name) => !/\s/.test(name) && name.toLowerCase(),
  // no attributes to filter initially
  attributeFilter: [],
  // for each new behavior defined, return a new filter list to scan for
  definedCallback: (name) => (
    attributeNames.push('aria-'+name) &&
    {attributeFilter: attributeNames}
  );

ariaAttributeBehaviors.define('level', AriaTreegridExpander)
// matches <[tagname] aria-level[="..."] ...>

Combine all of the above into a customBehaviors registry

window.customBehaviors =
  window.customBehaviors ||
  new CustomBehaviorRegistry({
    queryGenerator: (name, behavior, options) => {
      let parts = [],
        query = "";
      if (options.asQuery) {
        query =
          options.asQuery + "" === options.asQuery ? options.asQuery : name;
      }
      if (options.asTag) {
        if (!behavior.tagFilter?.length) {
          behavior.tagFilter = [];
        }
        const value =
          options.asTag + "" === options.asTag ? options.asTag : name;
        behavior.tagFilter.includes(value) || behavior.tagFilter.push(value);
        parts.push(value);
      }
      if (options.asClass) {
        parts.push(
          "." + options.asClass + "" === options.asClass
            ? options.asClass
            : name,
        );
      }
      if (options.asAttribute) {
        parts.push(
          "[" +
            (options.asAttribute + "" === options.asAttribute
              ? options.asAttribute
              : name) +
            "]",
        );
      }
      if (options.asAttributeValue + "" === options.asAttributeValue) {
        parts.push("[" + options.asAttributeValue + '="' + name + '"]');
      }
      if (parts.length) {
        query += (query.length ? ", " : "") + ":is(" + parts.join(", ") + ")";
      } else if (!query.length && behavior.tagFilter?.length) {
        query = "*";
      }
      return query;
    },
    definedCallback: (name, behavior, options) => {
      const attributeFilter =
        window.customBehaviors[Symbol.for("attributeFilter")] || [];
      let update = false;
      if (options.asClass && !attributeFilter.includes("class")) {
        attributeFilter.push("class");
        update = true;
      }
      if (options.asAttribute) {
        const value =
          options.asAttribute + "" === options.asAttribute
            ? options.asAttribute
            : name;
        if (!attributeFilter.includes(value)) {
          attributeFilter.push(value);
          update = true;
        }
      }
      if (options.asAttributeValue + "" === options.asAttributeValue) {
        const value = options.asAttributeValue.replace(/[*|~$^]$/, "");
        if (!attributeFilter.includes(value)) {
          attributeFilter.push(value);
          update = true;
        }
      }
      if (update) {
        window.customBehaviors[Symbol.for("attributeFilter")] = attributeFilter;
        return { attributeFilter };
      }
    },
  });

check out presets/custom-behaviors for a more robust version with usage details.

Define a Behavior

define(name, Behavior, options) registers a behavior and immediately connects it to matching elements already in the document. It will listen for mutations on the DOM and any shadow DOMs connected after the registry was created to dynamically update the registry as required.

class ExampleBehavior {
  static observedAttributes = ["aria-expanded", "data-state-*"];
  static tagFilter = ["button", "input", "output"];
  /* static tagExcludes = ["div", "span"];  only include one of these lists */

  static preConnectionCheck(element, options) {}

  constructor(element, options) {}

  connectedCallback(element) {}
  disconnectedCallback(element) {}
  connectedMoveCallback(element) {}
  attributeChangedCallback(element, attributeName, oldValue, newValue) {}
}

| Member | Description | | --- | --- | | static observedAttributes | Iterable of attribute names that trigger attributeChangedCallback when updted. Names ending in -* will match any attribute with that name prefix. This is a live list. Changes take effect when a new behavior trigger is observed. | | static tagFilter | Iterable of allowed tag names. When present, only matching elements that also have one of these tag names will be connected. This is a live list. Changes take effect when a new behavior trigger is observed, but registry.update() should be used to rescan existing elements. | | static tagExcludes | Iterable of excluded tag names. When present, only matcing elements WITHOUT one of these tag names will be connected. This is a live list. Changes take effect when a new behavior trigger is observed, but registry.update() should be used to rescan existing elements. | | static preConnectionCheck(element, options) | Runs before a behavior connects. Return false to skip the connection, true to continue, or an options object to merge into the definition. | | constructor(element, options) | Creates the behavior instance the first time an element connects. | | connectedCallback(element) | Runs when a behavior instance connects to an element, including reconnections. | | disconnectedCallback(element) | Runs when a connected element is removed or stops matching the behavior selector. | | connectedMoveCallback(element) | Runs when an already connected element is moved within the DOM and still matches. Without it, the registry runs the disconnect and connect callbacks instead. | | attributeChangedCallback(element, attributeName, oldValue, newValue) | Runs when a listed observed attribute changes on a connected element. |

Including both a tagFilter and tagExcludes list will cancel each other out and never connect any element

Examples

For live examples, see:

See the Intl Time and Intl Data examples for a behaviors that do locale aware formats with native <time> and <data> elements.