AI Agent Skills Draft

Self-contained instruction files for AI coding agents working with @exadel/esl. Each file is independent — copy or download the ones relevant to your task and drop them into your AI config (.cursor/rules/, .ai/rules/, .github/copilot-instructions.md, AGENTS.md or any equivalent instructions file your tool supports).

ESL Core

Core consumer guide for @exadel/esl: base classes, decorators, traversing queries, event system, class helpers, and media abstractions.

Use when: You need the main mental model for writing or reviewing ESL consumer code in an idiomatic way, including custom tags, mixins, attributes, events, and responsive behavior.

Download .md
Preview skill content

Skill: ESL Core

Version target: This skill is written for ESL v6 consumer code.

When to use: You are writing or reviewing consumer code that uses @exadel/esl and need the core mental model of the library: base classes, registration, decorators, query helpers, events, media conditions, and built-in syntax sugar.

Primary goal: Generate idiomatic ESL code that follows the library's own patterns instead of falling back to raw DOM or framework-specific habits.


Public import rules

For consumer code, import from the package public entries under @exadel/esl/modules/.... Direct imports from @exadel/esl are also valid when the consumer project uses bundling/tree-shaking in a way that does not pull unnecessary code into the final bundle.

Typical imports:

import {ESLBaseElement} from '@exadel/esl/modules/esl-base-element/core';
import {ESLMixinElement} from '@exadel/esl/modules/esl-mixin-element/core';
import {ESLTraversingQuery} from '@exadel/esl/modules/esl-traversing-query/core';
import {ESLMediaQuery, ESLMediaRuleList} from '@exadel/esl/modules/esl-media-query/core';
import {attr, boolAttr, jsonAttr, prop, listen, ready} from '@exadel/esl/modules/esl-utils/decorators';

Rules:

  • Prefer public core entries.
  • Root import from @exadel/esl is acceptable in tree-shaken setups.
  • Do not import from internal implementation files or repository-only paths.
  • For most day-to-day work inside an ESL component, prefer the built-in $$* shortcuts over re-wiring low-level utilities manually.

ESL mental model

ESL has two base component types with almost the same authoring style:

Type Base class What it is Host element
Custom tag ESLBaseElement A real custom element (<my-element>) this
Custom attribute / mixin ESLMixinElement Behavior attached via attribute (<div my-mixin>) this.$host

Shared day-to-day API:

  • $$find, $$findAll
  • $$cls
  • $$attr
  • $$fire
  • $$on, $$off
  • $$error
  • @listen
  • attribute decorators like @attr, @boolAttr, @jsonAttr

The main difference is where those APIs act:

  • in ESLBaseElement they target the element itself
  • in ESLMixinElement they target the mixin host ($host)

ESLBaseElement and ESLMixinElement

ESLBaseElement

Use when you need a new HTML tag with its own DOM lifecycle.

import {ESLBaseElement} from '@exadel/esl/modules/esl-base-element/core';
import {attr, boolAttr, jsonAttr, listen} from '@exadel/esl/modules/esl-utils/decorators';

export class MyElement extends ESLBaseElement {
  static override is = 'my-element';

  @attr({defaultValue: ''}) public title: string;
  @boolAttr() public active: boolean;
  @jsonAttr({defaultValue: {}}) public config: Record<string, unknown>;

  protected override connectedCallback(): void {
    super.connectedCallback();
    // init logic
  }

  protected override disconnectedCallback(): void {
    // cleanup before super if needed
    super.disconnectedCallback();
  }

  @listen('click')
  protected _onClick(e: MouseEvent): void {
    // ...
  }
}

MyElement.register();

ESLMixinElement

Use when you need to attach behavior to an existing element via an attribute.

import {ESLMixinElement} from '@exadel/esl/modules/esl-mixin-element/core';
import {attr, boolAttr, jsonAttr, listen} from '@exadel/esl/modules/esl-utils/decorators';

export class MyMixin extends ESLMixinElement {
  static override is = 'my-mixin';
  static override observedAttributes = ['title'];

