Skirbi Developer Guide
Component Authors
This document is for component authors building on top of HTMLElementSugar.
It explains the mental model, lifecycle, template system, and extension points.
It is intentionally explicit and a bit repetitive — future-you will thank you.
Design goals
- Classless HTML output
- No Shadow DOM
- Custom elements stay in the DOM as CSS hooks
- Templates define structure
- JavaScript defines behavior
- CSS is the primary theming layer
- Fail fast when something is misconfigured
- Work well with static sites (Hugo)
Core mental model
Think of a component as a one-time compiler:
<my-article title="Foo">
Hello world
</my-article>
↓ (on connect)
<my-article>
<article>
<my-header>
<header>
<h1>Foo</h1>
</header>
</my-header>
<section>
Hello world
</section>
</article>
</my-article>
- The custom tag (
my-article) remains - Semantic HTML is rendered inside
- JS runs once and then gets out of the way
Registration
Every component must define a static tag and be registered once.
class MyArticle extends HTMLElementSugar {
static tag = 'my-article';
}
MyArticle.register();
Important behavior
static register() {
if (!customElements.get(this.tag)) {
this.init();
customElements.define(this.tag, this);
}
}
- Registration is define-once
- If another class already registered the same tag, this is a no-op
- This enables theme-level overrides
Attribute handling
class MyHeader extends HTMLElementSugar {
static tag = 'my-header';
static attributeDefs = {
title: { default: '' },
subtitle: { default: '' },
};
}
What this gives you:
observedAttributesdefaultConfigattributeMap- Automatic updates via
attributeChangedCallback
After:
connectedCallback() {
super.connectedCallback();
}
You can rely on:
this.config.title
this.config.subtitle
being correct and initialized.
Templates
Templates define structure, not behavior.
They are resolved during .register() via checkTemplate().
Template resolution modes
1. Strict ID (fail-fast)
static HtmlTemplate = 'my-article-template';
- Looks up
<template id="my-article-template"> - Throws immediately if missing or invalid
Use when: the template must exist.
2. Template via javascript function
class TemplateFunction extends HTMLElementSugar {
static tag = 'template-function';
static HtmlTemplate() {
const t = document.createElement('template');
t.innerHTML = `<div class="track-row"><div class="track-info">from fn</div></div>`;
return t;
}
}
We recently added a helper you can now do this too:
class TemplateFunction extends HTMLElementSugar {
static tag = 'template-function';
static HtmlTemplate = this.tpl(`
<div class="track-row">
<div class="track-info">from tpl</div>
</div>
`);
}
3. Fallback tuple (recommended)
static HtmlTemplate = [
'my-article-template',
() => {
const t = document.createElement('template');
t.innerHTML = `
<article>
<my-header></my-header>
<section my-body></section>
</article>
`;
return t;
}
];
Behavior:
- If a template with that ID exists → use it
- Otherwise → use the fallback
- Still resolved at register-time, not lazily
This is ideal for:
- Libraries
- Hugo themes
- “Works by default, override if needed”
3. Direct template or factory
Also supported, but less common.
Rendering pattern (canonical)
Every component should follow this shape:
connectedCallback() {
super.connectedCallback();
const frag = this.renderFromTemplate();
// mutate fragment
// move light-DOM children
// validate placeholders
this.replaceChildren(frag);
}
Key rules
- Always mutate the cloned fragment
- Never mutate the cached template
- Prefer
replaceChildren()(keep wrapper) - Use
replaceWith()only if you intentionally want to remove the custom tag - Use placeholders (see next chapter) instead of elements for things you want to modify. This allows consumers to override your defaults without having to use the same kind of elements.
Placeholder convention
Use attribute markers, not classes:
<section my-body></section>
<h1 my-title></h1>
Advantages:
- Classless
- Hard to collide with user HTML
- Easy to validate and query
Always fail fast if a required placeholder is missing.
Composition
Components can freely compose other components in templates:
<my-header></my-header>
<my-meta></my-meta>
The platform guarantees:
- If the child component is defined → it upgrades immediately
- If not → it upgrades when defined later
No special handling required.
Aliases
Aliases are alternate tag names pointing to the same behavior.
MyArticle.alias('my-post');
Aliases may also set default attributes.
When to override behavior
Override behavior only when necessary.
Preferred order:
- CSS
- Template override
- Behavior override (subclass + register)
If you subclass often, your template surface is probably too small.
Summary for authors
- Components are one-shot compilers
- Templates are the structural API
- CSS is the theme language
- JS should be boring and predictable
- Fail fast, always