@skirbi/semtic
v0.0.29
Published
HTML for humans and CSS
Downloads
642
Maintainers
Readme
@skirbi/semtic
Semantic HTML authoring components built on top of @skirbi/sugar.
Semtic is the structure layer of Skirbi. It gives you small, light-DOM Web Components that turn compact authoring markup into real, inspectable HTML.
It aims to be boring:
- light DOM only
- real semantic HTML output
- real form controls where forms are involved
- custom elements remain in the DOM as CSS hooks
- attributes are the component API
- templates define structure
- JavaScript defines behavior
- CSS is the primary theming layer
- no Shadow DOM
- no styling runtime
- no hidden utility framework
CSS is where styling belongs. Use @skirbi/pinta for the default Skirbi theme
and recipes, @skirbi/dibuho for higher-level composition, or roll your own CSS
and target the Semtic elements directly.
Install
npm install @skirbi/semticRegister all Semtic components:
import '@skirbi/semtic/register-semtic';Or register only what you use:
import { SemticForm, SemticInput } from '@skirbi/semtic';
SemticForm.register();
SemticInput.register();Package shape
@skirbi/semtic exports component classes without registering them.
@skirbi/semtic/register-semtic registers the Semtic components as a side-effect
import.
All custom elements in this package use the semtic- prefix.
Component-level API details live on the documentation site.
Core mental model
Think of a Semtic component as a one-time compiler.
You author this:
<semtic-article title="Hello world">
<p>Clean authoring. Real HTML output.</p>
</semtic-article>On connect, the component renders semantic HTML inside the custom element:
<semtic-article title="Hello world">
<article>
<semtic-header>
<header>
<h1>Hello world</h1>
</header>
</semtic-header>
<section semtic-body>
<p>Clean authoring. Real HTML output.</p>
</section>
</article>
</semtic-article>The exact internal shape varies per component, but the contract is stable:
author compactly, output readable HTML, keep everything inspectable.
- The custom tag remains.
- Semantic HTML is rendered inside.
- The rendered DOM becomes the CSS surface.
- JavaScript runs once and then gets out of the way.
Registration
Every component defines a static tag and registers itself once.
import { HTMLElementSugar } from '@skirbi/sugar';
class SemticExample extends HTMLElementSugar {
static tag = 'semtic-example';
}
SemticExample.register();Registration is define-once:
static register() {
if (!customElements.get(this.tag)) {
this.init();
customElements.define(this.tag, this);
}
}If another class already registered the same tag, registration is a no-op. This keeps theme-level or app-level overrides possible without requiring a separate registry layer.
Component contract fields
Semtic components expose their authoring contract through static fields.
class SemticFieldset extends HTMLElementSugar {
static tag = 'semtic-fieldset';
static exampleHTML = '<semtic-fieldset></semtic-fieldset>';
static exampleRenderedHTML = '<fieldset><legend semtic-label></legend></fieldset>';
static renderGuardSelector = ':scope > fieldset';
static morphTriggerSelector = ':scope > *';
static attributeDefs = {
label: { default: '' },
description: { default: '' },
'required-label': { default: '*' },
};
}Common fields:
| Field | Purpose |
| --- | --- |
| tag | Custom element name registered with the browser. |
| exampleHTML | Minimal authored example used by documentation tooling. |
| exampleRenderedHTML | Expected rendered shape used by documentation tooling. |
| renderGuardSelector | Selector used to detect that a component already rendered. |
| morphTriggerSelector | Selector used to detect authored or morphed content that should render. |
| attributeDefs | Attribute API, defaults, and observed attribute source. |
| HtmlTemplate | Structural template or template fallback tuple. |
Treat these fields as the component contract. Keep them close to the component and document them with JSDoc comments so generated docs can group them later.
Attribute handling
Use attributeDefs for component-owned attributes.
class SemticExampleHeader extends HTMLElementSugar {
static tag = 'semtic-example-header';
static attributeDefs = {
title: { default: '' },
subtitle: { default: '' },
};
}This gives the component:
observedAttributesdefaultConfigattributeMap- automatic updates through
attributeChangedCallback
After calling super.connectedCallback(), component code can rely on the config
being initialized:
connectedCallback() {
super.connectedCallback();
this.config.title;
this.config.subtitle;
}Form controls may also forward unknown host attributes to the real control. That
is what makes wire:*, x-*, hx-*, aria-*, data-*, and native form
attributes useful without every component explicitly knowing about every tool.
Templates
Templates define structure, not behavior.
They are resolved during .register() by Sugar's template checks.
Strict ID
static HtmlTemplate = 'semtic-example-template';This looks up <template id="semtic-example-template"> and fails fast if it is
missing or invalid.
Use this when the template must exist.
Fallback tuple
static HtmlTemplate = [
'semtic-example-template',
() => {
const t = document.createElement('template');
t.innerHTML = `
<article>
<semtic-header></semtic-header>
<section semtic-body></section>
</article>
`;
return t;
}
];Behavior:
- If a template with that ID exists, use it.
- Otherwise, use the fallback.
- Resolution still happens at register-time.
This is the default Semtic-friendly mode: works by default, but remains overridable by a theme or app.
Direct template or factory
Direct templates and factories are also supported, but they are less common. Prefer the fallback tuple unless there is a clear reason not to.
Rendering pattern
Most Semtic components should follow this shape:
connectedCallback() {
super.connectedCallback();
if (this.hasRenderedShape?.()) return;
const frag = this.renderFromTemplate();
// Mutate the cloned fragment.
// Move light-DOM children.
// Validate required placeholders.
this.replaceChildren(frag);
}Rules:
- Always mutate the cloned fragment.
- Never mutate the cached template.
- Prefer
replaceChildren()so the custom element remains as the host hook. - Use
replaceWith()only when intentionally removing the custom element. - Fail fast when required placeholders are missing.
Placeholder convention
Use attribute markers, not classes:
<section semtic-body></section>
<h1 semtic-title></h1>Attribute markers are:
- classless
- unlikely to collide with user HTML
- easy to validate
- easy to query from the cloned fragment
Use the semtic- prefix for Semtic-owned markers where possible.
Composition
Components can freely compose other components in templates:
<semtic-header></semtic-header>
<semtic-article-meta></semtic-article-meta>No special upgrade handling is required. If the child component is already defined, it upgrades immediately. If it is defined later, the platform upgrades it then.
Aliases and overrides
Aliases are alternate tag names pointing to the same behavior.
SemticArticle.alias('semtic-post');Aliases may also set default attributes.
Override behavior only when necessary. Preferred order:
- CSS
- Template override
- Behavior override with a subclass and registration
If subclassing becomes common, the template surface is probably too small.
Styling
Semtic is not the styling layer.
Preferred options:
- Use
@skirbi/pintafor Skirbi's default tokens and component recipes. - Use
@skirbi/dibuhofor Skirbi's composition patterns and higher-level UI. - Roll your own CSS and target the Semtic elements directly.
@import '@skirbi/pinta/pinta.css';
@import '@skirbi/dibuho/dibuho.css';<html skirbi-theme>
...
</html>Local app CSS is fine:
<semtic-panel class="login-card">
...
</semtic-panel>.login-card {
max-width: 28rem;
margin-inline: auto;
}Attributes also work well as local styling hooks:
<semtic-panel login-card>
...
</semtic-panel>semtic-panel[login-card] {
max-width: 28rem;
}Semtic should not grow a large utility API. Prefer plain CSS for spacing, layout tuning, composition, and one-off styling.
Layout primitives
Semtic includes a few layout primitives and visual hooks, such as
semtic-grid, semtic-flex, semtic-stack, and semtic-divider.
They are intentionally small. They are readable authoring hooks, not a utility framework.
<semtic-grid class="player-details">
<semtic-panel>Player</semtic-panel>
<semtic-panel>Stats</semtic-panel>
</semtic-grid>.player-details {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1rem;
}Form controls
Semtic form controls wrap real HTML controls in the light DOM. That keeps them compatible with normal forms and tools such as Livewire, Alpine, HTMX, and plain browser APIs.
Shared behavior:
- real control elements
- label support
- required marker support
- error content support
- native
inputandchangeevents - attribute forwarding where appropriate
Example:
<semtic-form method="post" action="/login">
<semtic-input
label="Email"
type="email"
name="email"
autocomplete="email"
required
></semtic-input>
</semtic-form>For detailed attributes and rendered shapes, use the generated JSDoc output.
Livewire
Semtic components are light-DOM components and form controls forward unknown
attributes to the real control, so wire:*, x-*, hx-*, aria-*, and
data-* attributes can pass through where appropriate.
For Livewire applications using Sugar/Semtic components, import the optional Sugar Livewire integration before registering Semtic:
import '@skirbi/sugar/livewire';
import '@skirbi/semtic/register-semtic';This helps Livewire compare hydrated trees instead of comparing authored markup to already-rendered component markup.
Documentation
Run JSDoc with:
npm run jsdocThe source files are the documentation input. Each module should have a
file-level @module block and each public component contract field should have
a nearby JSDoc comment.
A custom JSDoc template can later group known Semtic static fields into a nicer component contract section. The source should stay simple: no decorators, no TypeScript syntax, and no second documentation DSL unless the default JSDoc data is not enough.
Example: login page
<html skirbi-theme="client">
<body>
<semtic-page>
<semtic-grid class="login-layout">
<semtic-panel class="login-card">
<semtic-stack>
<semtic-header
title="Welcome back"
subtitle="Sign in to continue"
></semtic-header>
<semtic-form method="post" action="/login">
<semtic-stack>
<semtic-input
label="Email"
type="email"
name="email"
autocomplete="email"
required
></semtic-input>
<semtic-input
label="Password"
type="password"
name="password"
autocomplete="current-password"
required
></semtic-input>
<semtic-flex class="login-actions">
<button type="submit">Login</button>
</semtic-flex>
</semtic-stack>
</semtic-form>
</semtic-stack>
</semtic-panel>
</semtic-grid>
</semtic-page>
</body>
</html>.login-layout {
min-height: 100dvh;
display: grid;
place-items: center;
}
.login-card {
width: min(100%, 28rem);
}
.login-actions {
display: flex;
justify-content: end;
}Philosophy
Semtic is:
- an authoring layer
- semantic-first
- minimal
- framework-independent
- light-DOM only
- accessibility-aware
- designed for normal CSS
Semtic is not:
- a CSS framework
- a UI kit
- a design system
- a utility-class replacement
- a substitute for app-specific CSS
Status
Stable for real-world usage. Designed to evolve slowly. YAGNI by default.
