<custom-element>
Defines a component. The definition holds an optional <script>, any number of <style> and <link rel="stylesheet"> elements, and the markup every instance renders in its shadow root. There is no <template> wrapper and no JavaScript definition API.
<custom-element name="tag-name"
[form-associated] [role="…"] [delegates-focus] [slot="…"]>
<script type="realm">/* runs once per element */</script>
<style type="realm">/* scoped to the component */</style>
<!-- markup -->
</custom-element>type="realm" is for definitions written in a page. In a file loaded with <import-element>, use a plain <script> and <style>. See The script.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>custom-element demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="./click-counter.html"></import-element>
<click-counter step="2"></click-counter>
</body>
</html><custom-element name="click-counter">
<script>
const props = defineProps({ step: prop.number().int().default(1) });
const count = useState({ value: 0 });
function add() {
count.value += props.step;
}
</script>
<style>
:host {
display: inline-flex;
align-items: center;
gap: calc(var(--spacing) * 3);
}
</style>
<ui-button onclick="add()">Add</ui-button>
<output>Count: <slot name="#count.value"></slot></output>
</custom-element>Attributes
| Attribute | Type | Default | Meaning |
|---|---|---|---|
name |
tag name containing - |
required | The element name. Without a dash REALM warns and defines nothing |
form-associated |
boolean | absent | Makes the element a form field (static formAssociated = true). See Forms |
role |
ARIA role | none | Default role, set as internals.role. Page authors can still override it with a role attribute |
delegates-focus |
boolean | absent | Attaches the shadow root with delegatesFocus: true, so focusing the host focuses the first focusable element inside |
slot |
slot name | none | Makes the component a compound part. See slot |
form-associated
A form-associated component:
- takes part in form submission, reset, validation,
<fieldset disabled>and<label>like a native field, - sets its form value from the
valueprop (and fromcheckedwhen it declares one, sendingvalueor"on"while checked), - re-reads its attributes on form reset and restores
valueorcheckedon back/forward navigation, - delegates to an inner
<input>,<select>or<textarea>markedref="control", - reports
props.disabledastruewhile a surrounding fieldset is disabled, and starts disabled when created inside one, - can use
onFormReset,onFormDisabledandonFormStateRestore.
slot
With slot="title" on the definition, every instance gets slot="title" unless it sets its own slot, so it lands in the parent's title slot. A part like this can call its parent's functions and show its parent's state without a script, and those parent lookups do not log the usual reads "name" from <parent> message.
<custom-element name="card-title" slot="title">
<h3><slot></slot></h3>
</custom-element>Children
| Child | Meaning |
|---|---|
First <script> |
The component script. Extra scripts log a warning and are ignored |
<style> |
Adopted into every instance's shadow root. Scoped to the component |
<link rel="stylesheet" href> |
Fetched once (relative to the file that holds the definition) and adopted like <style>. A failed load warns |
| Anything else | Markup, cloned into each instance's open shadow root |
Styles are scanned for var(--attr-*), var(--state-*) and :state(…) so those stay reactive.
The script
The script runs once per element, when it is first connected. Its top-level const, let, var, function and class names and its imports can be referenced from markup (#name.path) and called from handlers. The script helpers are in scope without importing.
| Script | Where | Behaviour |
|---|---|---|
<script type="realm"> |
Definitions written in a page | The browser ignores the type, so the code only runs as the component script. A plain <script> in a page definition has already run in the page scope, and REALM warns |
<script> |
Files loaded with <import-element> |
Files are parsed, not executed, so a plain script is safe |
<script type="module"> |
Either | Use it when the script has import statements, for readability. REALM treats any script with leading import statements as a module |
<script src="setup.js"> |
Either | Loads a pre-assembled setup module produced by the Vite plugin or compileDefinition. It is not a way to load a plain script file |
import statements must come first. Relative specifiers (./store.js) are resolved against the file that holds the definition. A script with a syntax error, or whose import fails, logs a warning and the component renders without it. An error thrown while the script runs is logged with console.error, and the markup still renders.
Use <style type="realm"> in page definitions too, so the browser does not apply the styles to the page before REALM reads them.
Instances
- Each instance gets an open shadow root with the definition's stylesheets and markup.
- Moving an element (removing and re-inserting it in the same task) keeps its state. Removing it tears it down: observers stop, listeners and requests abort,
onDisconnectedhooks run. - The
hiddenattribute always hides a component, whateverdisplayits styles set. - REALM elements written inside a page definition are not mounted while they are part of the definition.
- Defining the same name with the same source twice is ignored. A different source logs a warning and the first is kept. A name another library already registered logs a warning.
Where definitions go
Inline in a page (REALM defines them on start and when they are added later) or in an HTML file loaded with <import-element>. realm.css hides definitions with display: none.