or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

tessl/npm-lit-labs--ssr-dom-shim

DOM shim for Lit Server Side Rendering (SSR) providing minimal implementations of DOM APIs for Node.js environments.

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/@lit-labs/ssr-dom-shim@1.4.x

To install, run

npx @tessl/cli install tessl/npm-lit-labs--ssr-dom-shim@1.4.0

index.mddocs/

@lit-labs/ssr-dom-shim

@lit-labs/ssr-dom-shim provides minimal implementations of core DOM APIs (Element, HTMLElement, EventTarget, Event, CustomEvent, CustomElementRegistry, and customElements) specifically designed for Server Side Rendering (SSR) web components from Node.js environments. This enables Lit components and other web components to run in server environments without requiring a full DOM implementation.

Package Information

  • Package Name: @lit-labs/ssr-dom-shim
  • Package Type: npm
  • Language: TypeScript
  • Installation: npm install @lit-labs/ssr-dom-shim

Core Imports

import { 
  Element, 
  HTMLElement, 
  EventTarget, 
  Event, 
  CustomEvent, 
  CustomElementRegistry, 
  customElements,
  ElementInternals,
  ariaMixinAttributes,
  HYDRATE_INTERNALS_ATTR_PREFIX,
  type HTMLElementWithEventMeta
} from "@lit-labs/ssr-dom-shim";

For CommonJS:

const { 
  Element, 
  HTMLElement, 
  EventTarget, 
  Event, 
  CustomEvent, 
  CustomElementRegistry,
  customElements,
  ElementInternals,
  ariaMixinAttributes,
  HYDRATE_INTERNALS_ATTR_PREFIX
} = require("@lit-labs/ssr-dom-shim");

Basic Usage

import { HTMLElement, customElements, Event } from "@lit-labs/ssr-dom-shim";

// Define a custom element
class MyComponent extends HTMLElement {
  connectedCallback() {
    this.setAttribute('initialized', 'true');
    this.dispatchEvent(new Event('component-ready'));
  }
}

// Register the custom element
customElements.define('my-component', MyComponent);

// Create and use the element
const element = new MyComponent();
element.setAttribute('title', 'Hello SSR');
console.log(element.getAttribute('title')); // "Hello SSR"

// Event handling
element.addEventListener('component-ready', () => {
  console.log('Component is ready!');
});

Architecture

The SSR DOM shim is built around several key components:

  • Element Hierarchy: EventTargetElementHTMLElement providing the standard DOM inheritance chain
  • Event System: Complete event lifecycle with capturing, bubbling, and composition across shadow boundaries
  • Custom Elements: Registry system supporting web component definition, lookup, and lifecycle management
  • Element Internals: Form association and ARIA support for accessibility in SSR contexts
  • Attribute Management: WeakMap-based attribute storage maintaining browser-like behavior
  • Shadow DOM: Lightweight shadow root support with proper encapsulation boundaries

Capabilities

Event System

Core event handling functionality compatible with browser EventTarget API, supporting all standard event lifecycle phases.

class EventTarget {
  /**
   * Add an event listener with optional capture and once behaviors
   * @param type - Event type to listen for
   * @param callback - Event handler function or object with handleEvent method
   * @param options - Configuration options or boolean for capture mode
   */
  addEventListener(
    type: string,
    callback: EventListenerOrEventListenerObject | null,
    options?: AddEventListenerOptions | boolean
  ): void;

  /**
   * Remove an event listener
   * @param type - Event type
   * @param callback - Event handler to remove
   * @param options - Configuration options or boolean for capture mode
   */
  removeEventListener(
    type: string,
    callback: EventListenerOrEventListenerObject | null,
    options?: EventListenerOptions | boolean
  ): void;

  /**
   * Dispatch an event through the event system
   * @param event - Event instance to dispatch
   * @returns true if event was not cancelled
   */
  dispatchEvent(event: Event): boolean;
}

/**
 * Basic Event implementation with standard DOM Event API
 */
class Event {
  /**
   * Create a new Event
   * @param type - Event type name
   * @param options - Event configuration
   */
  constructor(type: string, options?: EventInit);

  readonly type: string;
  readonly bubbles: boolean;
  readonly cancelable: boolean;
  readonly composed: boolean;
  readonly defaultPrevented: boolean;
  readonly target: EventTarget | null;
  readonly currentTarget: EventTarget | null;
  readonly eventPhase: number;
  readonly timeStamp: number;
  readonly isTrusted: boolean;

