Skip to content

Instantly share code, notes, and snippets.

@kentcdodds
Created October 11, 2025 04:13
Show Gist options
  • Save kentcdodds/d8903f6c51763aa8d681af2982b90831 to your computer and use it in GitHub Desktop.
Save kentcdodds/d8903f6c51763aa8d681af2982b90831 to your computer and use it in GitHub Desktop.
Unofficial, AI-generated @remix-run/dom and @remix-run/events docs

@remix-run/events and @remix-run/dom Documentation

NOTE: this was generated by Cursor from within the Remix demo using the following prompt:

Please use the example code in @public/ and the types definitions to write documentation for @remix-run/events and @remix-run/dom and stick it in a markdown file in the root of this repo.

This documentation covers two core Remix 3 packages: @remix-run/events for declarative event handling and @remix-run/dom for creating reactive UI components.

Table of Contents


@remix-run/events

A declarative event handling library that provides a clean, composable API for managing event listeners across any EventTarget.

Core API

events(target, descriptors)

Adds events to a target and returns a cleanup function. This is the primary way to attach event listeners.

import { events, dom } from "@remix-run/events";

let cleanup = events(target, [
  dom.click(event => {
    console.log(event.target);
  })
]);

// Later: cleanup all event listeners
cleanup();

With EventTarget objects:

import { events } from "@remix-run/events";

let drummer = new Drummer(80);

events(drummer, [
  Drummer.change(() => this.render()),
  Drummer.kick(() => {
    console.log('kick!');
  })
]);

On document:

import { events } from "@remix-run/events";
import { space, arrowUp, arrowDown } from "@remix-run/events/key";

events(document, [
  space(() => {
    drummer.toggle();
  }),
  arrowUp(() => {
    drummer.setTempo(drummer.bpm + 1);
  }),
  arrowDown(() => {
    drummer.setTempo(drummer.bpm - 1);
  }),
]);

events(target)

Creates an event container that allows dynamic event management.

let container = events(target);

container.on([
  dom.click(event => {
    console.log("first handler");
  })
]);

// Change events dynamically
container.on([
  dom.mouseover(event => {
    console.log("new handler");
  })
]);

// Clean up all events
container.cleanup();

bind(type, handler, options?)

Attaches a raw string event to a target. Particularly useful for custom elements and web components.

import { events, bind } from "@remix-run/events";

events(target, [
  bind("custom-event", event => {
    console.log(event.target);
  })
]);

Type Signature:

function bind<E extends Event = Event, ECurrentTarget = any, ETarget = any>(
  type: string,
  handler: EventHandler<E, ECurrentTarget, ETarget>,
  options?: AddEventListenerOptions
): EventDescriptor<ECurrentTarget>

Target Helpers

Pre-configured target proxies that provide type-safe access to native DOM events.

dom - HTMLElement Events

import { events, dom } from "@remix-run/events";

events(element, [
  dom.click(event => { /* ... */ }),
  dom.mouseover(event => { /* ... */ }),
  dom.pointermove(event => { /* ... */ }),
  // All HTMLElementEventMap events available
]);

win - Window Events

import { events, win } from "@remix-run/events";

events(window, [
  win.resize(event => { /* ... */ }),
  win.scroll(event => { /* ... */ }),
]);

doc - Document Events

import { events, doc } from "@remix-run/events";

events(document, [
  doc.DOMContentLoaded(event => { /* ... */ }),
  doc.visibilitychange(event => { /* ... */ }),
]);

xhr - XMLHttpRequest Events

import { events, xhr } from "@remix-run/events";

let request = new XMLHttpRequest();
events(request, [
  xhr.load(event => { /* ... */ }),
  xhr.error(event => { /* ... */ }),
]);

ws - WebSocket Events

import { events, ws } from "@remix-run/events";

let socket = new WebSocket('ws://localhost:8080');
events(socket, [
  ws.message(event => { /* ... */ }),
  ws.error(event => { /* ... */ }),
]);

Custom Event Types

createEventType(eventName)

Creates a pair of functions: an event binder and an event creator. This is perfect for creating type-safe custom events on EventTarget subclasses.

import { createEventType } from '@remix-run/events';

// Create event types
let [kick, createKick] = createEventType('drum:kick');
let [snare, createSnare] = createEventType('drum:snare');
let [tempoChange, createTempoChange] = createEventType<number>('drum:tempo-change');

export class Drummer extends EventTarget {
  // Export as static methods for easy access
  static kick = kick;
  static snare = snare;
  static tempoChange = tempoChange;

