---
url: /start.md
description: >-
Get started with Elena. Learn how to install the library, build your first
Progressive Web Component, and bundle a library of Progressive Web Components.
---
# Quick start
The fastest way to get started is to include the following directly into your webpage:
```html
```
```html
```
> \[!WARNING]
> This relies on the unpkg CDN and is not recommended for production. For production use, install `@elenajs/core` locally and bundle your components with `@elenajs/bundler`.
## Installation
To install Elena as a dependency in your project, run:
::: code-group
```sh [npm]
npm install @elenajs/core
```
```sh [yarn]
yarn add @elenajs/core
```
```sh [pnpm]
pnpm add @elenajs/core
```
```sh [bun]
bun add @elenajs/core
```
:::
Then import Elena in your component files:
```js
import { Elena } from "@elenajs/core";
```
## Building your first component
There are three types of Progressive Web Components:
1. **Composite Components** that wrap and enhance the HTML composed inside them. All of their HTML and CSS lives in the Light DOM. You could also call these [HTML Web Components](https://adactio.com/journal/20618).
2. **Primitive Components** that are self-contained and render their own HTML. All of their CSS lives in the Light DOM together with the base HTML required for rendering the initial state.
3. **Declarative Components** that are a hybrid of these and utilize [Declarative Shadow DOM](#_3-declarative-components).
> \[!TIP]
> Elena doesn’t force you to think about this taxonomy. They’re all just components, and you choose how to build yours. But since [Progressive Web Components](/components/overview) is a design philosophy rather than a library feature, understanding the distinction between these approaches helps when deciding what fits your use case.
### 1. Composite Components
A Composite Component enhances whatever HTML is composed inside it, applying styling and behavior around it. It has no `render()` method and never touches its children.
::: code-group
```js [JavaScript]
import { Elena } from "@elenajs/core";
/** ░█ [ELENA]: Composite Component example */
export default class Stack extends Elena(HTMLElement) {
static tagName = "my-stack";
static props = ["direction"];
direction = "column";
}
Stack.define();
```
```css [Styles]
@scope (my-stack) {
:scope {
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-flow: column wrap;
flex-direction: column;
gap: 0.5rem;
}
:scope[direction="row"] {
flex-direction: row;
}
}
```
:::
#### Usage
```html
First
Second
Third
```
### 2. Primitive Components
A Primitive Component owns and controls its inner HTML markup. Two things to know to get started:
* **`html`** is Elena’s tagged template function. It auto-escapes interpolated values to prevent XSS, and nested `html` fragments pass through without double-escaping.
* **`this.text`** is a built-in reactive property. Elena captures any text content placed inside the element before hydration, so you can pass text as a child node or set it as a property. Utilizing this helps to avoid layout shifts.
::: code-group
```js [JavaScript]
import { Elena, html } from "@elenajs/core";
/** ░█ [ELENA]: Primitive Component example */
export default class Button extends Elena(HTMLElement) {
static tagName = "my-button";
static props = ["variant"];
variant = "default";
render() {
return html`
${this.text}
`;
}
}
Button.define();
```
```css [Styles]
@scope (my-button) {
:scope,
*:where(:not(img, svg):not(svg *)),
*::before,
*::after {
all: unset;
display: revert;
}
:scope {
--my-button-bg: pink;
display: inline-block;
}
:scope:not([hydrated]),
.my-button:is(button) {
background: var(--my-button-bg);
display: inline-flex;
}
:scope[variant="primary"] {
--my-button-bg: green;
}
:scope[variant="danger"] {
--my-button-bg: red;
}
}
```
:::
#### Usage
```html
Save
Cancel
```
### 3. Declarative Components
A Declarative Component utilizes Declarative Shadow DOM which lets you define a shadow root directly in HTML using a `` element. The browser attaches the shadow root during parsing, so the content is visible before JavaScript loads.
::: code-group
```js [JavaScript]
import { Elena } from "@elenajs/core";
/** ░█ [ELENA]: Declarative Component example */
export default class Button extends Elena(HTMLElement) {
static tagName = "elena-button";
static shadow = "open";
}
Button.define();
```
```html [HTML]
Click me
```
:::
#### Usage
```html [HTML]
Click me
```
In practice, you have to write the `` block by hand every time you use the component, which gets repetitive quickly unless you abstract this duplication away in your own application.
## Usage with frameworks
Elena components are standard custom elements and work in any framework. Import and define your component once, then use it like any HTML element:
::: code-group
```html [HTML]
Save
Cancel
```
```tsx [React]
import "my-design-system";
export default function App() {
return (
Save
Cancel
);
}
```
```tsx [Next.js]
import "my-design-system";
export default function App() {
return (
Save
Cancel
);
}
```
```vue [Vue]
Save
Cancel
```
```svelte [Svelte]
Save
Cancel
```
```ts [Angular]
import "my-design-system";
@Component({
selector: "app-root",
template: `
`,
})
export class AppComponent {}
```
:::
For more, see the [Framework Integration](/advanced/frameworks) examples.
## Bundling components
[@elenajs/bundler](https://github.com/arielsalminen/elena/tree/main/packages/bundler) is the build tool for Elena component libraries. It bundles JavaScript and TypeScript source files, minifies styles, generates a Custom Elements Manifest, and produces TypeScript declarations.
::: code-group
```sh [npm]
npm install --save-dev @elenajs/bundler
```
```sh [yarn]
yarn add --dev @elenajs/bundler
```
```sh [pnpm]
pnpm add --save-dev @elenajs/bundler
```
```sh [bun]
bun add --dev @elenajs/bundler
```
:::
Run the build:
```bash
npx elena build
```
### Bundler configuration
Create an `elena.config.mjs` at the root of your package:
```js
/**
* ░█ [ELENA]: Bundler configuration
*
* @type {import("@elenajs/bundler").ElenaConfig}
*/
export default {
// Source directory scanned for .js/.ts entry files and .css files.
input: "src",
// Rollup output options.
output: {
dir: "dist",
format: "esm",
sourcemap: true,
},
// Entry for the single-file bundle. Set to false to disable.
bundle: "src/index.js",
// Additional Rollup plugins appended after Elena’s built-in set.
// plugins: [],
// Custom Elements Manifest options. Set to false to skip entirely.
// analyze: {
// plugins: [],
// },
// Browserslist targets for transpilation. Enables syntax transforms
// (e.g. class fields, optional chaining) to widen browser support.
// target: ["chrome 71", "firefox 69", "safari 12.1"],
// Custom Terser minifier options, merged with the defaults.
// terser: { ecma: 2020, module: true },
};
```
For more, see the [Component Libraries](/advanced/libraries) guide.
## Command Line Interface
[@elenajs/cli](https://github.com/arielsalminen/elena/tree/main/packages/cli) scaffolds new Elena components interactively. It generates JavaScript, TypeScript, or single-file HTML source files with all Elena patterns pre-configured.
::: code-group
```sh [npm]
npm install --save-dev @elenajs/cli
```
```sh [yarn]
yarn add --dev @elenajs/cli
```
```sh [pnpm]
pnpm add --save-dev @elenajs/cli
```
```sh [bun]
bun add --dev @elenajs/cli
```
:::
Run without arguments to step through all options:
```bash
npx elena-create
```
Or pass a component name to skip the name prompt:
```bash
npx elena-create my-button
npx elena-create my-date-picker
```
For more, see the [CLI documentation](/advanced/cli).
## Next steps
* View the [Live Examples](/examples/) to see Elena in action.
* Browse our [FAQ](/advanced/faq) for frequently asked questions.
* Read the [Component API](/api/) reference to learn about props, events, and lifecycle hooks.
* Learn how to build a full [Component Library](/advanced/libraries).
* Explore [Framework Integration](/advanced/frameworks) examples for React, Vue, Angular, Next.js and more.
* Try Elena in the [Playground](/playground/).
---
---
url: /examples.md
description: >-
Live demo of Elena’s Progressive Web Components on a regular page without any
server-side configuration.
---
# Live examples
This page demonstrates Elena’s [Progressive Web Components](/components/overview) on a regular page without any server-side configuration. This is the recommended way to consume them. It gives you almost identical loading experience compared to using the experimental `@elenajs/ssr` tool. You can compare with a [version that has pre-rendering](/examples/pre-rendered) to see the difference.
## Basic example
```html
Button
Download
```
## Component variants
```html
Primary
Primary
Default
Danger
Outline
Disabled
```
## Component sizes
```html
Small
Medium
Large
```
## Components as links
```html
Elena on GitHub
External
Download
```
## Interactive components
```html
Increment
Reset
```
## Dynamic components
```html
Click to cycle (primary)
primary
```
## Full width components
```html
Full width
```
## Animated components
```html
Loading...
Loading...
Loading...
```
## Styling components
```html
Custom style
Like
```
## Example library
**[@elenajs/components](https://github.com/arielsalminen/elena/tree/main/packages/components)** is a reference component library built with Elena. It demonstrates real-world component patterns and is available as a starting point for your own library.
::: code-group
```sh [npm]
npm install @elenajs/components
```
```sh [yarn]
yarn add @elenajs/components
```
```sh [pnpm]
pnpm add @elenajs/components
```
```sh [bun]
bun add @elenajs/components
```
:::
It includes the following components:
| Component | Tag | Description |
| ---------------------- | ------------------------ | --------------------------------------------------------------- |
| Button | `` | Renders a `` or `` with variants, states, and icons. |
| Spinner | `` | Animated loading indicator that inherits the current color. |
| Stack | `` | Flexbox layout wrapper with configurable direction and gap. |
| Visually Hidden | ``| Hides content visually while keeping it accessible. |
### Usage
Import the full bundle to register all components:
```js
import "@elenajs/components";
```
Or import individual components for better tree-shaking:
```js
import "@elenajs/components/dist/button.js";
import "@elenajs/components/dist/stack.js";
```
Each component has a matching CSS file:
```css
@import "@elenajs/components/dist/button.css";
@import "@elenajs/components/dist/stack.css";
```
Or import the full CSS bundle:
```css
@import "@elenajs/components/dist/bundle.css";
```
The source code is in `packages/components/` of the [Elena monorepo](https://github.com/arielsalminen/elena) and serves as an example of how to structure, build, and publish a library with `@elenajs/bundler`.
## Next steps
* For more details, view [Component Libraries](/advanced/libraries).
* Explore [Framework Integration](/advanced/frameworks) examples for React, Vue, Angular, Next.js and more.
* Browse our [FAQ](/advanced/faq) for frequently asked questions.
* Try Elena in the [Playground](/playground/).
---
---
url: /components/overview.md
description: >-
Learn about Progressive Web Components and the different approaches to
building them to help you decide what fits your use case.
---
# Overview
A [Progressive Web Component](https://arielsalminen.com/2026/progressive-web-components/) is a native Custom Element designed in two layers: a base layer of HTML and CSS that renders immediately, without JavaScript, and an enhancement layer of JavaScript that adds reactivity, event handling, and more advanced templating. There are three types of Progressive Web Components:
## 1. Composite
Composite Components wrap and enhance the HTML composed inside them. All of their HTML and CSS lives in the Light DOM. You could also call these [HTML Web Components](https://adactio.com/journal/20618).
## 2. Primitive
Primitive Components are self-contained and render their own HTML. All of their CSS lives in the Light DOM together with the base HTML required for rendering the initial state.
## 3. Declarative
Declarative Components are a hybrid of these and utilize [Declarative Shadow DOM](/components/templates#declarative-shadow-dom).
> \[!TIP]
> Elena doesn’t force this taxonomy. They’re [all just web components](/advanced/faq#can-i-build-normal-web-components-with-elena), and you choose how to build yours. But since [Progressive Web Components](https://arielsalminen.com/2026/progressive-web-components/) is a design philosophy rather than a library feature, understanding the distinction between these approaches helps when deciding what fits your use case.
## Next steps
* Start with the [Quick Start](/start/) guide.
* View the [Live examples](/examples/) for demos.
* Browse our [FAQ](/advanced/faq) for frequently asked questions.
---
---
url: /components/lifecycle.md
description: >-
Understand Elena’s Progressive Web Component lifecycle, from connection and
rendering to updates and disconnection.
---
# Lifecycle
Elena’s component lifecycle has two parts: the standard custom element lifecycle, and a reactive update cycle triggered by property changes.
Updates are batched asynchronously: if multiple props change before the update starts, all changes are captured in the same update. See [rendering](/components/rendering) for more details.
## `connectedCallback()`
Runs when the element is added to the page. Sets up props, captures text content, renders, and wires up events.
## `willUpdate()`
Runs before every render, including the first. Override to compute derived state before the template evaluates.
## `render()`
Returns the HTML for this component as an `html` template. Called on connect and whenever the component needs re-rendering.
## `firstUpdated()`
Runs once after the first render. `this.element` is available here. Override to run one-time setup that requires the DOM.
## `updated()`
Runs after every render, including the first. `this.element` is available here. Override to react to changes after the DOM is updated. On first connect, `firstUpdated()` runs before `updated()`.
## `requestUpdate()`
Manually schedules a re-render. Use this when Elena can’t detect a change automatically. For example, when mutating an object or array in place:
```js
this.items.push("new item");
this.requestUpdate();
```
## `updateComplete`
A `Promise` that resolves after the next pending render finishes. Use it to wait for the DOM to settle before reading it:
```js
this.label = "Updated";
await this.updateComplete;
console.log(this.element.textContent); // "Updated"
```
Resolves immediately if no render is pending.
## `disconnectedCallback()`
Runs when the element is removed from the page. Cleans up event listeners.
## `adoptedCallback()`
Runs when the element is moved to a new document via `document.adoptNode()`. Override to react to document changes:
```js
adoptedCallback() {
super.adoptedCallback();
// react to the document change
}
```
## `attributeChangedCallback()`
Runs when an observed attribute changes. Updates the matching JavaScript property and triggers a re-render. Override to react to attribute changes directly:
```js
attributeChangedCallback(prop, oldValue, newValue) {
super.attributeChangedCallback(prop, oldValue, newValue);
// react to the change
}
```
| Parameter | Type | Description |
| --- | --- | --- |
| `prop` | `string` | The attribute name that changed. |
| `oldValue` | `string \| null` | The previous attribute value, or `null` if it wasn’t set. |
| `newValue` | `string \| null` | The new attribute value, or `null` if the attribute was removed. |
---
---
url: /components/rendering.md
description: >-
Learn how Elena detects changes, batches updates, and efficiently re-renders
components.
---
# Rendering
Elena renders a web component once when it connects to the page. After that, any change to a [reactive property](/components/props) triggers an update. Elena performs updates asynchronously so property changes are batched: if there are multiple changes, they are combined into a single render.
## How the DOM is updated
When Elena updates, it calls `render()` and compares the result to what is currently in the DOM. It uses two strategies depending on what changed:
* **Patch:** if the template shape is the same, Elena patches only the text nodes and attribute values that changed. The DOM structure stays intact, preserving element identity, focus state, and scroll position.
* **Morph:** if the template shape changed, Elena rebuilds the affected portion of the DOM and morphs it into place, updating attributes and text in existing nodes where possible.
## What triggers an update
Elena schedules an update when any of these change:
* **A prop value** listed in `static props` is set to a new value
* **The `text` property** is updated
* **An observed attribute** changes on the host element
* **`requestUpdate()`** is called manually
All four go through the same path: Elena schedules a batched update, then runs the [update cycle](/components/lifecycle). Only the parts of the DOM that changed are [updated](#how-the-dom-is-updated).
## How prop changes are detected
Props listed in `static props` are backed by JavaScript getters and setters. When you assign a new value, the setter compares it to the current value and schedules a re-render if it changed:
```js
// This will trigger an update
this.variant = "primary";
// This does not: the value is the same as above
this.variant = "primary";
```
Elena uses strict equality (`===`) to compare values. If the new value is the same reference as the old one, no render is scheduled.
### Reflected vs non-reflected props
By default, props reflect to HTML attributes. When a reflected prop changes, Elena updates the attribute, which triggers `attributeChangedCallback()`, which schedules the update. Non-reflected props (declared with `{ name, reflect: false }`) skip the attribute and schedule the update directly from the setter.
## Batched updates
Elena never renders synchronously in response to a prop change. Instead, it queues a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) the first time a change is detected. If more props change before the microtask runs, they are all included in the same update:
```js
// These three changes produce one update, not three
button.variant = "primary";
button.disabled = true;
button.text = "Save";
```
Because microtasks run before the browser paints, the DOM is updated in time for the next frame. There is no visible flicker between changes.
## What Elena cannot detect
Elena tracks prop changes through setters. If a value is mutated in place without going through the setter, Elena has no way to know it changed.
This applies to:
* **Pushing to an array:** `this.items.push("new")` mutates the existing array. The setter never fires because the array reference stays the same.
* **Changing a nested property:** `this.user.name = "Alex"` mutates the object in place.
* **Mutating any object or array** without reassigning the prop.
In these cases, call `requestUpdate()` to tell Elena that something changed:
```js
this.items.push("new item");
this.requestUpdate();
```
Alternatively, replace the value with a new reference. This goes through the setter and triggers a render automatically:
```js
this.items = [...this.items, "new item"];
```
> \[!TIP]
> For simple cases, `requestUpdate()` is fine. For components that update lists or nested data frequently, replacing the value with a new reference is more predictable because it works the same as any other prop change.
## The update cycle
Each render follows the same sequence:
1. `willUpdate()` runs first, so you can compute derived state before the template evaluates.
2. `render()` is called to produce the new template shape.
3. Elena updates the DOM using the strategy [described above](#how-the-dom-is-updated).
4. `updated()` runs after the DOM is updated.
On the very first render, `firstUpdated()` also runs, right before `updated()`. See [Lifecycle](./lifecycle) for the full callback reference.
## Waiting for a render
Use `updateComplete` to wait for the current render to finish:
```js
this.text = "Updated";
await this.updateComplete;
console.log(this.element.textContent); // "Updated"
```
If no render is pending, `updateComplete` resolves immediately. See [Methods](./methods) for more details.
## Before the first render
Props can be set on an element before it connects to the page. Elena stores these values internally, but does not render until `connectedCallback()` runs. This means you can safely configure a component before adding it to the DOM:
```js
const button = document.createElement("elena-button");
button.variant = "primary";
button.text = "Save";
document.body.appendChild(button); // First render happens here
```
---
---
url: /components/options.md
description: >-
Reference for all Elena static class field options including tagName, props,
events, element, shadow, and styles.
---
# Options
Elena components are configured using static class fields:
```js
/**
* ░█ [ELENA]: Options
*/
export default class Button extends Elena(HTMLElement) {
// Custom element tag name to register.
static tagName = "elena-button";
// Props to observe and sync as attributes.
static props = ["variant", "disabled"];
// Events to delegate from the inner element.
static events = ["click", "focus", "blur"];
// CSS selector for the inner element ref (this.element).
// Omit when the target is the firstElementChild (faster default).
static element = ".my-button";
// Enable Shadow DOM (opt-in for those who need it).
static shadow = "open";
// Stylesheets to adopt into the shadow root.
// Only required when using Shadow DOM.
static styles = styles;
// Scoped registry for this component's shadow root.
// Only applies when using Shadow DOM.
static registry = registry;
}
// Register the custom element after the class body is defined.
Button.define();
```
> \[!TIP]
> All options are optional. `static element` is a CSS selector for the inner element ref (`this.element`), used for event delegation and direct DOM access. When omitted, Elena uses `firstElementChild` instead, if available.
## Option reference
| Field | Type | Description |
|-------|------|-------------|
| `tagName` | `string` | The HTML tag name for this component (e.g. `"elena-button"`). Required for `define()` to register the element. |
| `props` | `(string \| { name: string, reflect?: boolean })[]` | The list of props this component accepts. Each prop stays in sync with its matching HTML attribute. Use `{ name, reflect: false }` to keep a prop JS-only without writing it back to the attribute. |
| `events` | `string[]` | Events to forward from the inner element up to the host (e.g. `["click", "focus", "blur"]`). Elena sets up listeners automatically and removes them when the element is disconnected. |
| `element` | `string` | A CSS selector for the inner element that `this.element` points to (e.g. `".inner"`, `"button"`). Defaults to the first child element when omitted. `this.element` is available in `render()`, lifecycle methods, and custom methods. |
| `shadow` | `"open" \| "closed"` | Attaches a shadow root to the host element. Elena renders into the shadow root instead of the host, fully isolating styles and DOM from the rest of the page. |
| `styles` | `CSSStyleSheet \| string \| (CSSStyleSheet \| string)[]` | One or more stylesheets to adopt into the shadow root. Only applies when `shadow` is also set. Pass a `CSSStyleSheet` via [CSS Module Scripts](https://web.dev/articles/css-module-scripts), or a raw CSS string. |
| `registry` | `CustomElementRegistry` | A [scoped registry](#scoped-registration) to associate with this component's shadow root. Child custom elements inside the shadow root are resolved from this registry instead of the global one. Only applies when `shadow` is also set. Chrome 146+, Edge 146+, Safari 26+. |
## Registering a component
Call `define()` on the class to register it as a custom element. Elena reads the tag name from `static tagName` and includes SSR guards and prevents double-registration:
```js
Button.define();
```
### Scoped registration
Pass a `CustomElementRegistry` to `define()` to register a component in a [Scoped Custom Element Registry](https://developer.chrome.com/blog/scoped-registries) instead of the global one:
```js
const registry = new CustomElementRegistry();
Button.define(registry);
```
Scoped registries isolate custom element definitions per shadow root. For details about browser compatibility, see [known issues](/advanced/gotchas).
## Next steps
* For more on declaring props, default values, and reflection, see [Props](./props).
---
---
url: /components/props.md
description: >-
Learn how to declare, type, and use props in Elena components, including
attribute syncing, reflection, and supported types.
---
# Props
Props are values you pass to an Elena component. Declare them in `static props` and Elena will keep them in sync with HTML attributes, and re-render the components whenever they change.
## Declaring props
List prop names in `static props`, and give each one a default value as a class field:
```js
/**
* ░█ [ELENA]: Props
*/
export default class Button extends Elena(HTMLElement) {
static props = ["variant", "disabled", "value", "type"];
variant = "default";
disabled = false;
value = "";
type = "button";
}
```
The default value also tells Elena what type the prop is. A default of `false` means it’s a boolean, `0` means a number, `[]` means an array, and so on. Elena uses this to convert the incoming attribute string to the right type.
> \[!TIP]
> When naming props, try to keep them simple and a maximum of one word (e.g. `variant`).
## Built-in props
Every Elena element has a built-in `text` prop. On first connect, Elena automatically captures this text content from the light DOM, so you can pass text naturally as children:
```html
Click me
```
Use `this.text` in `render()` to reference it. Elena trims leading and trailing whitespace when capturing text content, so ` Click me ` gives `this.text === "Click me"`.
```js
render() {
return html`${this.text} `;
}
```
Setting `text` programmatically also triggers a re-render:
```js
document.querySelector("my-button").text = "Save changes";
```
## Non-reflected props
By default, Elena writes all props back to the host element as HTML attributes. To turn this off for a specific prop, use the object form:
```js
export default class Button extends Elena(HTMLElement) {
static props = [
"variant",
{ name: "icon", reflect: false },
];
variant = "default";
icon = "";
}
```
`icon` still works as `this.icon` in JavaScript and updates the component when changed, but Elena won’t add it as an attribute on the element.
## Documenting props
Document each prop with JSDoc annotations. `@property` marks the field as a component prop, and `@type` describes the expected value.
::: code-group
```js [JavaScript]
/**
* The style variant of the button.
* @property
* @type {"default" | "primary" | "danger"}
*/
variant = "default";
/**
* Makes the component disabled.
* @property
* @type {Boolean}
*/
disabled = false;
/**
* The value used to identify the button in forms.
* @property
* @type {string}
*/
value = "";
```
```ts [TypeScript]
/**
* The style variant of the button.
* @property
*/
variant: "default" | "primary" | "danger" = "default";
/**
* Makes the component disabled.
* @property
*/
disabled: boolean = false;
/**
* The value used to identify the button in forms.
* @property
*/
value: string = "";
```
:::
> \[!TIP]
> **`@elenajs/bundler`** transforms these annotations into TypeScript types and a Custom Elements Manifest, giving IDEs and documentation tools rich information about your components.
## Prop types
Elena supports the following prop types. These are also picked up by `@elenajs/bundler` and included in the Custom Elements Manifest:
::: code-group
```js [JavaScript]
/** @type {string} */
/** @type {Number} */
/** @type {Array} */
/** @type {Boolean} */
/** @type {Object} */
/** @type {"default" | "primary" | "danger"} */
```
```ts [TypeScript]
// Use inline type annotations instead of @type in TypeScript:
variant: "default" | "primary" | "danger" = "default";
disabled: boolean = false;
value: string = "";
count: number = 0;
items: string[] = [];
```
:::
## Naming props
Keep prop names short and single-word where possible (e.g. `variant`). Avoid names that conflict with existing DOM prototype members. Reserved names include:
`align`, `title`, `lang`, `translate`, `dir`, `dataset`, `hidden`, `tabIndex`, `accessKey`, `draggable`, `spellcheck`, `autocapitalize`, `contentEditable`, `isContentEditable`, `inputMode`, `offsetParent`, `offsetTop`, `offsetLeft`, `offsetWidth`, `offsetHeight`, `style`, `innerText`, `outerText`, `oncopy`, `oncut`, `onpaste`, `onabort`, `onblur`, `oncancel`, `oncanplay`, `oncanplaythrough`, `onchange`, `onclick`, `onclose`, `oncontextmenu`, `oncuechange`, `ondblclick`, `ondrag`, `ondragend`, `ondragenter`, `ondragleave`, `ondragover`, `ondragstart`, `ondrop`, `ondurationchange`, `onemptied`, `onended`, `onerror`, `onfocus`, `onfocusin`, `onfocusout`, `oninput`, `oninvalid`, `onkeydown`, `onkeypress`, `onkeyup`, `onload`, `onloadeddata`, `onloadedmetadata`, `onloadstart`, `onmousedown`, `onmouseenter`, `onmouseleave`, `onmousemove`, `onmouseout`, `onmouseover`, `onmouseup`, `onmousewheel`, `onpause`, `onplay`, `onplaying`, `onprogress`, `onratechange`, `onreset`, `onresize`, `onscroll`, `onseeked`, `onseeking`, `onselect`, `onstalled`, `onsubmit`, `onsuspend`, `ontimeupdate`, `ontoggle`, `onvolumechange`, `onwaiting`, `onwheel`, `onauxclick`, `ongotpointercapture`, `onlostpointercapture`, `onpointerdown`, `onpointermove`, `onpointerup`, `onpointercancel`, `onpointerover`, `onpointerout`, `onpointerenter`, `onpointerleave`, `onselectstart`, `onselectionchange`, `nonce`, `click`, `focus`, `blur`, `namespaceURI`, `prefix`, `localName`, `tagName`, `id`, `className`, `classList`, `slot`, `attributes`, `shadowRoot`, `assignedSlot`, `innerHTML`, `outerHTML`, `scrollTop`, `scrollLeft`, `scrollWidth`, `scrollHeight`, `clientTop`, `clientLeft`, `clientWidth`, `clientHeight`, `attributeStyleMap`, `onbeforecopy`, `onbeforecut`, `onbeforepaste`, `onsearch`, `previousElementSibling`, `nextElementSibling`, `children`, `firstElementChild`, `lastElementChild`, `childElementCount`, `onfullscreenchange`, `onfullscreenerror`, `onwebkitfullscreenchange`, `onwebkitfullscreenerror`, `setPointerCapture`, `releasePointerCapture`, `hasPointerCapture`, `hasAttributes`, `getAttributeNames`, `getAttribute`, `getAttributeNS`, `setAttribute`, `setAttributeNS`, `removeAttribute`, `removeAttributeNS`, `hasAttribute`, `hasAttributeNS`, `toggleAttribute`, `getAttributeNode`, `getAttributeNodeNS`, `setAttributeNode`, `setAttributeNodeNS`, `removeAttributeNode`, `closest`, `matches`, `webkitMatchesSelector`, `attachShadow`, `getElementsByTagName`, `getElementsByTagNameNS`, `getElementsByClassName`, `insertAdjacentElement`, `insertAdjacentText`, `insertAdjacentHTML`, `requestPointerLock`, `getClientRects`, `getBoundingClientRect`, `scrollIntoView`, `scroll`, `scrollTo`, `scrollBy`, `scrollIntoViewIfNeeded`, `animate`, `computedStyleMap`, `before`, `after`, `replaceWith`, `remove`, `prepend`, `append`, `querySelector`, `querySelectorAll`, `requestFullscreen`, `webkitRequestFullScreen`, `webkitRequestFullscreen`, `part`, `createShadowRoot`, `getDestinationInsertionPoints`, `nodeType`, `nodeName`, `baseURI`, `isConnected`, `ownerDocument`, `parentNode`, `parentElement`, `childNodes`, `firstChild`, `lastChild`, `previousSibling`, `nextSibling`, `nodeValue`, `textContent`, `hasChildNodes`, `getRootNode`, `normalize`, `cloneNode`, `isEqualNode`, `isSameNode`, `compareDocumentPosition`, `contains`, `lookupPrefix`, `lookupNamespaceURI`, `isDefaultNamespace`, `insertBefore`, `appendChild`, `replaceChild`, `removeChild`.
---
---
url: /components/events.md
description: >-
Learn how to use events with Elena, fire custom events, and document events in
the Custom Elements Manifest.
---
# Events
## Delegated events
When a web component renders its own inner DOM via `render()`, native events fire on the inner element (e.g. the ``). Bubbling events like `click` reach the host naturally, but non-bubbling events like `focus` and `blur` do not.
`static events` tells Elena which events to manage. Non-bubbling events are forwarded from the inner element to the host, so consumers can attach listeners to the custom element directly:
```js
/**
* ░█ [ELENA]: Events
*/
export default class Button extends Elena(HTMLElement) {
static events = ["click", "focus", "blur"];
}
```
Consumers can then listen on the host element as usual:
::: code-group
```html [HTML]
Click me
```
```html [Angular]
Click me
```
```tsx [Next.js]
export default function App() {
return (
console.log("clicked!", e)}>
Click me
);
}
```
```tsx [React]
function App() {
return (
console.log("clicked!", e)}>
Click me
);
}
```
```svelte [Svelte]
console.log("clicked!", e)}>
Click me
```
```vue [Vue]
Click me
```
:::
Elena sets up the listeners automatically and removes them when the element is disconnected from the DOM.
> \[!TIP]
> Bubbling events (like `click`, `change`, `input`) pass through to the host naturally with all their original properties intact. Non-bubbling events (like `focus` and `blur`) are forwarded to the host as plain `Event` instances.
If you need more control, you can also manage event listeners manually. Add them in `connectedCallback` and remove them in `disconnectedCallback`:
```js
export default class Button extends Elena(HTMLElement) {
connectedCallback() {
super.connectedCallback();
this.addEventListener("click", this._onClick);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("click", this._onClick);
}
_onClick = e => {
console.log("clicked!", e);
};
}
```
> \[!WARNING]
> Avoid defining your own `handleEvent` method on a component that uses `static events`. Elena uses `handleEvent` internally for event delegation, and overriding it will break the built-in behavior.
## Custom events
Use the standard `CustomEvent` to fire your own events from inside a component:
```js
this.dispatchEvent(new CustomEvent("my-event", {
bubbles: true,
composed: true,
detail: { value: 42 },
}));
```
## Documenting events
Use the `@event` JSDoc tag on the component class to document which events a component fires. These are picked up by `@elenajs/bundler` and included in the Custom Elements Manifest:
```js
/**
* @event click - Programmatically fire click on the component.
* @event focus - Programmatically move focus to the component.
* @event blur - Programmatically remove focus from the component.
*/
export default class Button extends Elena(HTMLElement) { /*...*/ }
```
---
---
url: /components/methods.md
description: >-
Learn about Elena’s lifecycle methods, utility methods, and how to define
custom methods on your Progressive Web Components.
---
# Methods
Elena extends the standard [custom elements lifecycle](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks) with a reactive update cycle. All lifecycle methods can be extended by calling `super`.
## Lifecycle methods
### `connectedCallback()`
Runs when the element is added to the page. Sets up props, captures text content, renders, and wires up events.
```js
connectedCallback() {
super.connectedCallback();
console.log("Element was added to the DOM.");
}
```
### `willUpdate()`
Runs before every render, including the first. Use this to compute derived values before the template evaluates:
```js
willUpdate() {
this.label = `${this.firstName} ${this.lastName}`;
}
```
### `render()`
Returns the component’s HTML as an `html` template. Called on connect and on every re-render. Composite Components that don’t own their inner HTML don’t use this method. See [Templates](./templates) for full details.
```js
render() {
return html`${this.text} `;
}
```
### `firstUpdated()`
Runs once after the first render. `this.element` is available here. Use it for one-time setup that needs the DOM:
```js
firstUpdated() {
console.log("DOM first updated.");
}
```
### `updated()`
Runs after every render, including the first. `this.element` is available here. On first connect, `firstUpdated()` runs before `updated()`:
```js
updated() {
console.log("DOM updated, current value:", this.element.textContent);
}
```
### `disconnectedCallback()`
Runs when the element is removed from the page. Cleans up event listeners.
```js
disconnectedCallback() {
super.disconnectedCallback();
console.log("Element was removed from the DOM.");
}
```
### `attributeChangedCallback()`
Runs when an observed attribute changes. Updates the matching JavaScript property and triggers a re-render. Override to react to attribute changes directly:
```js
attributeChangedCallback(prop, oldValue, newValue) {
super.attributeChangedCallback(prop, oldValue, newValue);
console.log(`${prop} changed from ${oldValue} to ${newValue}`);
}
```
## Utility methods
### `requestUpdate()`
Manually schedules a re-render. Use this when Elena can’t detect a change automatically, for example when mutating an object or array in place:
```js
this.items.push("new item");
this.requestUpdate();
```
### `updateComplete`
A `Promise` that resolves after the next pending render finishes. Use it to wait for the DOM to settle before reading it:
```js
this.label = "Updated";
await this.updateComplete;
console.log(this.element.textContent); // "Updated"
```
### `ClassName.define()`
Registers the component with the browser using the `static tagName` field. Call this once after defining the class. Does nothing in non-browser environments.
```js
MyElement.define();
```
## Custom methods
You can define your own methods on any Elena component:
```js
export default class Button extends Elena(HTMLElement) {
focus() {
this.element?.focus();
}
}
```
Use `@internal` in JSDoc to mark methods that are implementation details and should not be part of the public API:
```js
/** @internal */
_buildLabel() {
return `${this.firstName} ${this.lastName}`;
}
```
---
---
url: /components/mixins.md
description: >-
Learn how to write and compose mixins to share behavior across Elena
Progressive Web Components.
---
# Mixins
Mixins are a standard JavaScript pattern for sharing behavior across [Progressive Web Components](/components/overview). Since Elena’s `Elena()` factory is itself a mixin, custom mixins compose naturally with it.
## Writing a mixin
A mixin is a function that takes a base class and returns an extended class:
```js
const Draggable = (superClass) => class extends superClass {
connectedCallback() {
super.connectedCallback();
this.setAttribute("draggable", "true");
this.addEventListener("dragstart", this._onDragStart);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("dragstart", this._onDragStart);
}
_onDragStart(e) {
e.dataTransfer.setData("text/plain", this.text);
}
};
```
## Using a mixin
Apply a mixin by wrapping it around the Elena base class:
```js
import { Elena, html } from "@elenajs/core";
class DraggableCard extends Draggable(Elena(HTMLElement)) {
static tagName = "draggable-card";
render() {
return html`${this.text}
`;
}
}
DraggableCard.define();
```
The mixin’s `connectedCallback` calls `super.connectedCallback()`, which reaches Elena’s built-in lifecycle. The result is a component with both Elena’s reactive rendering and your custom behavior.
## Composition order
Always apply mixins **after** `Elena()`:
```js
class MyComponent extends Draggable(Elena(HTMLElement)) { }
```
This way the mixin’s `super.connectedCallback()` reaches Elena’s implementation and has access to its API.
## Lifecycle rules
Elena’s lifecycle methods follow the standard `super` call pattern. Mixins that override lifecycle methods **must call `super`** to preserve Elena’s behavior:
| Method | Must call `super`? |
|---|---|
| `connectedCallback()` | Yes |
| `disconnectedCallback()` | Yes |
| `adoptedCallback()` | Yes |
| `attributeChangedCallback()` | Yes |
| `firstUpdated()` | Yes |
| `updated()` | Yes |
| `willUpdate()` | No (base is a no-op) |
## Multiple mixins
Mixins stack. Apply them left to right, with each wrapping the previous:
```js
const Loggable = (superClass) => class extends superClass {
connectedCallback() {
super.connectedCallback();
console.log(`${this.tagName} connected`);
}
};
```
```js
const Tooltipped = (superClass) => class extends superClass {
connectedCallback() {
super.connectedCallback();
if (this.getAttribute("tooltip")) {
this._setupTooltip();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this._teardownTooltip();
}
_setupTooltip() { /* ... */ }
_teardownTooltip() { /* ... */ }
};
```
```js
class FancyButton extends Loggable(Tooltipped(Elena(HTMLElement))) {
static tagName = "fancy-button";
// ...
}
FancyButton.define();
```
## Adding props
Mixins can introduce their own props. List them in `static props` on the final component class so Elena can observe them:
```js
const Sizeable = (superClass) => class extends superClass {
/** @property @type {"sm" | "md" | "lg"} */
size = "md";
};
```
```js
class SizeableButton extends Sizeable(Elena(HTMLElement)) {
static tagName = "sizeable-button";
static props = ["size", "variant"];
/** @property @type {"default" | "primary"} */
variant = "default";
render() {
return html`${this.text} `;
}
}
SizeableButton.define();
```
> \[!TIP]
> Props must always be listed in `static props` on the component class for Elena to observe and sync them. A mixin can provide the default value and logic, but the final class is responsible for registering the prop.
---
---
url: /components/templates.md
description: >-
Learn how to write component templates using Elena’s html tagged template
literal, including conditionals, loops, and XSS-safe interpolation.
---
# Templates
Elena uses an HTML-based template syntax built on JavaScript [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals).
## `html`
Use the `html` tagged template to write your component’s markup:
```js
import { html } from "@elenajs/core";
render() {
return html`
${this.text}
`;
}
```
Values you interpolate are escaped automatically to prevent XSS. Nested `html` fragments are passed through as trusted markup without double-escaping:
```js
render() {
const badge = html`${this.count} `;
return html`
${this.text} ${badge}
`;
}
```
Arrays of `html` fragments are rendered as HTML, so you can use `.map()` to render lists:
```js
render() {
return html`
${this.items.map(item => html`${item} `)}
`;
}
```
Templates can also have multiple root elements:
```js
render() {
return html`
${this.label}
`;
}
```
## `nothing`
Use `nothing` in conditional template expressions when there is nothing to render. It always produces an empty string and signals the template engine that no processing is needed:
```js
import { html, nothing } from "@elenajs/core";
render() {
return html`
${this.icon ? html`${this.icon} ` : nothing}
${this.text}
`;
}
```
Prefer `nothing` over `""` or `false` in template expressions. Empty strings and boolean false can produce unexpected whitespace or output.
## `unsafeHTML`
Values interpolated into `html` are auto-escaped to prevent XSS. `unsafeHTML` lets you render a plain string as raw HTML, skipping the escaping. Only use this for content you fully control, such as an SVG icon or trusted server markup:
```js
import { html, unsafeHTML, nothing } from "@elenajs/core";
render() {
const icon = this.icon ? unsafeHTML(`${this.icon} `) : nothing;
const text = this.text ? html`${this.text} ` : nothing;
return html`
${text} ${icon}
`;
}
```
> \[!WARNING]
> Only use `unsafeHTML` with content you control. Never pass user-supplied strings to it.
## Element ref
When `static element` is set, Elena resolves `this.element` after the first render, giving you direct access to the inner DOM element. Use it in `firstUpdated()`, `updated()`, or any custom method:
```js
export default class Button extends Elena(HTMLElement) {
static element = ".my-button";
updated() {
this.element.focus();
}
}
```
See [Options](./options) for details on `static element`.
## Text content
Every Elena element has a built-in reactive `text` prop. On first connect, Elena captures the element’s text content from the light DOM, so you can pass text naturally as children:
```html
Click me
```
Use `this.text` in `render()` to reference it:
```js
render() {
return html`${this.text} `;
}
```
Setting `text` programmatically triggers a re-render:
```js
const button = document.querySelector("elena-button");
button.text = "Save changes";
```
When used with frameworks, static text as children works fine. For dynamic text that changes over time, use the `text` property instead, since components with `render()` own their internal DOM and frameworks can’t update children after Elena has rendered:
```html
// React
// Angular
// Vue
```
## Advanced examples
### Rendering lists
Use `.map()` to render arrays of data as repeated markup. Each array element can be an `html` fragment, a plain string, or `nothing`:
```js
render() {
return html`
${this.links.map(link =>
link.visible
? html`${link.label} `
: nothing
)}
`;
}
```
For components where the list data is a prop, use `willUpdate()` to derive the filtered or transformed list before rendering:
```js
willUpdate() {
this._visibleLinks = this.links.filter(link => link.visible);
}
render() {
return html`
${this._visibleLinks.map(link =>
html`${link.label} `
)}
`;
}
```
### Conditional attributes
You can conditionally add or remove HTML attributes by interpolating a string or `nothing`:
```js
render() {
return html`
${this.text}
`;
}
```
### Helper render methods
For components that can render as different elements (e.g. a button that becomes a link when `href` is set), split the logic into helper methods and compose them in `render()`:
```js
import { html, unsafeHTML, nothing } from "@elenajs/core";
/** @internal */
renderButton(template) {
return html`
${template}
`;
}
/** @internal */
renderLink(template) {
return html`
${template}
`;
}
render() {
const icon = this.icon ? unsafeHTML(`${this.icon} `) : nothing;
const markup = html`
${this.text ? html`${this.text} ` : nothing}
${icon}
`;
return this.href ? this.renderLink(markup) : this.renderButton(markup);
}
```
### Multi-root template
Templates can return multiple root elements. Useful for components that pair a label with an input:
```js
render() {
return html`
${this.label}
${this.error ? html`${this.error}
` : nothing}
`;
}
```
### Declarative Shadow DOM
Declarative Shadow DOM lets you define a shadow root directly in HTML using a `` element. The browser attaches the shadow root during parsing, so the shadow content is visible before JavaScript loads.
When a component with `static shadow` connects and finds a shadow root already attached, Elena skips `attachShadow()` and works with the existing one instead. Content stays in the light DOM and is projected into the shadow root via ``:
::: code-group
```html [HTML]
Click me
```
```js [JavaScript]
import { Elena } from "@elenajs/core";
export default class Button extends Elena(HTMLElement) {
static tagName = "elena-button";
static shadow = "open";
}
Button.define();
```
:::
In practice, you have to write the `` block by hand every time you use the component, which gets repetitive quickly unless you abstract this duplication away in your own application. `@elenajs/ssr` may later get Declarative Shadow DOM support which would eliminate that entirely, but this isn’t currently on our roadmap.
For now, Declarative Shadow DOM is mainly useful when you need Shadow DOM style isolation and want the component to be visible before JavaScript loads.
---
---
url: /components/styles.md
description: >-
These guidelines cover the approaches we recommend when styling Progressive
Web Components to make them work reliably across the custom element lifecycle.
---
# Styles
These guidelines cover the approaches we recommend when styling [Progressive Web Components](/components/overview) to make them work reliably across the custom element lifecycle. You can craft the CSS however works best for your project, but the patterns below help avoid some common pitfalls.
## Writing scoped styles
Elena recommends the [@scope](https://caniuse.com/css-cascade-scope) at-rule to prevent component styles from leaking out to the rest of the page:
```css
@scope (elena-button) {
/**
* Scoped styles for the elena-button. These won’t leak
* out or affect any other elements in your app.
*/
}
```
To style the host element itself, use `:scope`:
```css
@scope (elena-button) {
/* Targets the host element (elena-button) */
:scope {
all: unset;
display: inline-block;
}
}
```
### Preventing styles from leaking in
`@scope` stops your styles from leaking out, but it does not prevent global styles from leaking in. To prevent global styles from reaching in, add a universal reset as the first rule inside `@scope`:
```css
/* Scope makes sure styles don’t leak out */
@scope (elena-button) {
/* Reset makes sure styles don’t leak in */
:scope,
*:where(:not(img, svg):not(svg *)),
*::before,
*::after {
all: unset;
display: revert;
}
/* Rest of your component styles */
}
```
For projects that use [CSS cascade layers](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers), you can also control which styles win by declaring a layer order. Wrap your component styles in a named layer, then declare it after your base layer so it takes precedence:
```css
/* Global CSS layer order */
@layer global, elena;
/* Global CSS layer */
@layer global {
button { color: red; }
}
```
```css
/* Component CSS layer */
@layer elena {
@scope (elena-button) {
button { color: blue; }
}
}
```
## CSS pre-hydration styles
For components with `render()` specifically, our recommendation is to ship them with CSS styles that visually match the loading and `hydrated` states. This can be achieved utilizing the provided `hydrated` attribute in your web component’s styles:
```css
/* Elena CSS pre-hydration styles */
:scope:not([hydrated]),
.element { ... }
```
Since both selectors now share the same baseline styles, there are no visible layout shifts, FOUC, or FOIC (Flash Of Unstyled Content, Flash Of Invisible Content).
Sometimes you may need access to more than just the initial text content pre-hydration to avoid layout shifts. This can be achieved with pseudo elements in CSS by referencing the attributes set on the host:
```css
:scope:not([hydrated])::before {
content: attr(label);
}
:scope:not([hydrated])::after {
content: attr(placeholder);
}
```
For more details, see the [Server Side Rendering](/advanced/ssr) section.
> \[!TIP]
> You can skip this section entirely for components without `render()`, when you plan to [hide components until loaded](/advanced/loading#hide-until-loaded), when using `elenajs/ssr`, or when the rest of your app renders client side only.
## Styling variants and states
Use attribute selectors on `:scope` for variant and state styling:
```css
:scope[variant="primary"] { color: red }
:scope[disabled] { opacity: 0.5 }
```
## Full example
Here’s a full example using these patterns:
```css
/* Scope makes sure styles don’t leak out */
@scope (elena-button) {
/* Reset makes sure styles don’t leak in */
:scope,
*:where(:not(img, svg):not(svg *)),
*::before,
*::after {
all: unset;
display: revert;
}
/* Targets the host element (elena-button) */
:scope {
/* Public theming API (with default values set) */
--_elena-button-bg: var(--elena-button-bg, blue);
--_elena-button-text: var(--elena-button-text, white);
--_elena-button-font: var(--elena-button-font, system-ui, sans-serif);
/* Internal theming API references (usage) */
background-color: var(--_elena-button-bg);
color: var(--_elena-button-text);
/* Display mode for the host element */
display: inline-block;
}
/* CSS pre-hydration styles */
:scope:not([hydrated]),
.elena-button {
font-family: var(--_elena-button-font);
color: var(--_elena-button-text);
background: var(--_elena-button-bg);
display: inline-block;
}
/* Rest of your component styles */
:scope[variant="primary"] {
--_elena-button-bg: var(--elena-button-bg, red);
}
}
```
## Composite Components
Composite Components style the host element and can pass styles down to their composed children. Since they never render their own internal markup, there are no pre-hydration concerns:
```css
/* Scope makes sure styles don’t leak out */
@scope (elena-stack) {
/* Targets the host element (elena-stack) */
:scope {
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-flow: column wrap;
flex-direction: column;
gap: 0.5rem;
}
/* Attributes provide customization */
:scope[direction="row"] {
flex-direction: row;
}
}
```
## Theming API
Define public CSS custom properties on `:scope` to expose a theming API for consumers:
```css
/* Component definition */
@scope (elena-button) {
:scope {
/* Public theming API (with default values set) */
--_elena-button-bg: var(--elena-button-bg, blue);
--_elena-button-text: var(--elena-button-text, white);
/* Internal theming API references (usage) */
background-color: var(--_elena-button-bg);
color: var(--_elena-button-text);
}
}
```
Consumers can override these from outside the component:
```css
/* Consumer override */
elena-button {
--elena-button-bg: green;
}
```
Custom properties cascade naturally: overrides set on a parent element are inherited by all matching components inside it.
## Documenting CSS properties
Document public CSS custom properties with `@cssprop` JSDoc on the component class:
```js
/**
* The description of the component goes here.
*
* @cssprop [--elena-button-text] - Overrides the default text color.
* @cssprop [--elena-button-bg] - Overrides the default background color.
* @cssprop [--elena-button-font] - Overrides the default font-family.
*/
export default class Button extends Elena(HTMLElement) { /*...*/ }
```
> \[!TIP]
> **`@elenajs/bundler`** transforms these annotations into the Custom Elements Manifest, which tools and documentation generators can use to surface the component’s public CSS API.
## Shadow DOM
When `@scope` with a reset isn’t enough, Elena supports an opt-in Shadow DOM mode for components. Set `static shadow` to `"open"` or `"closed"` on the class, and pass your styles via `static styles`:
```js
import styles from "./button.css" with { type: "css" };
export default class Button extends Elena(HTMLElement) {
static tagName = "elena-button";
static shadow = "open";
static styles = styles;
}
Button.define();
```
Elena attaches the shadow root on first connect and adopts the stylesheets automatically. Rendering happens inside the shadow root, so external styles cannot reach in.
> \[!WARNING]
> Enabling Shadow DOM means that your component now relies entirely on client side JavaScript for rendering. Nothing will be visible to the user before the component has been fully initialized. See [Declarative Shadow DOM](/components/templates.md#declarative-shadow-dom) for a standards-based approach that can mitigate this.
### CSS in shadow mode
Since external stylesheets cannot reach inside the shadow root, you no longer need `@scope`:
```css
/* No @scope needed */
.my-button {
font-family: sans-serif;
color: white;
background: blue;
}
```
CSS custom properties still pierce shadow boundaries, so your theming API continues working the same way:
```css
/* Still works: */
elena-button {
--elena-button-bg: green;
}
```
## Styles without `@scope`
For older browsers that don’t support `@scope`, use namespaced selectors and the `:is()` pattern instead:
```css
/* Reset makes sure styles don’t leak in */
elena-button,
elena-button *:where(:not(img, svg):not(svg *)),
elena-button *::before,
elena-button *::after {
all: unset;
display: revert;
}
elena-button {
display: inline-block;
}
:is(elena-button:not([hydrated]), .elena-button) {
/* shared styles */
}
elena-button[variant="primary"] {
/* variant */
}
```
---
---
url: /advanced/docs.md
description: >-
Learn how to document Elena’s Progressive Web Components using JSDoc
annotations to generate a Custom Elements Manifest and TypeScript
declarations.
---
# Creating documentation
Elena components are documented using [JSDoc](https://jsdoc.app) annotations directly in the source code. When you run `elena build`, Elena reads these annotations and generates a Custom Elements Manifest, per-component TypeScript declarations, and JSX integration types.
## Class-level annotations
Place these on the JSDoc comment directly above the class declaration. These are picked up by `@elenajs/bundler` and included in the Custom Elements Manifest.
### `@displayName`
Sets the human-readable display name for the component in documentation tools.
```js
/**
* @displayName Button
*/
export default class Button extends Elena(HTMLElement) { /*...*/ }
```
### `@status`
Indicates the stability of the component. Common values are `alpha`, `beta`, and `stable`.
```js
/**
* @status alpha
*/
export default class Button extends Elena(HTMLElement) { /*...*/ }
```
### `@slot`
Documents the default slot of a Composite Component or the slots that a web component with Shadow DOM accepts.
```js
/**
* @slot - The stacked content
*/
export default class Stack extends Elena(HTMLElement) { /*...*/ }
```
### `@event`
Documents the events a component fires.
```js
/**
* @event click - Programmatically fire click on the component.
* @event focus - Programmatically move focus to the component.
* @event blur - Programmatically remove focus from the component.
*/
export default class Button extends Elena(HTMLElement) { /*...*/ }
```
### `@cssprop`
Documents public CSS custom properties exposed for theming.
```js
/**
* @cssprop [--elena-button-text] - Overrides the default text color.
* @cssprop [--elena-button-bg] - Overrides the default background color.
* @cssprop [--elena-button-font] - Overrides the default font family.
*/
export default class Button extends Elena(HTMLElement) { /*...*/ }
```
## Prop annotations
Document each prop with JSDoc directly above its instance field declaration. `@property` marks the field as a component prop. `@type` describes the expected value.
::: code-group
```js [JavaScript]
/**
* The style variant of the button.
* @property
* @type {"default" | "primary" | "danger"}
*/
variant = "default";
/**
* Makes the component disabled.
* @property
* @type {Boolean}
*/
disabled = false;
/**
* The value used to identify the button in forms.
* @property
* @type {string}
*/
value = "";
```
```ts [TypeScript]
/**
* The style variant of the button.
* @property
*/
variant: "default" | "primary" | "danger" = "default";
/**
* Makes the component disabled.
* @property
*/
disabled: boolean = false;
/**
* The value used to identify the button in forms.
* @property
*/
value: string = "";
```
:::
The supported `@type` values in JavaScript are:
```js
/** @type {string} */
/** @type {Number} */
/** @type {Array} */
/** @type {Boolean} */
/** @type {Object} */
/** @type {"default" | "primary" | "danger"} */
```
In TypeScript, write inline type annotations instead of `@type` and the bundler will derive the types directly from the declarations.
## Method annotations
Use `@internal` to mark methods that are implementation details and should not appear in the public API documentation.
```js
/**
* Renders a link: .
* @internal
*/
renderLink(template) {
return html` ${template} `;
}
```
## Full example
Here is a complete component with all JSDoc annotations in place.
::: code-group
```js [JavaScript]
import { Elena, html } from "@elenajs/core";
/**
* Button component is used for interface actions.
*
* @displayName Button
* @status alpha
*
* @event click - Programmatically fire click on the component.
* @event focus - Programmatically move focus to the component.
* @event blur - Programmatically remove focus from the component.
*
* @cssprop [--elena-button-text] - Overrides the default text color.
* @cssprop [--elena-button-bg] - Overrides the default background color.
* @cssprop [--elena-button-font] - Overrides the default font family.
* @cssprop [--elena-button-radius] - Overrides the default border radius.
*/
export default class Button extends Elena(HTMLElement) {
static tagName = "elena-button";
static events = ["click", "focus", "blur"];
static props = [
"variant",
"disabled",
];
/**
* The style variant of the button.
* @property
* @type {"default" | "primary" | "danger" | "outline"}
*/
variant = "default";
/**
* Makes the component disabled.
* @property
* @type {boolean}
*/
disabled = false;
/**
* Renders the template.
* @internal
*/
render() {
return html`
${this.text}
`;
}
}
Button.define();
```
```ts [TypeScript]
import { Elena, html } from "@elenajs/core";
/**
* Button component is used for interface actions.
*
* @displayName Button
* @status alpha
*
* @event click - Programmatically fire click on the component.
* @event focus - Programmatically move focus to the component.
* @event blur - Programmatically remove focus from the component.
*
* @cssprop [--elena-button-text] - Overrides the default text color.
* @cssprop [--elena-button-bg] - Overrides the default background color.
* @cssprop [--elena-button-font] - Overrides the default font family.
* @cssprop [--elena-button-radius] - Overrides the default border radius.
*/
export default class Button extends Elena(HTMLElement) {
static tagName = "elena-button";
static events = ["click", "focus", "blur"];
static props = [
"variant",
"disabled",
];
/**
* The style variant of the button.
* @property
*/
variant: "default" | "primary" | "danger" | "outline" = "default";
/**
* Makes the component disabled.
* @property
*/
disabled: boolean = false;
/**
* Renders the template.
* @internal
*/
render() {
return html`
${this.text}
`;
}
}
Button.define();
```
:::
## How it works
`@elenajs/bundler` runs `elena build` in two stages:
1. **Rollup build:** compiles JavaScript and TypeScript source files, minifies CSS, and writes output to `dist/`.
2. **CEM analysis:** reads your source files and JSDoc annotations, then produces:
* **`custom-elements.json`:** the [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/) describing the component’s tag name, props, events, and CSS custom properties in a machine-readable format.
* **Per-component `.d.ts` files:** a TypeScript declaration file for each component (e.g. `button.d.ts`) with typed props and event handlers derived from your JSDoc. This lets TypeScript resolve sub-path imports like `@my-lib/components/dist/button.js`.
* **`custom-elements.d.ts`:** JSX integration types that map your custom element tag names to their prop types, enabling autocomplete and type checking for ` ` in JSX/TSX files.
## Generated output
### `dist/custom-elements.json`
The [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/) describing the component’s tag name, props, events, and CSS custom properties in a machine-readable format.
```json
{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/index.js",
"declarations": [],
"exports": [
{
"kind": "js",
"name": "Button",
"declaration": {
"name": "default",
"module": "./button/button.js"
}
}
]
},
{
"kind": "javascript-module",
"path": "src/button/button.js",
"declarations": [
{
"name": "Button",
"displayName": "Button",
"description": "Button component is used for interface actions.",
"status": "alpha",
"tagName": "elena-button",
"customElement": true,
"kind": "class",
"cssProperties": [
{
"description": "Overrides the default text color.",
"name": "--elena-button-text"
},
{
"description": "Overrides the default background color.",
"name": "--elena-button-bg"
},
{
"description": "Overrides the default font family.",
"name": "--elena-button-font"
},
{
"description": "Overrides the default border radius.",
"name": "--elena-button-radius"
}
],
"members": [
{
"kind": "field",
"name": "tagName",
"type": {
"text": "string"
},
"static": true,
"default": "\"elena-button\""
},
{
"kind": "field",
"name": "events",
"type": {
"text": "array"
},
"static": true,
"default": "[\"click\", \"focus\", \"blur\"]"
},
{
"kind": "field",
"name": "props",
"type": {
"text": "array"
},
"static": true,
"default": "[\"variant\", \"disabled\"]"
}
],
"events": [
{
"description": "Programmatically fire click on the component.",
"name": "click"
},
{
"description": "Programmatically move focus to the component.",
"name": "focus"
},
{
"description": "Programmatically remove focus from the component.",
"name": "blur"
}
],
"attributes": [
{
"name": "variant",
"type": {
"text": "\"default\" | \"primary\" | \"danger\" | \"outline\""
},
"default": "\"default\"",
"description": "The style variant of the button.",
"fieldName": "variant"
},
{
"name": "disabled",
"type": {
"text": "boolean"
},
"default": "false",
"description": "Makes the component disabled.",
"fieldName": "disabled"
},
{
"name": "text",
"fieldName": "text",
"type": {
"text": "string"
},
"description": "The text content of the element, captured from light DOM before the first render."
}
],
"mixins": [
{
"name": "Elena",
"package": "@elenajs/core"
}
],
"superclass": {
"name": "HTMLElement"
}
}
],
"exports": [
{
"kind": "js",
"name": "default",
"declaration": {
"name": "Button",
"module": "src/button/button.js"
}
},
{
"kind": "custom-element-definition",
"name": "elena-button",
"declaration": {
"name": "Button",
"module": "src/button/button.js"
}
}
]
}
]
}
```
### `dist/button.d.ts`
A TypeScript declaration file for the component with typed props and event handlers derived from your JSDoc. This lets TypeScript resolve sub-path imports like `@my-lib/components/dist/button.js`.
```ts
export type { ButtonProps } from './custom-elements.js';
declare class Button extends HTMLElement {
/** The style variant of the button. */
variant?: "default" | "primary" | "danger" | "outline";
/** Makes the component disabled. */
disabled?: boolean;
/** The text content of the element, captured from light DOM before the first render. */
text?: string;
/** Programmatically fire click on the component. */
onclick?: (e: CustomEvent) => void;
/** Programmatically move focus to the component. */
onfocus?: (e: CustomEvent) => void;
/** Programmatically remove focus from the component. */
onblur?: (e: CustomEvent) => void;
}
export default Button;
```
### `dist/custom-elements.d.ts`
JSX integration types that map your custom element tag names to their prop types, enabling autocomplete and type checking for ` ` in JSX/TSX files.
````ts
/**
* This type can be used to create scoped tags for your components.
*
* Usage:
*
* ```ts
* import type { ScopedElements } from "path/to/library/jsx-integration";
*
* declare module "my-library" {
* namespace JSX {
* interface IntrinsicElements
* extends ScopedElements<'test-', ''> {}
* }
* }
* ```
*
*/
export type ScopedElements = {
[Key in keyof CustomElements as `${Prefix}${Key}${Suffix}`]: CustomElements[Key];
};
type BaseProps = {
/** Content added between the opening and closing tags of the element */
children?: any;
/** Used for declaratively styling one or more elements using CSS (Cascading Stylesheets) */
class?: string;
/** Used for declaratively styling one or more elements using CSS (Cascading Stylesheets) */
className?: string;
/** Takes an object where the key is the class name(s) and the value is a boolean expression. When true, the class is applied, and when false, it is removed. */
classList?: Record;
/** Specifies the text direction of the element. */
dir?: "ltr" | "rtl";
/** Contains a space-separated list of the part names of the element that should be exposed on the host element. */
exportparts?: string;
/** For and , lets you associate the label with some control. */
htmlFor?: string;
/** Specifies whether the element should be hidden. */
hidden?: boolean | string;
/** A unique identifier for the element. */
id?: string;
/** Keys tell React which array item each component corresponds to */
key?: string | number;
/** Specifies the language of the element. */
lang?: string;
/** Contains a space-separated list of the part names of the element. Part names allows CSS to select and style specific elements in a shadow tree via the ::part pseudo-element. */
part?: string;
/** Use the ref attribute with a variable to assign a DOM element to the variable once the element is rendered. */
ref?: unknown | ((e: unknown) => void);
/** Adds a reference for a custom element slot */
slot?: string;
/** Prop for setting inline styles */
style?: Record;
/** Overrides the default Tab button behavior. Avoid using values other than -1 and 0. */
tabIndex?: number;
/** Specifies the tooltip text for the element. */
title?: string;
/** Passing 'no' excludes the element content from being translated. */
translate?: "yes" | "no";
};
type BaseEvents = {};
export type ButtonProps = {
/** The style variant of the button. */
variant?: "default" | "primary" | "danger" | "outline";
/** Makes the component disabled. */
disabled?: boolean;
/** The text content of the element, captured from light DOM before the first render. */
text?: string;
/** Programmatically fire click on the component. */
onclick?: (e: CustomEvent) => void;
/** Programmatically move focus to the component. */
onfocus?: (e: CustomEvent) => void;
/** Programmatically remove focus from the component. */
onblur?: (e: CustomEvent) => void;
};
export type CustomElements = {
/**
* Button component is used for interface actions.
* ---
*
*
* ### **Events:**
* - **click** - Programmatically fire click on the component.
* - **focus** - Programmatically move focus to the component.
* - **blur** - Programmatically remove focus from the component.
*
* ### **CSS Properties:**
* - **--elena-button-text** - Overrides the default text color. _(default: undefined)_
* - **--elena-button-bg** - Overrides the default background color. _(default: undefined)_
* - **--elena-button-font** - Overrides the default font family. _(default: undefined)_
* - **--elena-button-radius** - Overrides the default border radius. _(default: undefined)_
*/
"elena-button": Partial;
};
````
## Using the generated types
Import `CustomElements` in your consuming project to enable autocomplete in JSX.
```ts
// types.d.ts (in your consuming project)
import type { CustomElements } from "@my-lib/components";
type ElenaIntrinsicElements = {
[K in keyof CustomElements]: CustomElements[K] & {
onClick?: (e: MouseEvent) => void;
onFocus?: (e: FocusEvent) => void;
onBlur?: (e: FocusEvent) => void;
children?: React.ReactNode;
};
};
declare module "react" {
namespace JSX {
interface IntrinsicElements extends ElenaIntrinsicElements {}
}
}
```
For framework-specific TypeScript examples, see [Using TypeScript](/advanced/typescript).
---
---
url: /advanced/loading.md
description: >-
Learn how to handle async Elena component loading, detect when a component is
ready, and safely call methods after hydration.
---
# Loading components
## Load event
Elena components can be loaded and defined asynchronously, so an element may not be interactive immediately. This means that you can set a property before the component has initialized and it will be applied correctly once hydration completes. However, you cannot call a method before the JavaScript has loaded.
Most of the time this is not an issue, as you will be calling methods through event handlers. In cases where you want to call a method as soon as possible, for example during a page load, you need to wait for the Elena web component to be defined, using `customElements.whenDefined`:
```html
```
## Hide until loaded
Elena adds a `hydrated` attribute to the host element after its first connect. You can use this in CSS to hide or style a component before it becomes interactive:
```css
elena-button:not([hydrated]) {
visibility: hidden;
}
```
This is the recommended approach when you want precise control over a specific component.
### Hide all undefined elements
For a broader solution that covers all undefined elements, you can use this CSS snippet from [Scott Jehl](https://scottjehl.com/posts/web-component-self-destruct-css/):
```css
@keyframes hideElena {
0%,
100% {
visibility: hidden;
}
}
:not(:defined) {
animation: hideElena 2s;
}
```
> \[!TIP]
> This CSS snippet will take care that as soon as your elements get defined, the hiding will instantly and automatically unapply. But it will also unapply itself after two seconds no matter what, should the JavaScript take that long to do its thing, or fail to run at all.
---
---
url: /advanced/libraries.md
description: >-
Learn how to build and publish an Elena Progressive Web Component library
using @elenajs/bundler, including configuration, bundling, and TypeScript
support.
---
# Component libraries
**`@elenajs/bundler`** is the build tool for publishing Elena component libraries. It bundles JavaScript and TypeScript source files, minifies CSS, generates a [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/), and produces TypeScript declarations for each component.
## Install
::: code-group
```sh [npm]
npm install --save-dev @elenajs/bundler
```
```sh [yarn]
yarn add --dev @elenajs/bundler
```
```sh [pnpm]
pnpm add --save-dev @elenajs/bundler
```
```sh [bun]
bun add --dev @elenajs/bundler
```
:::
## CLI usage
The bundler provides an `elena` binary with `build` and `watch` commands:
```bash
npx elena build
```
To start a watch session that rebuilds on file changes:
```bash
npx elena watch
```
### Flags
| Flag | Description |
| ------------------ | ------------------------------------------------------------------------------------------- |
| `--config ` | Path to a config file. Defaults to `elena.config.mjs` or `elena.config.js` in the project root. |
## Configuration
Create an `elena.config.mjs` (or `elena.config.js`) at the root of your package:
```js
/**
* ░█ [ELENA]: Bundler configuration
*
* @type {import("@elenajs/bundler").ElenaConfig}
*/
export default {
// Source directory scanned for .js/.ts entry files and .css files.
input: "src",
// Rollup output options.
output: {
dir: "dist",
format: "esm",
sourcemap: true,
filename: "bundle.js",
},
// Entry for the single-file bundle. Set to false to disable.
bundle: "src/index.js",
// Additional Rollup plugins appended after Elena’s built-in set.
plugins: [],
// Custom Elements Manifest options. Set to false to skip entirely.
analyze: {
plugins: [],
},
// Browserslist targets for transpilation. Enables syntax transforms
// (e.g. class fields, optional chaining) to widen browser support.
target: ["chrome 71", "firefox 69", "safari 12.1"],
// Custom Terser minifier options, merged with the defaults.
terser: { ecma: 2020, module: true },
// Banner comment prepended to bundle output files.
// Use a @license tag so minifiers preserve it.
banner: `/** @license MIT */`,
// Controls how components are registered.
// "auto" (default) preserves .define() calls in the output.
// "scoped" strips .define() calls and generates a register.js helper.
registration: "auto",
};
```
### Options
| Option | Type | Default | Description |
| ------------------ | ----------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input` | `string` | `"src"` | Source directory to scan for `.js`, `.ts`, and `.css` files. |
| `output.dir` | `string` | `"dist"` | Output directory for compiled files. |
| `output.format` | `string` | `"esm"` | Rollup output format. |
| `output.sourcemap` | `boolean` | `true` | Whether to emit sourcemaps. |
| `output.filename` | `string` | `"bundle.js"` | Output filename for the single-file bundle. The CSS bundle filename is derived by replacing the `.js` extension with `.css`. |
| `bundle` | `string \| false` | `"src/index.js"` | Entry point for the single-file bundle. Auto-detects `src/index.ts` if no `.js` entry exists. Set to `false` to disable. |
| `plugins` | `Plugin[]` | `[]` | Additional Rollup plugins appended after the built-in set. |
| `analyze` | `object \| false` | `{ plugins: [] }` | CEM analysis options. Set to `false` to skip Custom Elements Manifest generation, TypeScript declarations, and JSX types entirely. |
| `analyze.plugins` | `Plugin[]` | `[]` | Additional CEM analyzer plugins. |
| `target` | `string \| string[] \| false` | `false` | Browserslist target(s) for transpilation. When set, enables syntax transforms (e.g. class fields, optional chaining) via `@babel/preset-env`. Example: `["chrome 71", "safari 12.1"]`. |
| `terser` | `object` | `{ ecma: 2020, module: true }` | Custom Terser minifier options, merged with the defaults. See the [Terser API docs](https://terser.org/docs/api-reference/) for available options. |
| `banner` | `string \| false` | `false` | Banner comment prepended to `index.js` and `bundle.js` output files. Use a `@license` JSDoc tag so minifiers preserve it. |
| `registration` | `"auto" \| "scoped"` | `"auto"` | Controls how components are registered. `"auto"` preserves `.define()` calls in the output. `"scoped"` strips `.define()` calls and generates a `register.js` with a `defineAll(registry?)` helper for [scoped registry](/components/options#scoped-registration) usage. |
## Build output
Running `elena build` produces:
| File | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
| `dist/*.js` | Individual ES modules for each source file. |
| `dist/*.css` | Minified individual CSS files. |
| `dist/bundle.js` | Single-file JavaScript bundle *(optional)*. |
| `dist/bundle.css` | Concatenated and minified CSS bundle. CSS files imported as CSS Module Scripts (`with { type: "css" }`) for Shadow DOM are excluded. |
| `dist/custom-elements.json` | [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/) describing all components. |
| `dist/custom-elements.d.ts` | JSX integration types mapping tag names to prop types. |
| `dist/*.d.ts` | Per-component TypeScript declarations with typed props and events. |
| `dist/register.js` | Registration helper with `defineAll(registry?)` and re-exported component classes. Only generated when using `registration: "scoped"`. |
> \[!TIP]
> CSS files that are imported as CSS Module Scripts (`import styles from "./button.css" with { type: "css" }`) for Shadow DOM use are automatically excluded from `bundle.css`. These files are instead inlined as `CSSStyleSheet` objects in the JavaScript output. Individual `.css` files are still emitted.
## Scoped registry builds
When `registration: "scoped"` is set, the bundler strips `.define()` calls and side-effect component imports from the output. Components are exported as bare classes, and a `register.js` helper is generated:
```js
export default {
registration: "scoped",
};
```
The generated `register.js` exports a `defineAll()` function and re-exports all component classes:
```js
import { defineAll } from "@elenajs/components/register";
const registry = new CustomElementRegistry();
defineAll(registry);
```
You can also import individual classes for selective registration:
```js
import { Button, Spinner } from "@elenajs/components/register";
const registry = new CustomElementRegistry();
Button.define(registry);
Spinner.define(registry);
```
This can be useful when multiple versions of the same library need to coexist on a single application. See [Scoped registration](/components/options#scoped-registration) for more details.
## TypeScript support
The bundler supports both JavaScript and TypeScript source files. When `.ts` files are detected in the source directory, the bundler automatically transpiles them via `@rollup/plugin-typescript`. The output is identical to what you get from JavaScript sources.
To use TypeScript, write your components with inline type annotations instead of JSDoc:
```ts
import { Elena, html } from "@elenajs/core";
export default class Button extends Elena(HTMLElement) {
static tagName = "elena-button";
static props = ["variant"];
/**
* The style variant of the component.
* @property
*/
variant: "default" | "primary" | "danger" = "default";
render() {
return html`${this.text} `;
}
}
Button.define();
```
A `tsconfig.json` is required in the project root. A minimal configuration:
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"skipLibCheck": true
},
"include": ["src"]
}
```
> \[!TIP]
> The bundler handles TypeScript declarations separately via the CEM analyzer. You don’t need `declaration: true` in your `tsconfig.json`.
See the [TypeScript](/advanced/typescript) page for more on using and consuming the generated types.
## Programmatic API
The bundler exports its internals so you can integrate it into your own build scripts:
```js
import {
createRollupConfig,
runRollupBuild,
watchRollupBuild,
createCemConfig,
runCemAnalyze,
} from "@elenajs/bundler";
```
Sub-path imports are also available:
```js
import { createRollupConfig, runRollupBuild, watchRollupBuild } from "@elenajs/bundler/rollup";
import { createCemConfig, runCemAnalyze } from "@elenajs/bundler/cem";
```
### `createRollupConfig(options?)`
Returns a Rollup configuration array. Useful if you want to wrap or extend the config in a custom `rollup.config.js`.
### `runRollupBuild(config)`
Runs both build phases (individual modules + optional single-file bundle) programmatically.
### `watchRollupBuild(config, opts?)`
Starts a Rollup watch session that rebuilds on file changes. Returns the Rollup watcher instance. Pass `opts.onRebuild` as an async callback to run after each successful rebuild (e.g. to re-run CEM analysis).
### `createCemConfig(options?)`
Returns the Custom Elements Manifest analyzer configuration object.
### `runCemAnalyze(config, cwd?)`
Runs the CEM analysis and writes `custom-elements.json`, `custom-elements.d.ts`, and per-component `.d.ts` files.
## Example library
**`@elenajs/components`** is a reference component library built with Elena. It demonstrates real-world component patterns and is available as a starting point for your own library.
::: code-group
```sh [npm]
npm install @elenajs/components
```
```sh [yarn]
yarn add @elenajs/components
```
```sh [pnpm]
pnpm add @elenajs/components
```
```sh [bun]
bun add @elenajs/components
```
:::
It includes the following components:
| Component | Tag | Description |
| ---------------------- | ------------------------ | --------------------------------------------------------------- |
| Button | `` | Renders a `` or `` with variants, states, and icons. |
| Spinner | `` | Animated loading indicator that inherits the current color. |
| Stack | `` | Flexbox layout wrapper with configurable direction and gap. |
| Visually Hidden | ``| Hides content visually while keeping it accessible. |
### Usage
Import the full bundle to register all components:
```js
import "@elenajs/components";
```
Or import individual components for better tree-shaking:
```js
import "@elenajs/components/dist/button.js";
import "@elenajs/components/dist/stack.js";
```
Each component has a matching CSS file:
```css
@import "@elenajs/components/dist/button.css";
@import "@elenajs/components/dist/stack.css";
```
Or import the full CSS bundle:
```css
@import "@elenajs/components/dist/bundle.css";
```
When a library is built with `registration: "scoped"`, import from `register.js` to control where components are registered:
```js
import { defineAll } from "@elenajs/components/register";
const registry = new CustomElementRegistry();
defineAll(registry);
```
The source code is in `packages/components/` of the [Elena monorepo](https://github.com/arielsalminen/elena) and serves as an example of how to structure, build, and publish a library with `@elenajs/bundler`.
## Next steps
* Explore [Framework Integration](/advanced/frameworks) examples for React, Vue, Angular, Next.js and more.
---
---
url: /advanced/ssr.md
description: >-
Learn how Elena supports server-side rendering (SSR), and how to use
@elenajs/ssr for Primitive Components with render().
---
# Server-side rendering
Elena’s approach to server-side rendering is simple and straightforward. Since [Progressive Web Components](/components/overview) are primarily HTML and CSS, you don’t need any special logic on the server to render them.
Components without a `render()` method are fully SSR-compatible by default, while components with `render()` provide partial support and complete hydration on the client side.
The “partial support” bit for the latter means that you can render the initial state without JavaScript, but JS is needed for the interactivity *(unless you also use the provided [@elenajs/ssr](#rendering-to-html-strings) tool).*
Elena also supports [Declarative Shadow DOM](#declarative-shadow-dom) for cases where you may need stronger isolation, but still want the component to render server-side.
## Avoiding layout shifts
For components with `render()` specifically, our recommendation is to ship them with CSS styles that visually match the loading and `hydrated` states. This can be achieved utilizing the provided `hydrated` attribute in your web component’s styles:
```css
/* Elena CSS pre-hydration styles */
:scope:not([hydrated]),
.element { ... }
```
Since both selectors now share the same baseline styles, there are no visible layout shifts, FOUC, or FOIC (Flash Of Unstyled Content, Flash Of Invisible Content). For more details, see the [CSS pre-hydration styles](/components/styles#css-pre-hydration-styles) section.
## Rendering to HTML strings
When you don’t want to handle the pre-hydration state with CSS, you can expand component templates inline using [@elenajs/ssr](https://github.com/arielsalminen/elena/tree/main/packages/ssr). Please note that this is an experimental package and we do not recommend it for production just yet.
> \[!WARNING]
> `@elenajs/ssr` is an experimental package and not yet ready for production use. APIs may change without notice.
### Install
::: code-group
```sh [npm]
npm install @elenajs/ssr
```
```sh [yarn]
yarn add @elenajs/ssr
```
```sh [pnpm]
pnpm add @elenajs/ssr
```
```sh [bun]
bun add @elenajs/ssr
```
:::
### Basic usage
Register your components once, then pass any HTML string through `ssr()`:
```js
import { ssr, register } from "@elenajs/ssr";
const { Button } = await import("@elenajs/components");
register(Button);
const html = ssr(`Save `);
// Outputs: 'Save '
```
### With nesting
Nested Elena components are expanded automatically:
```js
import { ssr, register } from "@elenajs/ssr";
const { Button, Stack, Input } = await import("@elenajs/components");
register(Button, Stack, Input);
const html = ssr(`
Send
`);
```
Output:
```html
Email
Send
```
## `@elenajs/ssr` API
### `register(...components)`
Register Elena component classes for SSR expansion. Each class must have a `tagName` defined. Call this once before using `ssr()`.
```js
import { register } from "@elenajs/ssr";
const { Button, Stack } = await import("@elenajs/components");
register(Button, Stack);
```
Throws an error if a component does not have a `tagName`.
### `ssr(html)`
Parse an HTML string, expand registered components with `render()`, and return the rendered HTML. Full HTML documents are supported: ``, ``, ``, and `` tags are preserved as-is alongside Elena component expansion.
| Parameter | Type | Description |
| --------- | -------- | ---------------------------------------- |
| `html` | `string` | HTML string containing Elena components. |
**Returns:** `string`, the rendered HTML with components expanded.
### `unregister(...components)`
Remove previously registered component classes from the SSR registry.
```js
import { register, unregister } from "@elenajs/ssr";
const { Button } = await import("@elenajs/components");
register(Button);
// ... later
unregister(Button);
```
### `clear()`
Remove all registered component classes from the SSR registry at once.
```js
import { clear } from "@elenajs/ssr";
clear();
```
### How it works
1. **Parse** the input HTML string into a tree.
2. **Walk** the tree and look up each custom element tag in the registry.
3. **Expand** matching custom elements by calling their `render()`.
4. **Recurse** into composite component children and non-component tags.
5. **Serialize** the tree back to an HTML string.
The rendered output matches what Elena produces on the client, using the same `html` tagged template escaping and whitespace normalization.
> \[!TIP]
> If a component’s `render()` throws an error, the SSR renderer logs a warning and falls back to passing the component through without expansion, preserving its original children. This prevents a single broken component from affecting the rest of the page.
### Client-side hydration
The HTML produced by `ssr()` is designed for progressive enhancement. When the component JavaScript loads on the client:
1. Elena’s `connectedCallback` fires on the pre-rendered element.
2. `render()` runs and hydrates the component with interactivity.
3. Event listeners are attached and methods become available.
## Pre-rendering with Eleventy
Use `@elenajs/ssr` with [Eleventy](https://www.11ty.dev/) as either a transform or a shortcode.
#### As a transform
A transform processes every rendered page automatically, expanding any registered components with `render()` found in the output HTML. No shortcodes or special syntax needed: just write Elena components directly in your templates:
```js
// eleventy.config.js
import { ssr, register } from "@elenajs/ssr";
const { Button, Stack, Input } = await import("@elenajs/components");
register(Button, Stack, Input);
export default function (eleventyConfig) {
eleventyConfig.addTransform("elena-ssr", (content, outputPath) => {
if (outputPath?.endsWith(".html")) {
return ssr(content);
} else {
return content;
}
});
}
```
> \[!TIP]
> Use `await import()` for component modules rather than a static `import` statement. Elena components extend `HTMLElement`, which requires a Node.js shim that `@elenajs/ssr` installs when it loads. Dynamic imports guarantee the shim is in place first, regardless of how an import sorter may reorder your static imports.
Then use Elena components directly in any Nunjucks, Liquid, or Markdown template:
```html
Subscribe
```
#### As a shortcode
If you prefer more control over which parts of a page are processed, use a shortcode instead:
```js
// eleventy.config.js
import { ssr, register } from "@elenajs/ssr";
const { Button } = await import("@elenajs/components");
register(Button);
export default function (eleventyConfig) {
eleventyConfig.addShortcode("render", (html) => ssr(html));
}
```
Then in a template:
```html
{% render 'Save ' %}
```
## Pre-rendering without a framework
If you’re working with plain HTML files and no framework or static site generator, you can use `@elenajs/ssr` directly in a Node.js build script. Place your HTML files in a `src/` directory and the script will expand all registered Elena components into `dist/`:
```js
// build.mjs
import { readFileSync, writeFileSync, readdirSync, mkdirSync } from "node:fs";
import { ssr, register } from "@elenajs/ssr";
const { Button, Stack } = await import("@elenajs/components");
register(Button, Stack);
mkdirSync("dist", { recursive: true });
for (const file of readdirSync("src").filter(f => f.endsWith(".html"))) {
const html = readFileSync(`src/${file}`, "utf-8");
writeFileSync(`dist/${file}`, ssr(html));
}
```
Given a source file `src/index.html`:
```html
Save
Cancel
```
The script produces `dist/index.html`:
```html
Save
Cancel
```
You can add this as an npm script in your `package.json` for convenience:
```json
{
"scripts": {
"build": "node build.mjs"
}
}
```
Running `pnpm build` will generate the pre-rendered output.
> \[!TIP]
> Use `await import()` for component modules rather than a static `import` statement. Elena components extend `HTMLElement`, which requires a Node.js shim that `@elenajs/ssr` installs when it loads. Dynamic imports guarantee the shim is in place first, regardless of how an import sorter may reorder your static imports.
## Declarative Shadow DOM
Declarative Shadow DOM lets you define a shadow root directly in HTML using a `` element. The browser attaches the shadow root during parsing, so the shadow content is visible before JavaScript loads.
When a component with `static shadow` connects and finds a shadow root already attached, Elena skips `attachShadow()` and works with the existing one instead. Content stays in the light DOM and is projected into the shadow root via ``:
::: code-group
```html [HTML]
Click me
```
```js [JavaScript]
import { Elena } from "@elenajs/core";
export default class Button extends Elena(HTMLElement) {
static tagName = "elena-button";
static shadow = "open";
}
Button.define();
```
:::
In practice, you have to write the `` block by hand every time you use the component, which gets repetitive quickly unless you abstract this duplication away in your own application. `@elenajs/ssr` may later get Declarative Shadow DOM support which would eliminate that entirely, but this isn’t currently on our roadmap.
For now, Declarative Shadow DOM is mainly useful when you need Shadow DOM style isolation and want the component to be visible before JavaScript loads.
## Framework examples
Elena currently provides SSR examples for the following frameworks:
* **[Eleventy](https://github.com/arielsalminen/eleventy-example-project)**
* **[Plain HTML](https://github.com/arielsalminen/html-example-project)**
* **[Next.js](https://github.com/arielsalminen/next-example-project):** Elena can be used inside [React Server Components](https://github.com/arielsalminen/next-example-project/blob/main/src/app/page.tsx)
---
---
url: /advanced/frameworks.md
description: >-
Elena works with any JavaScript framework. It uses standard web platform APIs,
so framework compatibility comes for free without any special adapters or
wrappers.
---
# Framework integrations
Elena works with any JavaScript framework. It uses standard web platform APIs, so framework compatibility comes for free without any special adapters or wrappers.
## Plain HTML
Since Elena ships as ES modules, you can load your components directly on a webpage. Here’s an example that loads the `@elenajs/components` from [unpkg](https://unpkg.com):
```html
```
This registers all Elena components globally as standard HTML tags.
**[View plain HTML example project →](https://github.com/arielsalminen/html-example-project)**
## Eleventy
Install your own Elena component library as a dependency, then copy the bundle to the output directory via `addPassthroughCopy` in `eleventy.config.js`. This example uses the existing `@elenajs/components` for demo purposes:
```js
eleventyConfig.addPassthroughCopy({
"node_modules/@elenajs/components/dist/bundle.js": "assets/bundle.js",
"node_modules/@elenajs/components/dist/bundle.css": "assets/bundle.css",
});
```
Now you can reference the bundle in your template:
```html
```
This registers all Elena components globally, making them available as standard HTML tags in any Nunjucks, Liquid, or Markdown template.
For server-side rendering with Eleventy, see [Pre-rendering with Eleventy](/advanced/ssr#pre-rendering-with-eleventy).
**[View Eleventy example project →](https://github.com/arielsalminen/eleventy-example-project)**
## Next.js
Because Elena components access the DOM on hydration, Next.js needs to be configured in a way that supports this. To achieve this, create a `"use client"` component with `useEffect` to defer Elena hydration to the browser:
```tsx
// src/elements-registry.tsx
"use client";
import { useEffect } from "react";
export default function ElementsRegistry() {
useEffect(() => {
import("@elenajs/components");
}, []);
return null;
}
```
Mount it once in the root layout so components are available everywhere. Elena components can then be used directly in Server Components for markup and styles, with interactivity handled by client components.
### TypeScript
Create a type declaration file to wire Elena’s types into React’s JSX namespace:
```ts
// src/elena.d.ts
import type { CustomElements } from "@elenajs/components";
type ElenaIntrinsicElements = {
[K in keyof CustomElements]: CustomElements[K] & {
onClick?: (e: MouseEvent) => void;
onFocus?: (e: FocusEvent) => void;
onBlur?: (e: FocusEvent) => void;
children?: React.ReactNode;
};
};
declare module "react" {
namespace JSX {
interface IntrinsicElements extends ElenaIntrinsicElements {}
}
}
```
**[View Next.js example project →](https://github.com/arielsalminen/next-example-project)**
## React
Import the component library and its styles, then use the components as regular JSX tags:
```tsx
import "@elenajs/components/dist/bundle.js";
import "@elenajs/components/dist/bundle.css";
function App() {
return (
console.log("clicked!")}>
Click me
);
}
```
### TypeScript
When using Vite’s `"jsx": "react-jsx"` transform, augment both the production and development JSX runtimes:
```ts
// src/elena.d.ts
import type { CustomElements } from "@elenajs/components";
type ElenaIntrinsicElements = {
[K in keyof CustomElements]: CustomElements[K] & {
onClick?: (e: MouseEvent) => void;
onFocus?: (e: FocusEvent) => void;
onBlur?: (e: FocusEvent) => void;
children?: React.ReactNode;
};
};
declare module "react/jsx-runtime" {
namespace JSX {
interface IntrinsicElements extends ElenaIntrinsicElements {}
}
}
declare module "react/jsx-dev-runtime" {
namespace JSX {
interface IntrinsicElements extends ElenaIntrinsicElements {}
}
}
```
Make sure `tsconfig.json` includes the `src` directory, then restart the TypeScript server in your editor after adding the file.
**[View React example project →](https://github.com/arielsalminen/react-example-project)**
## Svelte
Import the component library and its styles, then use the components as regular template tags:
```svelte
console.log("clicked!")}>Click me
```
### TypeScript
Extend `SvelteHTMLElements` with Elena’s `CustomElements` type map:
```ts
// src/elena.d.ts
import type { CustomElements } from "@elenajs/components";
declare module "svelte/elements" {
interface SvelteHTMLElements extends CustomElements {}
}
```
The [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) extension is required for template type checking.
**[View Svelte example project →](https://github.com/arielsalminen/svelte-example-project)**
## Vue
Import the component library and its styles, then use the components as regular template tags:
```vue
console.log('clicked!')">Click me
```
### TypeScript
Map Elena’s `CustomElements` into Vue’s `GlobalComponents` interface:
```ts
// src/elena.d.ts
import type { CustomElements } from "@elenajs/components";
import type { DefineComponent } from "vue";
type ElenaComponents = {
[K in keyof CustomElements]: DefineComponent;
};
declare module "vue" {
interface GlobalComponents extends ElenaComponents {}
}
```
The [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) extension is required for template type checking.
**[View Vue example project →](https://github.com/arielsalminen/vue-example-project)**
## Angular
Import the component library and its styles in a component, add `CUSTOM_ELEMENTS_SCHEMA` to the `schemas` array, then use the components as regular template tags:
```typescript
// app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "@elenajs/components/dist/bundle.js";
import "@elenajs/components/dist/bundle.css";
@Component({
selector: "app-root",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
templateUrl: "./app.component.html",
})
export class AppComponent {
onClick() {
console.log("clicked!");
}
}
```
```html
```
`CUSTOM_ELEMENTS_SCHEMA` tells Angular’s template compiler to accept unknown element names without errors. It also bypasses Angular’s template type-checker for these elements, so no additional type declarations are needed.
### Angular-specific syntax
| Concept | Syntax |
| ----------------- | ------------------------------- |
| Event binding | `(click)="handler()"` |
| Dynamic attribute | `[attr.variant]="value"` |
| Reactive state | `signal()` from `@angular/core` |
**[View Angular example project →](https://github.com/arielsalminen/angular-example-project)**
> \[!TIP]
> The provided examples import the existing `@elenajs/components` for demo purposes. Replace it with your own component library for production usage.
## Next steps
* For integration rules and framework-specific notes, see [Known issues](/advanced/gotchas#javascript-frameworks).
* For SSR-specific notes, see [Server-side rendering](/advanced/ssr).
---
---
url: /advanced/cli.md
description: >-
Learn how to use @elenajs/cli to interactively scaffold Elena components with
JS, TS, or single-file HTML output.
---
# Command line interface
`@elenajs/cli` is an interactive command line tool for scaffolding Elena components. It generates JavaScript, TypeScript, or single-file HTML source files and scoped CSS with all Elena [Progressive Web Component](/components/overview) patterns pre-configured.
## Install
::: code-group
```sh [npm]
npm install --save-dev @elenajs/cli
```
```sh [yarn]
yarn add --dev @elenajs/cli
```
```sh [pnpm]
pnpm add --save-dev @elenajs/cli
```
```sh [bun]
bun add --dev @elenajs/cli
```
:::
## Usage
### Interactive mode
Run without arguments to be guided through all options:
```bash
npx elena-create
```
### With a component name
Pass a kebab-case name (must contain at least one hyphen) to skip the name prompt:
```bash
npx elena-create my-button
npx elena-create my-stack
```
## Prompts
The CLI walks you through the following steps:
| Prompt | Description | Default |
| ---------------------- | ---------------------------------------------------------------------------------------------------- | ---------------- |
| **Component name** | Kebab-case name with at least one hyphen (e.g. `my-button`, `my-stack`). Skipped if passed as argument. | |
| **Component features** | Feature toggles for the generated code. See [Component features](#component-features) below. | None selected |
| **Language** | `JavaScript`, `TypeScript`, or `HTML`. | |
| **Output directory** | Where to generate the component folder. | `src/components` |
### Component features
You can toggle features to include in the generated code:
| Option | Description |
| --------------------- | ----------------------------------------------------------------------- |
| **Props** | Adds example props with `@property` / `@type` JSDoc annotations. |
| **Events** | Adds `events` option and `@event` JSDoc annotations. |
| **Methods** | Adds an example method stub. |
| **CSS Variables** | Adds `@cssprop` JSDoc annotations and CSS custom property declarations. |
| **CSS Encapsulation** | Adds the `all: unset` reset to prevent global styles from leaking in. |
| **CSS Pre-hydration** | Adds `:scope:not([hydrated])` styles for pre-hydration rendering. |
| **Code Comments** | Includes JSDoc annotations and CSS comments in the generated code. |
## Generated files
For a component named `my-button`, the CLI creates:
```
src/components/my-button/
├── my-button.js (or .ts)
└── my-button.css
```
When using the HTML language, a single file is generated instead:
```
src/components/my-button/
└── my-button.html
```
---
---
url: /advanced/typescript.md
description: >-
Learn how to use TypeScript with Elena, including authoring components in TS,
generated type declarations, and JSX type support.
---
# Using TypeScript
Elena is written in vanilla JavaScript with JSDoc annotations. The **`@elenajs/core`** library ships its own type declarations (`dist/elena.d.ts`) which are generated automatically by `tsc` from the JSDoc so that you get full IntelliSense and type checking.
```ts
import { Elena, html, unsafeHTML, nothing } from "@elenajs/core";
```
For advanced typing, you can also import the utility types directly:
```ts
import type {
ElenaPropObject,
ElenaElementConstructor,
ElenaConstructor
} from "@elenajs/core";
```
## Generating types
When you build your own Elena components, **`@elenajs/bundler`** can generate TypeScript declarations for each one. Running `elena build` produces:
* **Per-component `.d.ts` files**: A declaration file for each component (e.g. `button.d.ts`) with typed props and event handlers, derived from your JSDoc annotations. This lets TypeScript resolve sub-path imports like `@my-lib/components/dist/button.js`.
* **`custom-elements.json`**: The [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/), a machine-readable description of your components used by IDEs and documentation tools.
* **`custom-elements.d.ts`**: JSX integration types that map your custom element tag names to their prop types. This enables autocomplete and type checking for ` ` in JSX/TSX files.
## Using the generated types
The generated `custom-elements.d.ts` exports a `CustomElements` type map. To get type checking in JSX (this works with Next.js, see further down for more examples):
```ts
// types.d.ts (in your consuming project)
import type { CustomElements } from "@my-lib/components";
type ElenaIntrinsicElements = {
[K in keyof CustomElements]: CustomElements[K] & {
onClick?: (e: MouseEvent) => void;
onFocus?: (e: FocusEvent) => void;
onBlur?: (e: FocusEvent) => void;
children?: React.ReactNode;
};
};
declare module "react" {
namespace JSX {
interface IntrinsicElements extends ElenaIntrinsicElements {}
}
}
```
## TypeScript examples
Elena provides TypeScript examples for the following JavaScript frameworks:
* **[Next.js](https://github.com/arielsalminen/next-example-project)**
* **[React](https://github.com/arielsalminen/react-example-project)**
* **[Svelte](https://github.com/arielsalminen/svelte-example-project)**
* **[Vue](https://github.com/arielsalminen/vue-example-project)**
## Authoring with TypeScript
`elena build` auto-detects `.ts` files in your source directory and transpiles them via `@rollup/plugin-typescript`. No extra configuration is needed: write `.ts` files and the bundler handles the rest.
When using TypeScript to author Elena components, you can add inline type annotations directly to your instance field declarations:
```ts
/**
* The style variant of the component.
* @property
*/
variant: "default" | "primary" | "danger" = "default";
```
### Typing `static props` with `ElenaPropObject`
When using `{ name, reflect }` objects in `static props`, import `ElenaPropObject` to type the array:
```ts
import type { ElenaPropObject } from "@elenajs/core";
export default class Button extends Elena(HTMLElement) {
static props: (string | ElenaPropObject)[] = [
{ name: "icon", reflect: false },
];
}
```
---
---
url: /advanced/scoping.md
description: >-
Learn how to scope Elena’s Progressive Web Component styles using CSS @scope,
CSS encapsulation, and CSS cascade layers.
---
# Scoping styles
Elena recommends the [@scope](https://caniuse.com/css-cascade-scope) at-rule to prevent component styles from leaking out to the rest of the page:
```css
@scope (elena-button) {
/**
* Scoped styles for the elena-button. These won’t leak
* out or affect any other elements in your app.
*/
}
```
To style the host element itself, use `:scope`:
```css
@scope (elena-button) {
/* Targets the host element (elena-button) */
:scope {
all: unset;
display: inline-block;
}
}
```
## Preventing styles from leaking in
`@scope` stops your styles from leaking out, but it does not prevent global styles from leaking in. To block both directions, add a universal reset as the first rule inside `@scope`:
```css
/* Scope makes sure styles don’t leak out */
@scope (elena-button) {
/* Reset makes sure styles don’t leak in */
:scope,
*:where(:not(img, svg):not(svg *)),
*::before,
*::after {
all: unset;
display: revert;
}
/* Rest of your component styles */
}
```
## Shadow DOM
When `@scope` with a reset isn’t enough, Elena supports an opt-in Shadow DOM mode for components. Set `static shadow` to `"open"` or `"closed"` on the class, and pass your styles via `static styles`:
```js
import styles from "./button.css" with { type: "css" };
export default class Button extends Elena(HTMLElement) {
static tagName = "elena-button";
static shadow = "open";
static styles = styles;
}
Button.define();
```
Elena attaches the shadow root on first connect and adopts the stylesheets automatically. Rendering happens inside the shadow root, so external styles cannot reach in.
> \[!WARNING]
> Enabling Shadow DOM means that your component now relies entirely on client side JavaScript for rendering. Nothing will be visible to the user before the component has been fully initialized. See [Declarative Shadow DOM](/components/templates.md#declarative-shadow-dom) for a standards-based approach that can mitigate this.
### CSS in shadow mode
Since external stylesheets cannot reach inside the shadow root, you no longer need `@scope`:
```css
/* No @scope needed */
.my-button {
font-family: sans-serif;
color: white;
background: blue;
}
```
CSS custom properties still pierce shadow boundaries, so your theming API continues working the same way:
```css
/* Still works: */
elena-button {
--elena-button-bg: green;
}
```
## Styles without `@scope`
For older browsers that don’t support `@scope`, use namespaced selectors and the `:is()` pattern instead:
```css
/* Reset makes sure styles don’t leak in */
elena-button,
elena-button *:where(:not(img, svg):not(svg *)),
elena-button *::before,
elena-button *::after {
all: unset;
display: revert;
}
elena-button {
display: inline-block;
}
:is(elena-button:not([hydrated]), .elena-button) {
/* shared styles */
}
elena-button[variant="primary"] {
/* variant */
}
```
---
---
url: /advanced/gotchas.md
description: >-
Known browser compatibility issues and workarounds for Elena, including bugs
with CSS @scope in Firefox and Safari.
---
# Known issues
## Browser compatibility
As a baseline, Elena’s progressive approach supports any web browser that’s capable of rendering Custom Elements. After that, it’s up to you to determine what is appropriate for your project when authoring CSS styles and JavaScript interactivity.
Elena, the JavaScript library, supports the following browser versions: Chrome 71+, Firefox 69+, Safari 12.1+, Edge 79+, and Opera 58+.
**Firefox 148:** When using CSS `@scope` together with `attr[value]` selectors, the resulting behaviour can be buggy. This is fixed in [Firefox 149](https://www.firefox.com/en-US/firefox/149.0/releasenotes/) and newer. When necessary, you can [omit `@scope`](/components/styles#styles-without-scope) and use class based approach instead to go around this issue.
**Safari 26.3:** `@scope` rules are not applied to ` ` and `