  /** Prevent default behavior */
  preventDefault(): void;
  /** Stop event propagation to parent elements */
  stopPropagation(): void;
  /** Stop all further event handler execution */
  stopImmediatePropagation(): void;
  /** Get the event path for this event */
  composedPath(): EventTarget[];

  // Event phase constants
  static readonly NONE = 0;
  static readonly CAPTURING_PHASE = 1;
  static readonly AT_TARGET = 2;
  static readonly BUBBLING_PHASE = 3;
}

/**
 * CustomEvent with detail data payload
 */
class CustomEvent<T = any> extends Event {
  /**
   * Create a new CustomEvent with detail data
   * @param type - Event type name
   * @param options - Event configuration including detail property
   */
  constructor(type: string, options?: CustomEventInit<T>);

  /** Custom data payload */
  readonly detail: T;
}

interface EventInit {
  bubbles?: boolean;
  cancelable?: boolean;
  composed?: boolean;
}

interface CustomEventInit<T = any> extends EventInit {
  detail?: T;
}

interface AddEventListenerOptions {
  capture?: boolean;
  once?: boolean;
  passive?: boolean;
  signal?: AbortSignal;
}

interface EventListenerOptions {
  capture?: boolean;
}

DOM Elements

Element implementations providing attribute management, shadow DOM support, and element internals for SSR environments.

/**
 * Base Element class with attribute management and shadow DOM support
 */
class Element extends EventTarget {
  /** Array of element attributes as name/value pairs */
  readonly attributes: Array<{ name: string; value: string }>;
  /** Element's local name (tag name in lowercase) */
  readonly localName: string | undefined;
  /** Element's tag name (uppercase local name) */
  readonly tagName: string | undefined;
  /** Shadow root attached to this element (null for closed shadows) */
  readonly shadowRoot: ShadowRoot | null;

  /**
   * Set an attribute value (silently converts any value to string)
   * @param name - Attribute name  
   * @param value - Attribute value (converted to string)
   */
  setAttribute(name: string, value: unknown): void;

  /**
   * Get an attribute value
   * @param name - Attribute name
   * @returns Attribute value or null if not present
   */
  getAttribute(name: string): string | null;

  /**
   * Remove an attribute
   * @param name - Attribute name to remove
   */
  removeAttribute(name: string): void;

  /**
   * Check if element has an attribute
   * @param name - Attribute name to check
   * @returns true if attribute exists
   */
  hasAttribute(name: string): boolean;

  /**
   * Toggle an attribute's existence
   * @param name - Attribute name
   * @param force - Optional force flag to explicitly add/remove
   * @returns true if attribute exists after toggle
   */
  toggleAttribute(name: string, force?: boolean): boolean;

  /**
   * Attach a shadow root to this element
   * @param init - Shadow root configuration
   * @returns Shadow root instance
   */
  attachShadow(init: ShadowRootInit): ShadowRoot;

  /**
   * Attach ElementInternals for form association and ARIA
   * @returns ElementInternals instance
   * @throws Error if internals already attached
   */
  attachInternals(): ElementInternals;
}

/**
 * HTMLElement class extending Element for HTML-specific functionality
 */
class HTMLElement extends Element {}

interface ShadowRootInit {
  mode: ShadowRootMode;
}

type ShadowRootMode = 'open' | 'closed';

interface ShadowRoot {
  readonly host: Element;
}

Custom Elements Registry

Custom element registration and management system supporting web component definition, lookup, and lifecycle management for SSR contexts.

/**
 * Registry for custom element definitions
 */
class CustomElementRegistry {
  /**
   * Define a custom element
   * @param name - Element tag name (must contain hyphen)
   * @param ctor - Element constructor class
   * @throws Error if name already registered or constructor already used
   */
  define(name: string, ctor: CustomHTMLElementConstructor): void;

  /**
   * Get constructor for a custom element name
   * @param name - Element tag name
   * @returns Constructor class or undefined if not registered
   */
  get(name: string): CustomHTMLElementConstructor | undefined;

  /**
   * Get element name for a constructor
   * @param ctor - Element constructor
   * @returns Element name or null if not registered
   */
  getName(ctor: CustomHTMLElementConstructor): string | null;

  /**
   * Promise that resolves when element is defined
   * @param name - Element tag name to wait for
   * @returns Promise resolving with constructor when defined
   */
  whenDefined(name: string): Promise<CustomElementConstructor>;