  @attr({defaultValue: ''}) public title: string;
  @boolAttr() public active: boolean;
  @jsonAttr({defaultValue: {}}) public config: Record<string, unknown>;

  protected override connectedCallback(): void {
    super.connectedCallback();
    // init logic on this.$host
  }

  protected override disconnectedCallback(): void {
    super.disconnectedCallback();
  }

  @listen('click')
  protected _onClick(e: MouseEvent): void {
    // this.$host is the real DOM element
  }
}

MyMixin.register();

Key differences

Topic ESLBaseElement ESLMixinElement
Registration customElements.define(...) via register() ESLMixinRegistry via register()
Host this this.$host
HTML form <my-element> <div my-mixin>
static is custom tag name activation attribute name
Multiple per same host no yes
Primary observation native custom element lifecycle attribute-driven attach/detach

Registration rules

  • Set static is before calling register().
  • ESLBaseElement.register() optionally accepts a tag name, but the normal consumer path is defining static is in the class.
  • Custom element tag names and mixin is attributes must contain a dash to comply with custom element naming rules.
  • Do not mutate is after registration.

Lifecycle rules

  • Always call super.connectedCallback().
  • Always call super.disconnectedCallback().
  • @ready is optional. It does not define component readiness; it defers method execution until the DOM is ready (DOMContentLoaded) and the next task, which is useful when DOM lookup must wait for the parsed tree.
  • attributeChangedCallback reacts only to observed attributes.
  • For ESLBaseElement, that means attributes listed in static observedAttributes.
  • For ESLMixinElement, that means attributes listed in static observedAttributes, plus the primary mixin is attribute, which is always observed.
  • When triggered, attributeChangedCallback may still run on every write, not only on actual value change.
  • Guard expensive reactions when needed:
class Example extends ESLBaseElement {
  protected override attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
    if (oldValue === newValue) return;
    // real change handling
  }
}

Shared component API ($$* shortcuts)

Inside both ESLBaseElement and ESLMixinElement you can use:

API Meaning
$$find(sel) ESLTraversingQuery.first(sel, host)
$$findAll(sel) ESLTraversingQuery.all(sel, host)
$$cls(cls, value?) read/toggle CSS classes on host
$$attr(name, value?) read/set/remove host attribute
$$fire(name, init?) dispatch custom event
$$on(...) subscribe with ESL event system
$$off(...) unsubscribe with ESL event system
$$error(err, key) default logger used by @safe

Use these helpers first. They already encode ESL conventions.


ESLTraversingQuery

ESLTraversingQuery extends normal CSS selection with relative traversal syntax. It powers $$find and $$findAll.

This section describes the behavior and conventions expected in ESL v6 consumer code.

Why it matters

It is used everywhere in ESL because many components need to resolve targets relative to the current host, not only by global CSS selectors.

Important syntax

  • plain CSS selector: '.item.active'
  • empty query '' — returns the current base element / host
  • ::next — next sibling
  • ::prev — previous sibling
  • ::parent — direct parent
  • ::parent(.panel) — closest parent matching selector
  • ::closest(.panel) — closest ancestor including current element
  • ::child(button) — direct child elements
  • ::find(.item) — descendants
  • ::first, ::last, ::nth(2) — result limiting
  • ::visible — visible elements only
  • ::not([hidden]) — post-filtering
  • ::filter(:first-child) — post-filtering

Examples

this.$$find('');                   // current host: this for element, this.$host for mixin
this.$$find('::parent');
this.$$find('::closest(esl-panel)');
this.$$find('::find(button, a)::not([hidden])');
this.$$findAll('::find(.row)::visible');

Difference from querySelector

  • can start from the current host without repeating selectors
  • supports traversal tokens like ::parent, ::closest, ::next
  • is designed for component-relative targeting, not just document-wide CSS lookup

