XBL In Firefox

XBL (XML Binding Language) is a markup language used to define appearance and behavior for UI components in Firefox. The implementation of XBL and the set of implemented bindings are deeply ingrained into Firefox, and it's been an influence for Web Components specs like Shadow DOM.

But there are a number of problems with it. And since we're no longer bound to support XUL extensions and the Web Components implementations are progressing, it's a good time to think about what a migration away from XBL would look like.

We've proposed a plan do to just this, so I'll give an overview of the plan and share some of the tools I created to help come up with it. Note that much of this is covered in more detail in this design review document.

Firefox has a lot UI written in XBL - around 300 individual bindings and over 40K LOC with a pretty wide distribution of complexity, so we'll need to take different approaches depending on the binding. Here are some of the approaches we identified:

1. Convert many bindings to Web Components

For a simple case where the binding is only specifying some JS and anonymous content that gets attached to a node, it can be relatively straightforward to transform XBL into a JS class. I've built an online XBL to Custom Element converter that demonstrates this.

Note: there are XBL features like implements that use XPCOM and can make things things significantly more complicated (see "Special Cases" below). But to get a feel for the syntax, here's a simple example:

<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <binding id="okcancel">
    <content>
      <xul:button label="OK"/>
      <xul:button label="Cancel"/>
    </content>
    <handlers>
      <handler event="input"><![CDATA[
        this.onClick(event);
      ]]></handler>
    </handlers>
    <method name="onClick">
      <parameter name="aEvent"/>
      <body><![CDATA[
      console.log("Clicked", aEvent.target);
      ]]></body>
    </method>
  </binding>
</bindings>

Becomes:

class FirefoxOkcancel extends XULElement {
  connectedCallback() {
    this.innerHTML = `
      <xul:button label="OK"></xul:button>
      <xul:button label="Cancel"></xul:button>
    `;

    this.addEventListener("input", event => {
      this.onClick(event);
    });
  }

  onClick(aEvent) {
    console.log("Clicked", aEvent.target);
  }
}
customElements.define("firefox-okcancel", FirefoxOkcancel);

And the markup would change from:

<style type="text/css">
  okcancel { -moz-binding: url(okcancel.xml#okcancel); }
</style>
<okcancel />

to:

<script src="okcancel.js"></script>
<firefox-okcancel />

Notice that we are extending XULElement instead of HTMLElement. We made an intentional decision not to couple replacing XBL with replacing XUL. This is to limit the scope and risk of the project, since XUL provides a lot of features that HTML doesn't. Also, replacing XBL doesn't prevent or add a lot of extra cost to replacing XUL, if we decided to do so in the future.

A few notes about the differences:

  • With XBL the binding is attached via CSS (see for example xul.css). A Custom Element is attached via tag name, and is registered in a normal script via customElements.define.
  • Because bindings are attached via CSS, it makes migrating to Custom Elements tricky if more than one binding is attached to the same tag based on a CSS selector (i.e. button vs button[type="repeat"]). We'll need to either have two different elements (firefox-button and firefox-repeatbutton), or fold the logic into a single Custom Element definition and branch on the attribute.
  • XBL provides a <resources> element which allows you to specify stylesheets that will be scoped to the <content> within the binding. This is useful both as a way to only load styles when a binding is used in a document, and as a way to scope styles. Shadow DOM provides similar a capability, but with just Custom Elements we'll need to work around this.
  • Custom Element tag names require a dash by spec, however we could bypass that for chrome documents if we want to avoid a mass find-replace.

2. Convert some bindings to JS modules

There are some bindings that aren't actually widgets - bindings that are only used once, or don’t render any DOM. These can generally be converted to plain JS objects - either inside of a JSM file which is loaded only once per session, or in a script loaded inside of each browser window.

3. Unrefactored XUL/JS/CSS

Bindings that are rarely used and insert relatively little content can be unrefactored into their constituent parts (XUL, JS, CSS) and inserted into each bound element. It would be up to whoever was working on an individual binding to decide if this approach makes sense - we identified some candidates in the review.

4. Flatten the inheritance hierarchy

We use inheritance pretty widely in our bindings. There are likely cases where we don’t need to - the extra inheritance may have been required for XUL extensions, or may have had a purpose at one point but doesn't any longer.

I've created a page to help visualize the inheritance tree. Some potential candidates for this approach can be found in this tree where a binding is the only child of its parent, and the parent is never directly used.

5. Handle special cases

There are plenty of bindings in Firefox that don't fit into any of the approaches above. Generally, bindings that use the [implements] attribute but don’t map pretty closely back to an HTML tag may fall into this category.

Here are a few examples:

  • In-content XBL may provide special challenges due to security considerations (Scrollbars, Marquee, Video controls, Click-to-play plugin UI)
  • <tree>, which implements nsIDOMXULTreeElement and has a pretty complicated API
  • Bindings which take advantage of CSS selector matching to switch between bindings at runtime

What's next?

The next steps are mostly being tracked in this meta bug:

  • Migrate a few bindings and update the plan based on what we learn
  • Land XUL support for Custom Elements
  • Improve tooling to make converting bindings easier
  • Begin a bug breakdown for individual bindings