  /**
   * Upgrade an element (not supported in SSR)
   * @param element - Element to upgrade
   * @throws Error indicating SSR limitation
   */
  upgrade(element: HTMLElement): void;
}

/** Global custom elements registry instance */
const customElements: CustomElementRegistry;

interface CustomHTMLElementConstructor {
  new (): HTMLElement;
  observedAttributes?: string[];
}

type CustomElementConstructor = CustomHTMLElementConstructor;

Element Internals and ARIA

ElementInternals implementation providing form association capabilities and comprehensive ARIA attribute support for accessibility in SSR environments.

/**
 * ElementInternals class for form association and ARIA support
 */
class ElementInternals {
  /** Host element that this internals instance is attached to */
  readonly __host: HTMLElement;
  /** Shadow root of the host element */
  readonly shadowRoot: ShadowRoot;

  // ARIA Properties (all string type, settable)
  ariaAtomic: string;
  ariaAutoComplete: string;
  ariaBrailleLabel: string;
  ariaBrailleRoleDescription: string;
  ariaBusy: string;
  ariaChecked: string;
  ariaColCount: string;
  ariaColIndex: string;
  ariaColSpan: string;
  ariaCurrent: string;
  ariaDescription: string;
  ariaDisabled: string;
  ariaExpanded: string;
  ariaHasPopup: string;
  ariaHidden: string;
  ariaInvalid: string;
  ariaKeyShortcuts: string;
  ariaLabel: string;
  ariaLevel: string;
  ariaLive: string;
  ariaModal: string;
  ariaMultiLine: string;
  ariaMultiSelectable: string;
  ariaOrientation: string;
  ariaPlaceholder: string;
  ariaPosInSet: string;
  ariaPressed: string;
  ariaReadOnly: string;
  ariaRequired: string;
  ariaRoleDescription: string;
  ariaRowCount: string;
  ariaRowIndex: string;
  ariaRowSpan: string;
  ariaSelected: string;
  ariaSetSize: string;
  ariaSort: string;
  ariaValueMax: string;
  ariaValueMin: string;
  ariaValueNow: string;
  ariaValueText: string;
  role: string;

  // Form-related Properties
  /** Associated form element */
  readonly form: HTMLFormElement | null;
  /** Associated label elements */
  readonly labels: NodeListOf<HTMLLabelElement>;
  /** Custom element states */
  readonly states: Set<string>;
  /** Current validation message */
  readonly validationMessage: string;
  /** Current validity state */
  readonly validity: ValidityState;
  /** Whether element will validate */
  readonly willValidate: boolean;

  /**
   * Check element validity (always returns true in SSR)
   * @returns true (always valid in SSR environment)
   */
  checkValidity(): boolean;

  /**
   * Report element validity (always returns true in SSR)
   * @returns true (always valid in SSR environment)
   */
  reportValidity(): boolean;

  /**
   * Set form value (no-op in SSR)
   */
  setFormValue(): void;

  /**
   * Set element validity (no-op in SSR)
   */
  setValidity(): void;
}

/**
 * Mapping of ARIA property names to their corresponding HTML attribute names
 */
const ariaMixinAttributes: {
  readonly [K in keyof ARIAMixin]: string;
};

/**
 * Prefix for hydration-related internal attributes
 */
const HYDRATE_INTERNALS_ATTR_PREFIX: string;

/**
 * Internal type combining HTMLElement with event polyfill metadata
 * Used internally for event system functionality
 */
type HTMLElementWithEventMeta = HTMLElement & EventTargetShimMeta;

interface EventTargetShimMeta {
  /**
   * The event target parent represents the previous event target for an event
   * in capture phase and the next event target for a bubbling event.
   * Note that this is not the element parent
   */
  __eventTargetParent: EventTarget | undefined;
  /**
   * The host event target/element of this event target, if this event target
   * is inside a Shadow DOM.
   */
  __host: EventTarget | undefined;
}