Important nuance:

  • this.$$find('button') is a plain CSS query and behaves like a normal scoped/global selector lookup for the current query scope.
  • If you want an explicitly host-relative descendant search, prefer this.$$find('::find(button)') or this.$$findAll('::find(button)').

Do not treat $$find / $$findAll as a ban on native DOM APIs:

  • this.querySelector(...) / this.querySelectorAll(...) are still completely valid inside ESLBaseElement when a normal element-scoped CSS query is enough.
  • Prefer $$find / $$findAll when you need ESL traversing syntax, when the selector comes from component API, or when you want richer relative targeting such as ::parent, ::closest(...), or ::find(...).

Prefer $$find / $$findAll in ESL components instead of raw querySelector when the target is part of the component relationship model.


Attribute and property decorators

These decorators are host-aware:

  • in an element they work on this
  • in a mixin they work on this.$host

That means the same decorator patterns are reusable in both component types.

@attr

Generic property-to-attribute mapping.

Use it for:

  • strings
  • numbers
  • tri-state booleans
  • inherited values
  • custom parsing/serialization
import {attr} from '@exadel/esl/modules/esl-utils/decorators';

class Example extends ESLBaseElement {
  @attr({defaultValue: ''}) public title: string;

  @attr({
    defaultValue: true,
    parser: (v) => v !== 'false',
    serializer: (v) => v ? '' : null,
  })
  public closable: boolean;
}

Capabilities:

  • custom attribute name
  • data-* attributes
  • readonly
  • defaultValue
  • custom parser/serializer
  • inheritance from ancestors

@boolAttr

Boolean presence attribute.

import {boolAttr} from '@exadel/esl/modules/esl-utils/decorators';

class Example extends ESLBaseElement {
  @boolAttr() public disabled: boolean;
}

Semantics:

  • attribute present → true
  • attribute absent → false

Use @attr, not @boolAttr, when you need a default-enabled or tri-state boolean.

@jsonAttr

Object mapping decorator.

import {jsonAttr} from '@exadel/esl/modules/esl-utils/decorators';

class Example extends ESLBaseElement {
  @jsonAttr({defaultValue: {theme: 'light'}})
  public config: {theme: string};
}

Important: in current ESL it supports not only strict JSON but a relaxed object syntax suitable for HTML attributes.

Examples it can parse:

<my-element config='{"theme":"dark"}'></my-element>
<my-element config="{theme: 'dark', compact: true}"></my-element>
<my-element config="theme: 'dark'; compact: true"></my-element>

Think of it as JSON-like / config-like object syntax, not just strict JSON.

@prop

Prototype-level shared property or provider-backed property.

Use it to:

  • define shared constants
  • define provider-backed values
  • override inherited @attr / @boolAttr / @jsonAttr mappings in subclasses
import {attr, prop} from '@exadel/esl/modules/esl-utils/decorators';

class BasePanel extends ESLBaseElement {
  @attr({defaultValue: 'info'}) public kind: string;
}

class WarningPanel extends BasePanel {
  @prop('warning', {readonly: true}) public override kind: string;
}

Property providers

A provider is a function that receives the host as both this and argument.

(that) => that.someValue

Providers are important in ESL because they allow a value to be resolved from the current component context, including cases where a mixin reads from its host state.

Most common provider use cases:

  • @listen fields such as dynamic event, target, selector, or condition
  • @attr({defaultValue: (...) => ...})
  • @prop((that) => ...)

This is also the main way to pass the current instance into decorator configuration.


Event model: @listen vs $$on / $$off

@listen

Use @listen for class-level declarative event listeners.

import {listen} from '@exadel/esl/modules/esl-utils/decorators';

class Example extends ESLBaseElement {
  @listen('click')
  protected _onClick(e: MouseEvent): void {}

  @listen({event: 'keydown', target: 'window'})
  protected _onKeydown(e: KeyboardEvent): void {}

  @listen({event: 'click', selector: '.btn'})
  protected _onBtnClick(e: MouseEvent): void {}
}

Key idea:

  • metadata is declared on the method
  • ESL auto-subscribes on connect
  • ESL auto-unsubscribes on disconnect

Use @listen by default for stable listeners that belong to the component class.

$$on / $$off

Use them for manual or dynamic subscription control.

class Example extends ESLBaseElement {
  @listen({event: 'resize', target: 'window', auto: false})
  protected _onResize(): void {}

  protected override connectedCallback(): void {
    super.connectedCallback();
    this.$$on(this._onResize);
  }

  protected override disconnectedCallback(): void {
    this.$$off(this._onResize);
    super.disconnectedCallback();
  }
}

Use manual API when:

  • the listener is conditional
  • the target changes at runtime
  • the event type changes at runtime
  • you need to temporarily re-bind a handler

Mental split

  • @listen = declarative class contract
  • $$on / $$off = imperative runtime control

esl-event-listener ecosystem

ESL event handling is more powerful than raw addEventListener / removeEventListener.

Why it matters

It supports:

  • declarative listeners
  • delegation
  • target indirection
  • bulk unsubscribe by criteria
  • subscriptions without keeping the original callback manually
  • EventTarget adapters for observers and gestures

Important concepts

  • descriptors are attached to handlers
  • listeners are stored relative to a host object
  • unsubscription can use criteria like event name, handler, target, or group

Useful targets/adapters

These can be used directly in @listen or manual subscriptions:

  • ESLDecoratedEventTarget.for(target, decorator, ...args)
    • wraps an EventTarget with debounce/throttle-like behavior
  • ESLResizeObserverTarget.for(el)
    • gives resize events from ResizeObserver
  • ESLIntersectionTarget.for(el, settings?)
    • gives intersection events from IntersectionObserver
  • ESLSwipeGestureTarget.for(el, settings?)
    • gives swipe events
  • ESLWheelTarget.for(el, settings?)
    • gives longwheel events

Example:

import {listen} from '@exadel/esl/modules/esl-utils/decorators';
import {ESLMediaQuery} from '@exadel/esl/modules/esl-media-query/core';

class Example extends ESLBaseElement {
  @listen({event: 'change', target: ESLMediaQuery.for('@-sm')})
  protected _onViewportChange(): void {}
}

Prefer ESL event adapters when the library already exposes the observer/gesture model you need.


CSSClassUtils and $$cls

$$cls is the component-facing shortcut for host class management.

Basic use

this.$$cls('active');        // check
this.$$cls('active', true);  // add
this.$$cls('active', false); // remove

Supported token behavior

CSSClassUtils supports:

  • space-separated class lists: 'a b c'
  • inversion with !token
  • locker-aware low-level class management in the utility itself

Examples:

this.$$cls('open selected', true);
this.$$cls('hidden', false);

// inversion example on the low-level utility
CSSClassUtils.add($el, '!hidden');    // removes 'hidden'
CSSClassUtils.remove($el, '!hidden'); // adds 'hidden'

Key distinction from raw classList:

  • component code can work with token strings instead of multiple separate operations
  • the same syntax is used pervasively across ESL code

For component authoring, $$cls(...) is usually the shortest and most ergonomic host-level API, especially when class tokens come from configuration or component API.

If you are operating on non-host elements, using CSSClassUtils directly is also fully valid.


ESLMediaQuery and ESLMediaRuleList

ESLMediaQuery

Extended media condition object compatible with the event system.

Features:

  • native media query conditions
  • breakpoint shortcuts like @xs, @md, @+lg, @-sm
  • DPR shortcuts like @x2
  • environment shortcuts like @mobile, @ios, @safari
  • dynamic shortcuts through the media shortcut registry
  • tolerant parsing for logical combinations
  • works as EventTarget
  • dispatches change events with media/match information when the condition changes

Example:

import {ESLMediaQuery} from '@exadel/esl/modules/esl-media-query/core';

const mq = ESLMediaQuery.for('@md and @desktop');
if (mq.matches) {
  // desktop medium-and-up behavior
}

And with listeners:

class Example extends ESLBaseElement {
  @listen({event: 'change', target: ESLMediaQuery.for('@-sm')})
  protected _onMediaChange(): void {}
}

Practical example: a component can listen to a reduced-motion-related shortcut or condition and adapt animation behavior to user preferences without wiring raw matchMedia listeners manually.

ESLMediaRuleList

Maps media rules to values.

Useful when one attribute/config value should change by media condition.

Examples:

ESLMediaRuleList.parse('default | @xs => compact | @+md => full');
ESLMediaRuleList.parse('@xs => {gap: 8} | @+md => {gap: 16}', ESLMediaRuleList.OBJECT_PARSER);

// tuple format: values and queries are passed separately
ESLMediaRuleList.parse('1|2|3', '@xs|@md|@lg');

Use ESLMediaRuleList when the problem is not just “does this query match?” but “what value should be active under current conditions?”.

It supports both:

  • arrow-rule format: default | @xs => compact | @+md => full
  • tuple format: values, queries

Related decorators worth knowing

These are not the main focus of ESL Core, but often appear in real code:

  • @ready — defer execution until the DOM is ready (DOMContentLoaded) and the next task
  • @bind — lazy per-instance method binding
  • @decorate — wrap methods with debounce/throttle-like decorators
  • @memoize — cache getter/method results
  • @safe — catch sync errors and report through $$error

Common mistakes to avoid

  • Importing from repository internals instead of public @exadel/esl/modules/.../core paths.
  • Treating ESLBaseElement and ESLMixinElement as separate ecosystems instead of one shared model with different hosts.
  • Forgetting register().
  • Forgetting super.connectedCallback() / super.disconnectedCallback().
  • Using plain CSS lookup where ESL traversing syntax would better express a component relationship or a user-provided target API.
  • Using raw addEventListener for static class-owned listeners instead of @listen.
  • Using @boolAttr when a tri-state or inherited value actually requires @attr.
  • Assuming @jsonAttr accepts only strict JSON.
  • Forgetting that mixin logic acts on $host, not on the mixin instance as a DOM node.

Practical rule of thumb

When generating ESL consumer code:

  1. Choose the host model first: tag or mixin.
  2. Import from public core entrypoints.
  3. Use decorators for attribute/state mapping.
  4. Use @listen for stable listeners.
  5. Use $$on / $$off for dynamic listeners.
  6. Use $$find / $$findAll for component-relative lookup.
  7. Use $$cls / $$attr for host state reflection.
  8. Reach for ESLMediaQuery / ESLMediaRuleList when responsiveness is part of the API.

ESL Review

Review checklist and best practices for ESL consumer code.

Use when: You want an AI agent to review ESL-based code for correct host model, imports, lifecycle, decorators, events, traversal, and responsive patterns.

Download .md
Preview skill content

Skill: ESL Review

Version target: This review skill is suitable for ESL 5+ consumer code.

When to use: You are reviewing consumer code that uses @exadel/esl and need to check whether it follows ESL's core patterns, public API boundaries, and idiomatic component authoring style.

Primary goal: Catch code that technically works but ignores ESL conventions, bypasses built-in helpers, or misuses the element/mixin model.


Review mindset

Review ESL code through these questions:

  1. Is the correct host model used (ESLBaseElement vs ESLMixinElement)?
  2. Are imports taken from public @exadel/esl entrypoints?
  3. Does the code use ESL primitives (@attr, @listen, $$find, $$cls, ESLMediaQuery) instead of re-implementing them manually?
  4. Does lifecycle code preserve ESL auto-subscription / auto-cleanup behavior?
  5. Is responsive or event-driven behavior expressed in the ESL way rather than raw low-level APIs?

1. Host model correctness

Good signals

  • New custom tag extends ESLBaseElement.
  • Attribute-driven behavior extends ESLMixinElement.
  • Mixin code consistently works through $host.
  • The code uses the shared $$* APIs instead of treating mixins as DOM nodes.

