<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.

Previewindex.htmlclick-counter.html
Add Count:
<!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:

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

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.

Edit this page on GitHub · View as Markdown