  playKick() {
    // Dispatch the event
    this.dispatchEvent(createKick());
  }

  setTempo(bpm: number) {
    // Dispatch with detail
    this.dispatchEvent(createTempoChange({ detail: bpm }));
  }
}

// Usage
let drummer = new Drummer();
events(drummer, [
  Drummer.kick(() => {
    console.log('kick!');
  }),
  Drummer.tempoChange((event) => {
    console.log('tempo changed to', event.detail);
  })
]);

Return Value:

[
  // Event binder function
  <ECurrentTarget extends EventTarget = EventTarget>(
    handler: EventHandler<CustomEvent<Detail>, ECurrentTarget>,
    options?: AddEventListenerOptions
  ) => EventDescriptor<ECurrentTarget>,
  
  // Event creator function
  (...args: [init?: CustomEventInit<Detail>]) => CustomEvent<Detail>
]

Interactions

Interactions are higher-level event patterns that combine multiple low-level events into a single semantic event.

createInteraction(eventName, factory)

Creates a custom interaction that encapsulates complex event patterns.

import { createInteraction, events } from "@remix-run/events";
import { press } from "@remix-run/events/press";

let tempoTap = createInteraction<HTMLElement, number>(
  "tempo-tap",
  ({ target, dispatch }) => {
    let taps: number[] = [];
    let minTaps = 4;
    let maxInterval = 2000;
    let resetTimer: number;

    let handleTap = () => {
      let now = Date.now();
      clearTimeout(resetTimer);
      taps.push(now);
      taps = taps.filter(tap => now - tap < maxInterval);
      
      if (taps.length >= minTaps) {
        let intervals = [];
        for (let i = 1; i < taps.length; i++) {
          intervals.push(taps[i] - taps[i - 1]);
        }
        let bpms = intervals.map(interval => 60000 / interval);
        let avgBpm = Math.round(
          bpms.reduce((sum, value) => sum + value, 0) / bpms.length,
        );
        dispatch({ detail: avgBpm });
      }

      resetTimer = window.setTimeout(() => {
        taps = [];
      }, maxInterval);
    };

    // Return cleanup function(s)
    return events(target, [press(handleTap)]);
  },
);

// Usage
<Button
  on={[
    tempoTap(event => {
      drummer.play(event.detail);
    }),
  ]}
>
  SET TEMPO
</Button>

Factory Context:

{
  dispatch: (options?: CustomEventInit<Detail>, originalEvent?: Event) => void;
  target: Target;
}

Factory Return: Cleanup | Cleanup[] | void

Built-in Interactions

Press Interactions

High-level pointer and keyboard interactions for button-like elements.

press(handler, options?)

A complete press interaction that handles both pointer and keyboard activation (Space/Enter keys).

import { press } from "@remix-run/events/press";

<Button
  on={[
    press(event => {
      console.log('Button pressed!');
      console.log('Input type:', event.detail.inputType); // 'pointer' | 'keyboard'
      console.log('Original event:', event.detail.originalEvent);
    })
  ]}
>
  PLAY
</Button>

Options:

interface PressOptions {
  hit?: number;      // Hit detection threshold (default varies)
  release?: number;  // Release threshold (default varies)
  delay?: number;    // Long press delay (default varies)
}

Event Detail:

type PressEventDetail = {
  originalEvent: PointerEvent;
  target: Element;
  inputType: 'pointer';
} | {
  originalEvent: KeyboardEvent;
  target: Element;
  inputType: 'keyboard';
}
pressDown(handler, options?)

Fires when the press starts (pointer down or key down).

<Button on={[pressDown(event => { /* ... */ })]}>
  Press Me
</Button>
  • Sets rmx-active="true" attribute on the target during press
pressUp(handler, options?)

Fires when the press ends (pointer up or key up).

<Button on={[pressUp(event => { /* ... */ })]}>
  Press Me
</Button>
  • Removes rmx-active attribute from the target
longPress(handler, options?)

Fires after holding the press for a specified duration.

<Button on={[longPress(event => { /* ... */ }, { delay: 500 })]}>
  Hold Me
</Button>
outerPress(handler) / outerPressDown(handler) / outerPressUp(handler)

Detects presses outside the target element. Useful for closing modals, dropdowns, etc.

import { outerPress } from "@remix-run/events/press";

<Modal on={[outerPress(() => closeModal())]}>
  {/* Modal content */}
</Modal>

Outer Press Event Detail:

interface OuterPressEventDetail {
  originalEvent: PointerEvent;
}

Key Interactions

Keyboard interactions that automatically prevent default browser behavior and follow WAI-ARIA practices.

All key interactions come from @remix-run/events/key:

import { 
  space, 
  enter, 
  escape,
  arrowUp,
  arrowDown,
  arrowLeft,
  arrowRight,
  home,
  end,
  pageUp,
  pageDown,
  tab,
  backspace,
  del
} from "@remix-run/events/key";

Example Usage:

events(document, [
  space(() => {
    drummer.toggle();
  }),
  arrowUp(() => {
    drummer.setTempo(drummer.bpm + 1);
  }),
  arrowDown(() => {
    drummer.setTempo(drummer.bpm - 1);
  }),
]);

Available Key Interactions:

  • space - Space key (useful for triggering actions)
  • enter - Enter key (useful for submitting forms, selecting items)
  • escape - Escape key (useful for closing modals/menus)
  • arrowUp / arrowDown / arrowLeft / arrowRight - Arrow keys
  • home / end - Move to first/last item
  • pageUp / pageDown - Page navigation
  • tab - Tab key
  • backspace / del - Deletion keys

Event Detail:

type KeyInteractionEvent = CustomEvent<{
  originalEvent: KeyboardEvent;
}>
createKeyInteraction(key)

Create a custom key interaction for any key:

import { createKeyInteraction } from "@remix-run/events/key";

let ctrlS = createKeyInteraction('s');

events(document, [
  ctrlS(event => {
    if (event.detail.originalEvent.ctrlKey) {
      // Save document
    }
  })
]);

@remix-run/dom

A reactive component system that provides efficient DOM rendering and updates.

Component System

Components in Remix 3 are defined as functions with this: Remix.Handle.

import type { Remix } from "@remix-run/dom";

function MyComponent(this: Remix.Handle) {
  // Setup code runs once
  let drummer = this.context.get(DrumMachine);
  
  events(drummer, [
    Drummer.change(() => this.render())
  ]);

  // Return render function
  return () => (
    <div>
      <h1>BPM: {drummer.bpm}</h1>
    </div>
  );
}

Key Features:

  • Setup code runs once when component mounts
  • Return a render function that runs on each render
  • Access to lifecycle methods via this.Handle

Component Types

function DrumMachine(this: Remix.Handle<Drummer>) {
  // Generic type provides context type
  let drummer = new Drummer(80);
  this.context.set(drummer);
  
  return () => <Layout><Equalizer /></Layout>;
}

function Equalizer(this: Remix.Handle) {
  // Access parent context
  let drummer = this.context.get(DrumMachine);
  
  return () => <div>{drummer.bpm}</div>;
}

Remix.Handle API

this.render()

Trigger a re-render of the component.

events(drummer, [
  Drummer.change(() => this.render())
]);

this.context.set(value)

Set a context value that child components can access.

function Parent(this: Remix.Handle<MyType>) {
  let value = new MyType();
  this.context.set(value);
  
  return () => <Child />;
}

this.context.get(Component)

Get a context value from a parent component.

function Child(this: Remix.Handle) {
  let value = this.context.get(Parent);
  
  return () => <div>{value.data}</div>;
}

this.queueTask(callback)

Queue a task to run after the next render.

this.queueTask(() => {
  // This runs after DOM updates
  element.focus();
});

Root and Rendering

createRoot(element)

Creates a root for rendering your application.

import { createRoot } from "@remix-run/dom";

createRoot(document.body).render(<DrumMachine />);

createRangeRoot(range)

Creates a root from a DOM Range object for more precise insertion points.

import { createRangeRoot } from "@remix-run/dom";

let range = document.createRange();
createRangeRoot(range).render(<App />);

Element References

connect(callback)

Get a reference to an element when it connects to the DOM.

import { connect } from "@remix-run/dom";

function DrumControls(this: Remix.Handle) {
  let stopButton: HTMLButtonElement;
  let playButton: HTMLButtonElement;

  return () => (
    <>
      <Button
        on={[
          connect(event => (playButton = event.currentTarget)),
          press(() => {
            drummer.play();
            this.queueTask(() => {
              stopButton.focus();
            });
          }),
        ]}
      >
        PLAY
      </Button>
      <Button
        on={[
          connect(event => (stopButton = event.currentTarget)),
          press(() => {
            drummer.stop();
            this.queueTask(() => {
              playButton.focus();
            });
          }),
        ]}
      >
        STOP
      </Button>
    </>
  );
}