Review questions

  • Should this be a tag or a mixin?
  • Is a mixin chosen only because behavior must attach to an existing element?
  • Does mixin code accidentally use this where this.$host is the real DOM target?

Common issue

A mixin is written like a custom element and manipulates this.classList, this.querySelector, or this.dispatchEvent directly.

Preferred direction

Use:

  • this.$$cls(...)
  • this.$$find(...)
  • this.$$fire(...)
  • this.$$attr(...)

These already target the correct host semantics.


2. Public import boundaries

Good signals

  • Imports come from public @exadel/esl/modules/.../core entries or from root @exadel/esl in a tree-shaken setup.
  • Consumer code does not reference repository internals.

Review questions

  • Is the code importing a public entry or an implementation detail?
  • Is the import stable for npm consumers?

Red flags

  • imports from internal subfolders or repository-only paths
  • imports that bypass public package entrypoints entirely

Preferred direction

Use public package entries such as:

import {ESLBaseElement} from '@exadel/esl/modules/esl-base-element/core';
import {ESLMixinElement} from '@exadel/esl/modules/esl-mixin-element/core';
import {attr, boolAttr, jsonAttr, prop, listen} from '@exadel/esl/modules/esl-utils/decorators';

// or, in tree-shaken setups
import {ESLBaseElement, listen} from '@exadel/esl';

3. Registration and lifecycle

Good signals

  • static is is declared in the class.
  • register() is called.
  • super.connectedCallback() is preserved.
  • super.disconnectedCallback() is preserved.

Review questions

  • Was registration forgotten?
  • Is lifecycle code preserving ESL auto-subscription behavior?
  • Is attributeChangedCallback doing expensive work on redundant writes?

Red flags

  • missing register()
  • super.connectedCallback() omitted
  • super.disconnectedCallback() omitted
  • custom lifecycle logic that breaks auto-subscribe / auto-unsubscribe assumptions

Preferred direction

Guard attribute-change logic when necessary:

class Example extends ESLBaseElement {
  protected override attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
    if (oldValue === newValue) return;
    // real update logic
  }
}

4. Attribute and property modeling

Good signals

  • @attr used for typed attribute-backed properties.
  • @boolAttr used for presence booleans.
  • @jsonAttr used for object-like config attributes.
  • @prop used for shared constants or overriding inherited attribute mappings.

Review questions

  • Is the chosen decorator the simplest correct one?
  • Should a boolean be @boolAttr, or is it really tri-state and better modeled with @attr?
  • Is object config manually parsed even though @jsonAttr already exists?
  • Is subclass behavior overriding an inherited attribute mapping in a clean way?

Red flags

  • manual getAttribute / setAttribute boilerplate for simple cases
  • parsing JSON manually in component code without a strong reason
  • using @boolAttr for a value that needs a default-enabled or explicit-false model
  • using instance fields where @prop would better express a shared constant

Preferred direction

  • Prefer decorators over manual attribute plumbing.
  • Prefer @prop when the goal is to replace inherited attribute-backed behavior with a fixed or provider-based value.
  • Remember that @jsonAttr supports relaxed JSON-like config syntax, not only strict JSON.

5. Event model and listener ownership

Good signals

  • Stable class-owned listeners use @listen.
  • Dynamic or conditional listeners use $$on / $$off.
  • Listener targets and delegation are expressed declaratively when possible.

Review questions

  • Should this listener be declarative?
  • Is manual subscription really necessary here?
  • Does the code rely on raw DOM event wiring where ESL already gives a better abstraction?
  • Are re-subscriptions handled correctly when target/event/selector is dynamic?

Red flags

  • raw addEventListener / removeEventListener for stable component listeners
  • manual cleanup forgotten
  • @listen({auto: false}) declared but never actually subscribed
  • dynamic event/target/selector changes without unsubscribe + resubscribe

Preferred direction

  • @listen for stable listeners.
  • $$on / $$off for runtime control.
  • Use handler-reference unsubscription when refreshing dynamic listeners.

6. Traversal and target resolution

Good signals

  • $$find / $$findAll used for component-relative queries.
  • ESLTraversingQuery syntax is used when the relationship is structural (::parent, ::closest, ::find, ::next, ::prev).

Review questions

  • Is this really a plain CSS lookup, or is it a component-relative relationship?
  • Would $$find with traversing syntax make the intent clearer?

Red flags

  • verbose closest / parentElement / querySelector chains for patterns already expressible via ESLTraversingQuery
  • mixing global document lookup into code that should stay host-relative

Preferred direction

Prefer concise traversing queries for component relationships.


7. Host state reflection

Good signals

  • $$cls is used for component-driven class reflection.
  • $$attr is used for direct attribute reflection when decorators are not the right fit.
  • State naming and reflection are consistent.

Review questions

  • Is this host-state reflection part of the component contract?
  • Would $$cls / $$attr express the intent more clearly than raw DOM mutation?

Red flags

  • repetitive classList.add/remove or setAttribute/removeAttribute sequences for simple component state
  • class reflection scattered across unrelated methods without a clear model

Preferred direction

Use $$cls and $$attr where the code is expressing component state on the host.


8. Media and responsive logic

Good signals

  • ESLMediaQuery is used when responsiveness is part of the component behavior.
  • ESLMediaRuleList is used when media conditions map to values/configs.
  • media conditions are treated as observable sources through the ESL event layer.

Review questions

  • Is responsive behavior manually reimplemented with window resize checks?
  • Should this value be modeled as a media-rule list instead of imperative if/else code?

Red flags

  • manual matchMedia plumbing where ESLMediaQuery would be clearer
  • manual resize logic for value switching that belongs to ESLMediaRuleList
  • component code hardcoding breakpoint logic in many places instead of centralizing it

Preferred direction

Prefer ESLMediaQuery / ESLMediaRuleList when responsiveness is part of the API rather than a one-off imperative detail.


9. esl-event-listener ecosystem usage

Good signals

  • observer/gesture behavior uses ESL adapters where available
  • event targets are reused through built-in wrappers
  • the code takes advantage of EventTarget compatibility

Review questions

  • Is the code manually managing ResizeObserver, IntersectionObserver, swipe, or wheel state where ESL already exposes an adapter?
  • Could the listener become simpler if an ESL target wrapper was used?

Red flags

  • custom wrapper code around observers already supported by ESL
  • bespoke debounce/throttle event target logic instead of ESLDecoratedEventTarget

Preferred direction

Prefer existing ESL adapters such as:

  • ESLResizeObserverTarget
  • ESLIntersectionTarget
  • ESLSwipeGestureTarget
  • ESLWheelTarget
  • ESLDecoratedEventTarget

Short review checklist

Before approving ESL consumer code, verify:

  • [ ] Correct host model: ESLBaseElement vs ESLMixinElement
  • [ ] Public imports only
  • [ ] static is and register() are correct
  • [ ] Lifecycle preserves super.connectedCallback() / super.disconnectedCallback()
  • [ ] Attribute state uses decorators instead of unnecessary manual plumbing
  • [ ] Stable listeners use @listen
  • [ ] Dynamic listeners use $$on / $$off
  • [ ] DOM lookup uses $$find / $$findAll where component-relative traversal matters
  • [ ] Host state reflection uses $$cls / $$attr where appropriate
  • [ ] Responsive logic uses ESLMediaQuery / ESLMediaRuleList when it is part of the component model
  • [ ] No unnecessary reimplementation of existing ESL utilities

Practical rule of thumb

If the code looks like generic DOM code with a thin ESL wrapper on top, it is usually worth asking:

Which ESL primitive should own this behavior instead?

Most of the time the answer is one of:

  • @attr / @boolAttr / @jsonAttr
  • @listen
  • $$find / $$findAll
  • $$cls / $$attr
  • ESLMediaQuery / ESLMediaRuleList
  • an existing esl-event-listener target adapter