Appearance
Single-File Component Authoring
When emitting a self-contained custom element in one HTML file (a <template> plus a <script>, no build step), a handful of authoring rules keep the result readable, declarative, and free of collisions with the host page. These are the defaults to reach for, not a style to apply only on request.
When to use
Use these rules whenever you hand-author a custom element as a single drop-in file: a prototype component, a reviewable HTML artifact, or any element a human pastes into a page without a bundler.
The rules
Use a module script, not an IIFE
Wrap the logic in <script type="module">. Module scope already isolates everything, so the IIFE wrapper and its top-level return guard are dead weight. The define collapses to:
html
<script type="module">
if (!customElements.get("comment-layer")) {
customElements.define("comment-layer", CommentLayer);
}
</script>Note that document.currentScript is null in a module, so grab the template by name (below) rather than walking from the running script.
Grab the template by its name
Give the template a name and select it by that attribute. No id is needed, so there is nothing to collide with the host page:
javascript
const template = document.querySelector(`template[name="comment-layer"]`);Select template[name=...] (the template element itself), not the descendant form template [name=...] with a space. A template's content is an inert fragment, so the descendant selector cannot match into it and returns null.
Build UI from <template>, never string concatenation
Do not assemble DOM with HTML string concatenation or innerHTML. Declare each repeated piece as its own nested <template> and clone it, filling slots with textContent and dataset:
html
<template name="comment-item">
<div class="item">
<q></q>
<div class="note"></div>
</div>
</template>javascript
function cloneTemplate(name) {
return shadow
.querySelector(`template[name="${name}"]`)
.content.cloneNode(true);
}
const row = cloneTemplate("comment-item");
row.querySelector(".note").textContent = comment.note;This deletes the hand-rolled esc() escaper and makes the result XSS-safe by construction. Text set through textContent is never parsed as HTML, so an <img onerror> payload stays on the page as inert literal text.
Keep the scoped <style> inside the template
For a shadow-DOM component, the stylesheet must live in the shadow root to be scoped, and the only declarative single-file way to get it there is to clone it in with the rest of the template. So the <style> belongs inside the <template>, cloned alongside the markup. The alternative, an adopted constructable stylesheet, means handing CSS to CSSStyleSheet.replaceSync() as a JavaScript string — CSS-in-JS. Keep styles in the stylesheet and feed runtime values in as custom properties or attributes rather than authoring CSS in JavaScript. That rule is general, not specific to single-file components; see Dynamic Styles for the full custom-property vs CSS-in-JS treatment.
Keep it readable, not minified
Ship readable source: one statement per line, one CSS property per line, real names over one- and two-character abbreviations. Run the project's formatter and linter rather than reflowing by hand, so the behavior is provably unchanged. See Code Style.
See also
- Encapsulation: When to use shadow DOM vs light DOM, and where the scoped
<style>lives in each. - Extension Patterns: The light-DOM
<template>plusname-keyed script foundation, and CSS-first extension seams. - Popover API for Overlays: Render a component's floating surfaces in the top layer without a z-index stack.
- Clipboard Copy with Universal Fallback: Copy text reliably even from
file://or plain HTTP. - Dynamic Styles: Keep styling in CSS and feed runtime values in as custom properties instead of authoring CSS in JavaScript.