interface ARIAMixin {
  ariaAtomic: string | null;
  ariaAutoComplete: string | null;
  ariaBrailleLabel: string | null;
  ariaBrailleRoleDescription: string | null;
  ariaBusy: string | null;
  ariaChecked: string | null;
  ariaColCount: string | null;
  ariaColIndex: string | null;
  ariaColSpan: string | null;
  ariaCurrent: string | null;
  ariaDescription: string | null;
  ariaDisabled: string | null;
  ariaExpanded: string | null;
  ariaHasPopup: string | null;
  ariaHidden: string | null;
  ariaInvalid: string | null;
  ariaKeyShortcuts: string | null;
  ariaLabel: string | null;
  ariaLevel: string | null;
  ariaLive: string | null;
  ariaModal: string | null;
  ariaMultiLine: string | null;
  ariaMultiSelectable: string | null;
  ariaOrientation: string | null;
  ariaPlaceholder: string | null;
  ariaPosInSet: string | null;
  ariaPressed: string | null;
  ariaReadOnly: string | null;
  ariaRequired: string | null;
  ariaRoleDescription: string | null;
  ariaRowCount: string | null;
  ariaRowIndex: string | null;
  ariaRowSpan: string | null;
  ariaSelected: string | null;
  ariaSetSize: string | null;
  ariaSort: string | null;
  ariaValueMax: string | null;
  ariaValueMin: string | null;
  ariaValueNow: string | null;
  ariaValueText: string | null;
  role: string | null;
}

interface ValidityState {
  readonly badInput: boolean;
  readonly customError: boolean;
  readonly patternMismatch: boolean;
  readonly rangeOverflow: boolean;
  readonly rangeUnderflow: boolean;
  readonly stepMismatch: boolean;
  readonly tooLong: boolean;
  readonly tooShort: boolean;
  readonly typeMismatch: boolean;
  readonly valid: boolean;
  readonly valueMissing: boolean;
}

Usage Examples

Custom Element with Shadow DOM

import { HTMLElement, customElements } from "@lit-labs/ssr-dom-shim";

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.setAttribute('role', 'article');
    this.setAttribute('aria-label', 'Card component');
  }
}

customElements.define('my-card', MyCard);

const card = new MyCard();
console.log(card.shadowRoot); // ShadowRoot object
console.log(card.getAttribute('role')); // "article"

Event Handling with Custom Events

import { HTMLElement, CustomEvent } from "@lit-labs/ssr-dom-shim";

class EventComponent extends HTMLElement {
  emitCustomEvent() {
    const event = new CustomEvent('data-updated', {
      detail: { timestamp: Date.now(), value: 'new data' },
      bubbles: true,
      composed: true
    });
    
    this.dispatchEvent(event);
  }

  connectedCallback() {
    this.addEventListener('data-updated', (event) => {
      console.log('Data updated:', event.detail);
    });
  }
}

Form Integration with ElementInternals

import { HTMLElement, customElements } from "@lit-labs/ssr-dom-shim";

class FormInput extends HTMLElement {
  private internals: ElementInternals;

  constructor() {
    super();
    this.internals = this.attachInternals();
  }

  connectedCallback() {
    // Set ARIA properties
    this.internals.ariaLabel = 'Custom input field';
    this.internals.ariaRequired = 'true';
    this.internals.role = 'textbox';
  }
}

customElements.define('form-input', FormInput);

Registry Management

import { customElements, HTMLElement } from "@lit-labs/ssr-dom-shim";

// Define multiple components
customElements.define('button-component', class extends HTMLElement {});
customElements.define('input-component', class extends HTMLElement {});

// Check if component is defined
if (customElements.get('button-component')) {
  console.log('Button component is available');
}

// Wait for component to be defined
customElements.whenDefined('future-component').then(() => {
  console.log('Future component has been defined');
});

// Get component name from constructor
const ButtonComponent = customElements.get('button-component');
if (ButtonComponent) {
  console.log(customElements.getName(ButtonComponent)); // "button-component"
}

Global Environment Integration

The package automatically sets up global bindings when imported:

// These globals are conditionally assigned (only if undefined)
globalThis.Event; // Event implementation
globalThis.CustomEvent; // CustomEvent implementation
globalThis.litServerRoot; // Global HTMLElement for event handling

This ensures compatibility with Lit and other libraries that expect these globals to be available in the SSR environment.

Error Handling

The shim implementations include appropriate error handling for SSR-specific limitations:

  • customElements.upgrade() throws an error as upgrading is not supported in SSR contexts
  • ElementInternals.attachInternals() throws if internals are already attached
  • Form validation methods (checkValidity, reportValidity) log warnings and return sensible defaults for SSR

These behaviors maintain API compatibility while providing clear feedback about SSR environment constraints.