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.
A declarative event handling library that provides a clean, composable API for managing event listeners across any EventTarget
.
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);
}),
]);
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();
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>
Pre-configured target proxies that provide type-safe access to native DOM events.
import { events, dom } from "@remix-run/events";
events(element, [
dom.click(event => { /* ... */ }),
dom.mouseover(event => { /* ... */ }),
dom.pointermove(event => { /* ... */ }),
// All HTMLElementEventMap events available
]);
import { events, win } from "@remix-run/events";
events(window, [
win.resize(event => { /* ... */ }),
win.scroll(event => { /* ... */ }),
]);
import { events, doc } from "@remix-run/events";
events(document, [
doc.DOMContentLoaded(event => { /* ... */ }),
doc.visibilitychange(event => { /* ... */ }),
]);
import { events, xhr } from "@remix-run/events";
let request = new XMLHttpRequest();
events(request, [
xhr.load(event => { /* ... */ }),
xhr.error(event => { /* ... */ }),
]);
import { events, ws } from "@remix-run/events";
let socket = new WebSocket('ws://localhost:8080');
events(socket, [
ws.message(event => { /* ... */ }),
ws.error(event => { /* ... */ }),
]);
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 are higher-level event patterns that combine multiple low-level events into a single semantic event.
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
High-level pointer and keyboard interactions for button-like elements.
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';
}
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
Fires when the press ends (pointer up or key up).
<Button on={[pressUp(event => { /* ... */ })]}>
Press Me
</Button>
- Removes
rmx-active
attribute from the target
Fires after holding the press for a specified duration.
<Button on={[longPress(event => { /* ... */ }, { delay: 500 })]}>
Hold Me
</Button>
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;
}
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 keyshome
/end
- Move to first/last itempageUp
/pageDown
- Page navigationtab
- Tab keybackspace
/del
- Deletion keys
Event Detail:
type KeyInteractionEvent = CustomEvent<{
originalEvent: KeyboardEvent;
}>
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
}
})
]);
A reactive component system that provides efficient DOM rendering and updates.
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
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>;
}
Trigger a re-render of the component.
events(drummer, [
Drummer.change(() => this.render())
]);
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 />;
}
Get a context value from a parent component.
function Child(this: Remix.Handle) {
let value = this.context.get(Parent);
return () => <div>{value.data}</div>;
}
Queue a task to run after the next render.
this.queueTask(() => {
// This runs after DOM updates
element.focus();
});
Creates a root for rendering your application.
import { createRoot } from "@remix-run/dom";
createRoot(document.body).render(<DrumMachine />);
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 />);
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>
</>
);
}
Execute cleanup when an element is removed from the DOM.
import { disconnect } from "@remix-run/dom";
<div on={[disconnect(() => {
// Cleanup code
})]}>
Content
</div>
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>
);
}
Type for Remix JSX children.
export function Layout({ children }: { children: Remix.RemixNode }) {
return (
<div>
{children}
</div>
);
}
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>
Inline styles with TypeScript support.
<div
css={{
display: "flex",
background: "black",
borderRadius: "24px",
padding: "24px",
"&:hover": {
background: "#333",
}
}}
>
Content
</div>
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 EventHandler<E = Event, ECurrentTarget = any, ETarget = any> = (
event: EventWithTargets<E, ECurrentTarget, ETarget>,
signal: AbortSignal
) => any | Promise<any>;
interface EventDescriptor<ECurrentTarget = any> {
type: string;
handler: EventHandler<any, ECurrentTarget>;
isCustom?: boolean;
options?: AddEventListenerOptions;
}
interface EventContainer {
on: (events: EventDescriptor | EventDescriptor[] | undefined) => void;
cleanup: () => void;
}
type Cleanup = () => void;
-
Use interactions over raw DOM events - Interactions like
press
handle both pointer and keyboard events automatically. -
Clean up event listeners - Always store and call the cleanup function returned by
events()
when appropriate, though Remix components handle this automatically. -
Use context for shared state - Pass data between components using
this.context.set()
andthis.context.get()
. -
Batch renders with queueTask - Use
this.queueTask()
to run code after the next render, useful for focusing elements. -
Type your components - Use
Remix.Handle<T>
to type your component's context. -
Prefer built-in key interactions - Use pre-built key interactions like
space
,enter
, etc. instead of raw keyboard event handlers for better accessibility. -
Create custom interactions for complex patterns - When you have a complex event pattern that you reuse, create a custom interaction with
createInteraction
. -
Use createEventType for custom events - When creating
EventTarget
subclasses, usecreateEventType
to get type-safe event binding and dispatching.