disconnect(callback)

Execute cleanup when an element is removed from the DOM.

import { disconnect } from "@remix-run/dom";

<div on={[disconnect(() => {
  // Cleanup code
})]}>
  Content
</div>

Props Types

Remix.Props<K>

Get properly typed props for any HTML element or component.

import type { Remix } from "@remix-run/dom";

export function Button({ children, ...rest }: Remix.Props<"button">) {
  return (
    <button {...rest}>
      {children}
    </button>
  );
}

interface TempoButtonProps extends Remix.Props<"button"> {
  orientation: "up" | "down";
}

export function TempoButton({ orientation, css, ...rest }: TempoButtonProps) {
  return (
    <button {...rest} css={{ ...css }}>
      <Triangle orientation={orientation} />
    </button>
  );
}

Remix.RemixNode

Type for Remix JSX children.

export function Layout({ children }: { children: Remix.RemixNode }) {
  return (
    <div>
      {children}
    </div>
  );
}

Special Props

on Prop

The on prop accepts an array of event descriptors and applies them to the element.

<Button
  on={[
    press(() => console.log('pressed')),
    dom.mouseover(() => console.log('hover')),
  ]}
>
  Click Me
</Button>

css Prop

Inline styles with TypeScript support.

<div
  css={{
    display: "flex",
    background: "black",
    borderRadius: "24px",
    padding: "24px",
    "&:hover": {
      background: "#333",
    }
  }}
>
  Content
</div>

Complete Example

Here's a complete example combining both packages:

import { connect, createRoot, type Remix } from "@remix-run/dom";
import { events } from "@remix-run/events";
import { press } from "@remix-run/events/press";
import { space, arrowUp, arrowDown } from "@remix-run/events/key";
import { Drummer } from "./drummer";

function DrumMachine(this: Remix.Handle<Drummer>) {
  let drummer = new Drummer(80);

  // Listen to drummer events
  events(drummer, [
    Drummer.change(() => this.render())
  ]);

  // Add keyboard shortcuts
  events(document, [
    space(() => drummer.toggle()),
    arrowUp(() => drummer.setTempo(drummer.bpm + 1)),
    arrowDown(() => drummer.setTempo(drummer.bpm - 1)),
  ]);

  this.context.set(drummer);

  return () => (
    <div>
      <h1>BPM: {drummer.bpm}</h1>
      <Controls />
    </div>
  );
}

function Controls(this: Remix.Handle) {
  let drummer = this.context.get(DrumMachine);
  let playButton: HTMLButtonElement;

  return () => (
    <>
      <button
        disabled={drummer.isPlaying}
        on={[
          connect(event => (playButton = event.currentTarget)),
          press(() => drummer.play())
        ]}
      >
        PLAY
      </button>
      <button
        disabled={!drummer.isPlaying}
        on={[press(() => drummer.stop())]}
      >
        STOP
      </button>
    </>
  );
}

createRoot(document.body).render(<DrumMachine />);

Type Definitions

Event Handler

type EventHandler<E = Event, ECurrentTarget = any, ETarget = any> = (
  event: EventWithTargets<E, ECurrentTarget, ETarget>,
  signal: AbortSignal
) => any | Promise<any>;

Event Descriptor

interface EventDescriptor<ECurrentTarget = any> {
  type: string;
  handler: EventHandler<any, ECurrentTarget>;
  isCustom?: boolean;
  options?: AddEventListenerOptions;
}

Event Container

interface EventContainer {
  on: (events: EventDescriptor | EventDescriptor[] | undefined) => void;
  cleanup: () => void;
}

Cleanup

type Cleanup = () => void;

Best Practices

  1. Use interactions over raw DOM events - Interactions like press handle both pointer and keyboard events automatically.

  2. Clean up event listeners - Always store and call the cleanup function returned by events() when appropriate, though Remix components handle this automatically.

  3. Use context for shared state - Pass data between components using this.context.set() and this.context.get().

  4. Batch renders with queueTask - Use this.queueTask() to run code after the next render, useful for focusing elements.

  5. Type your components - Use Remix.Handle<T> to type your component's context.

  6. Prefer built-in key interactions - Use pre-built key interactions like space, enter, etc. instead of raw keyboard event handlers for better accessibility.

  7. Create custom interactions for complex patterns - When you have a complex event pattern that you reuse, create a custom interaction with createInteraction.

  8. Use createEventType for custom events - When creating EventTarget subclasses, use createEventType to get type-safe event binding and dispatching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment