Skip to main content

Working with Shadow DOM

Lit components use shadow DOM to encapsulate their DOM. Shadow DOM provides a way to add a separate isolated and encapsulated DOM tree to an element. DOM encapsulation is the key to unlocking interoperability with any other code—including other web components or Lit components—functioning on the page.

Shadow DOM provides three benefits:

  • DOM scoping. DOM APIs like document.querySelector won't find elements in the component's shadow DOM, so it's harder for global scripts to accidentally break your component.
  • Style scoping. You can write encapsulated styles for your shadow DOM that don't affect the rest of the DOM tree.
  • Composition. The component's shadow root, which contains its internal DOM, is separate from the component's children. You can choose how children are rendered in your component's internal DOM.

For more information on shadow DOM:

Older browsers. On older browsers where native shadow DOM isn't available, the web components polyfills may be used. Please note that Lit's polyfill-support module must be loaded along with the web components polyfills. See Requirements for legacy browsers for details.

Accessing nodes in the shadow DOM

Permalink to Accessing nodes in the shadow DOM

Lit renders components to its renderRoot, which is a shadow root by default. To find internal elements, you can use DOM query APIs, such as this.renderRoot.querySelector().

The renderRoot should always be either a shadow root or an element. Both implement the ParentNode interface and share API like .querySelectorAll() and .children.

You can query internal DOM after component initial render (for example, in firstUpdated), or use a getter pattern:

LitElement supplies a set of decorators that provide a shorthand way of defining getters like this.

@query, @queryAll, and @queryAsync decorators

Permalink to @query, @queryAll, and @queryAsync decorators

The @query, @queryAll, and @queryAsync decorators all provide a convenient way to access nodes in the internal component DOM.

Using decorators. Decorators are a proposed JavaScript feature, so you’ll need to use a compiler like Babel or TypeScript to use decorators. See Using decorators for details.

@query

Modifies a class property, turning it into a getter that returns a node from the render root. The optional second argument when true performs the DOM query only once and caches the result. This can be used as a performance optimization in cases when the node being queried will not change.

This decorator is equivalent to:

@queryAll

Identical to query except that it returns all matching nodes, instead of a single node. It's the equivalent of calling querySelectorAll.

Here, _divs would return both <div> elements in the template. For TypeScript, the typing of a @queryAll property is NodeListOf<HTMLElement>. If you know exactly what kind of nodes you'll retrieve, the typing can be more specific:

The exclamation point (!) after buttons is TypeScript's non-null assertion operator. It tells the compiler to treat buttons as always being defined, never null or undefined.

@queryAsync

Similar to @query, except that instead of returning a node directly, it returns a Promise that resolves to that node after any pending element render is completed. Code can use this instead of waiting for the updateComplete promise.

This is useful, for example, if the node returned by @queryAsync can change as a result of another property change.

Your component may accept children (like a <ul> element can have <li> children).

By default, if an element has a shadow tree, its children don't render at all.

To render children, your template needs to include one or more <slot> elements, which act as placeholders for child nodes.

To render an element's children, create a <slot> for them in the element's template. The children aren't moved in the DOM tree, but they're rendered as if they were children of the <slot>. For example:

To assign a child to a specific slot, ensure that the child's slot attribute matches the slot's name attribute:

  • Named slots only accept children with a matching slot attribute.

    For example, <slot name="one"></slot> only accepts children with the attribute slot="one".

  • Children with a slot attribute will only be rendered in a slot with a matching name attribute.

    For example, <p slot="one">...</p> will only be placed in <slot name="one"></slot>.

Specifying slot fallback content

Permalink to Specifying slot fallback content

You can specify fallback content for a slot. The fallback content is shown when no child is assigned to the slot.

Rendering fallback content. If any child nodes are assigned to a slot, its fallback content doesn't render. A default slot with no name accepts any child nodes. It won't render fallback content even if the only assigned nodes are text nodes containing whitespace, for example <example-element> </example-element>.

To access children assigned to slots in your shadow root, you can use the standard slot.assignedNodes method and the slotchange event.

For example, you can create a getter to access assigned nodes for a particular slot:

You can also use the slotchange event to take action when the assigned nodes change. The following example extracts the text content of all of the slotted children.

For more information, see HTMLSlotElement on MDN.

The @queryAssignedNodes decorator converts a class property into a getter that returns all of the assigned nodes for a given slot in the component's shadow tree. The optional second boolean argument when true flattens the assigned nodes, meaning any assigned nodes that are slot elements are replaced with their assigned nodes. The optional third argument is a css selector which filters the results to matching elements.

Using decorators. Decorators are a proposed JavaScript feature, so you’ll need to use a compiler like Babel or TypeScript to use decorators. See Using decorators for details.

The first example above is equivalent to the following code:

For TypeScript, the typing of a queryAssignedNodes property is NodeListOf<HTMLElement>.

Each Lit component has a render root—a DOM node that serves as a container for its internal DOM.

By default, LitElement creates an open shadowRoot and renders inside it, producing the following DOM structure:

There are two ways to customize the render root use by LitElement:

  • Setting shadowRootOptions.
  • Implementing the createRenderRoot method.

The simplest way to customize the render root is to set the shadowRootOptions static property. The default implementation of createRenderRoot passes shadowRootOptions as the options argument to attachShadow when creating the component's shadow root. It can be set to customize any options allowed in the ShadowRootInit dictionary, for example mode and delegatesFocus.

See Element.attachShadow() on MDN for more information.

The default implementation of createRenderRoot creates an open shadow root and adds to it any styles set in the static styles property. For more information on styling see Styles.

To customize a component's render root, implement createRenderRoot and return the node you want the template to render into.

For example, to render the template into the main DOM tree as your element's children, implement createRenderRoot and return this.

Rendering into children. Rendering into children and not shadow DOM is generally not recommended. Your element will not have access to DOM or style scoping, and it will not be able to compose elements into its internal DOM.