---
title: Events | Components
contributors:
  - voluntadpear
  - saisrikardumpeti
  - RATIU5
  - manucorporat
  - nnelgxorz
  - adamdbradley
  - hamatoyogi
  - fleish80
  - cunzaizhuyi
  - Pika-Pool
  - mhevery
  - AnthonyPAlicea
  - amatiash
  - harishkrishnan24
  - fabian-hiller
  - igorbabko
  - mrhoodz
  - julianobrasil
  - maiieul
  - Balastrong
  - Jemsco
  - shairez
  - wmertens
  - varixo
updated_at: '2026-04-04T00:00:00Z'
created_at: '2023-03-20T23:45:13Z'
---

import CodeSandbox from '../../../../../components/code-sandbox/index.tsx';
import { Note } from '~/components/note/note';

# Events

For a web application to be interactive, there needs to be a way to respond to user events. This is done by registering callback functions in the JSX template. Event handlers are registered using the `on{EventName}$` attribute. For example, the `onClick$` attribute is used to listen for `click` events.

```tsx /onClick$/
<button onClick$={() => alert('CLICKED!')}>click me!</button>
```

## Inline Handler

In the following example, the `onClick$` attribute of the `<button>` element is used to let Qwik know that a callback `() => store.count++` should be executed whenever the `click` event is fired by the `<button>`.

<CodeSandbox src="/src/routes/demo/state/counter-signal/index.tsx">
```tsx {7} /onClick$/
import { component$, useSignal } from '@qwik.dev/core';

export default component$(() => {
  const count = useSignal(0);

  return (
    <button onClick$={() => count.value++}>
      Increment {count.value}
    </button>
  );
});
```
</CodeSandbox>

You can also use `bind:propertyName` to conveniently have a [two-way binding](/docs/(qwik)/core/rendering/index.mdx#bind-attribute) between a signal and an input element.

Notice that `onClick$` ends with [`$`](/docs/(qwik)/advanced/dollar/index.mdx). This is a hint to both the [Optimizer](/docs/(qwik)/advanced/optimizer/index.mdx) and the developer that a special transformation occurs at this location. The presence of the `$` suffix implies a lazy-loaded boundary here. The code associated with the `click` handler is not loaded into the JavaScript Virtual Machine (VM) until the user activates the click event. However, to avoid delays during the first interaction, it is eagerly loaded into the browser cache.

<Note>In real-world applications, the listener may refer to complex code. By creating a lazy-loaded boundary (with the `$`), Qwik can tree-shake all of the code behind the click listener and delay its loading until the user clicks the button.</Note>

## Reusing Event Handlers

To reuse the same event handler for multiple elements or events, you have to wrap the event handler into the `$()` function exported by `@qwik.dev/core`. This transforms it into a [`QRL`](/docs/advanced/qrl/).

<CodeSandbox src="/src/routes/demo/events/extracted-handler/index.tsx">
```tsx {5} /increment/#a
import { component$, useSignal, $ } from '@qwik.dev/core';

export default component$(() => {
  const count = useSignal(0);
  const increment = $(() => count.value++);
  return (
    <>
      <button onClick$={increment}>Increment</button>
      <p>Count: {count.value}</p>
    </>
  );
});
```
</CodeSandbox>


<Note>If you extract the event handler, you must manually wrap it in the `$(...handler...)`. This ensures that it is lazily attached.</Note>

## Multiple Event Handlers

To register multiple event handlers for the same event, you can pass an array of event handlers to the `on{EventName}$` attribute.

```tsx /onClick$/
import { component$, useSignal, $ } from '@qwik.dev/core';

export default component$(() => {
  const count = useSignal(0);
  const print = $((ev) => console.log('CLICKED!', ev));
  const increment = $(() => count.value++);

  // The button when clicked will print "CLICKED!" to the console, increment the count and send an event to Google Analytics.
  return (
    <button
      onClick$={[print, increment, $(() => {
        ga.send('click', { label: 'increment' });
      })]}
    >
      Count: {count.value}
    </button>
  );
});
```

## Event Object

The first argument of the event handler is the `Event` object. This object contains information about the event that triggered the handler. For example, the `Event` object for a `click` event contains information about the mouse position and the element that was clicked. You can check out the [MDN docs to know more details about each DOM event](https://developer.mozilla.org/en-US/docs/Web/API/Event).

<CodeSandbox src="/src/routes/demo/events/mouse-position/index.tsx">
```tsx /event/
import { component$, useSignal } from '@qwik.dev/core';

export default component$(() => {
  const position = useSignal<{ x: number; y: number }>();
  return (
    <div
      onClick$={(event) => (position.value = { x: event.x, y: event.y })}
      style="height: 100vh"
    >
      <p>
        Clicked at: ({position.value?.x}, {position.value?.y})
      </p>
    </div>
  );
});
```
</CodeSandbox>

## Asynchronous Events

Due to Qwik's asynchronous nature, the execution of an event handler might be delayed if the implementation has not yet been loaded into the JavaScript VM. Consequently, the following APIs on an Event object will not work:

- `event.preventDefault()`
- `event.stopPropagation()`
- `event.currentTarget`

### preventDefault & stopPropagation

Because event handling is asynchronous, you can't use `event.preventDefault()` or `event.stopPropagation()`. To solve this, Qwik introduces a declarative way to prevent default through `preventdefault:{eventName}` and `stoppropagation:{eventName}` attributes.

<CodeSandbox src="/src/routes/demo/events/preventdefault/index.tsx">
```tsx {7}
import { component$ } from '@qwik.dev/core';

export default component$(() => {
  return (
    <a
      href="/docs"
      preventdefault:click // This will prevent the default behavior of the "click" event.
      stoppropagation:click // This will stop the propagation of the "click" event.
      onClick$={() => {
        // event.PreventDefault() will not work here, because handler is dispatched asynchronously.
        alert('Do something else to simulate navigation...');
      }}
    >
      Go to docs page
    </a>
  );
});
```
</CodeSandbox>

### Passive events

Use `passive:eventname` to mark a Qwik event listener as passive when the handler does not need
`preventDefault()`.

This is especially useful for events such as `touchstart`, `touchmove`, and `scroll`, where the
browser can optimize scrolling when it knows the listener is passive.

```tsx /passive:touchmove/ /passive:touchstart/ /passive:scroll/
<div passive:touchmove onTouchMove$={() => console.log('move')} />

<button
  passive:touchstart
  document:onTouchStart$={() => console.log('document touchstart')}
/>

<button passive:scroll window:onScroll$={() => console.log('window scroll')} />
```

`passive:eventname` is only a marker. It must match a Qwik handler for the same event, and Qwik
consumes it during JSX processing.

Passive listeners do not combine with `preventdefault:eventname` for the same event. Qwik warns
about that combination with the `preventdefault-passive-check` optimizer diagnostic and drops the
`preventdefault:*` marker for that event.

<Note>
  If you need to prevent the browser default, do not use `passive:eventname` for that event. If
  you intentionally want to suppress the optimizer warning for one line, use
  [`@qwik-disable-next-line`](/docs/(qwik)/advanced/optimizer-hints/index.mdx).
</Note>

### Capture events

Use `capture:eventname` to run an element event handler during the capture phase instead of the
default bubbling phase.

This is useful when a parent element needs to observe or intercept an event before it reaches a
child's bubbling handler.

```tsx /capture:click/
<div
  capture:click
  onClick$={() => console.log('parent capture')}
>
  <button onClick$={() => console.log('button bubble')} />
</div>
```

When the button is clicked, Qwik runs:

1. the parent's `click` handler during capture
2. the button's `click` handler during bubbling

`capture:eventname` is scoped to a single event. For example, this makes `click` capture-phase
while leaving `scroll` in its normal mode:

```tsx /capture:click/
<div
  capture:click
  onClick$={() => console.log('capture click')}
  onScroll$={() => console.log('bubble scroll')}
/>
```

`capture:eventname` only applies to element-scoped handlers such as `onClick$`. It does not change
`window:on*` or `document:on*` listeners.

<Note>
  `capture:eventname` is a marker for the existing handler bucket for that event. A single element
  event cannot be both capture and bubble at the same time. For example, you can mark `click` as
  capture and leave `scroll` as bubble on the same element, but you cannot register both capture
  and bubble handlers for `click` on the same element.
</Note>

### Event Target

Because event handling is asynchronous, you can't use `event.currentTarget`. To solve this, Qwik handlers provide a `currentTarget` as a second argument.

<CodeSandbox src="/src/routes/demo/events/target/index.tsx" style={{ height: '10em' }}>
```tsx /currentTarget/
import { component$, useSignal } from '@qwik.dev/core';

export default component$(() => {
  const currentElm = useSignal<HTMLElement|null>(null);
  const targetElm = useSignal<HTMLElement|null>(null);

  return (
    <section onClick$={(event, currentTarget) => {
      currentElm.value = currentTarget;
      targetElm.value = event.target as HTMLElement;
    }}>
      Click on any text <code>target</code> and <code>currentElm</code> of the event.
      <hr/>
      <p>Hello <b>World</b>!</p>
      <hr/>
      <ul>
        <li>currentElm: {currentElm.value?.tagName}</li>
        <li>target: {targetElm.value?.tagName}</li>
      </ul>
    </section>
  );
});
```
</CodeSandbox>

<Note>`currentTarget` in the DOM points to the element that the event listener was attached to. In the example above it will always be the `<SECTION>` element.</Note>

### Synchronous Event Handling

In some cases, it is necessary to handle an event traditionally, because some APIs need to be used synchronously. For example, `dragstart` event must be processed synchronously and therefore it can't be combined with Qwik's lazy code execution.

To do this, you can leverage a `useVisibleTask` to programmatically add an event listener using the DOM API directly.

<CodeSandbox src="/src/routes/demo/events/synchronous/index.tsx" style={{ height: '10em' }}>
```tsx {7-20}
import { component$, useSignal, useVisibleTask$ } from '@qwik.dev/core';

export default component$(() => {
  const draggableRef = useSignal<HTMLElement>();
  const dragStatus = useSignal('');

  useVisibleTask$(({ cleanup }) => {
    if (draggableRef.value) {
      // Use the DOM API to add an event listener.
      const dragstart = () => (dragStatus.value = 'dragstart');
      const dragend = () => (dragStatus.value = 'dragend');

      draggableRef.value!.addEventListener('dragstart', dragstart);
      draggableRef.value!.addEventListener('dragend', dragend);
      cleanup(() => {
        draggableRef.value!.removeEventListener('dragstart', dragstart);
        draggableRef.value!.removeEventListener('dragend', dragend);
      });
    }
  });

  return (
    <div>
      <div draggable ref={draggableRef}>
        Drag me!
      </div>
      <p>{dragStatus.value}</p>
    </div>
  );
});
```
</CodeSandbox>

<Note>Using `VisibleTask` to listen for events is an anti-pattern in Qwik because it causes eager execution of code in the browser defeating [resumability](/docs/(qwik)/concepts/resumable/index.mdx). Only use it when you have no other choice. You should use JSX to listen for events, for example: `<div onClick$={...}>`. Alternatively, if you need to listen to events programmatically, consider using the `useOn(...)` event methods.</Note>

## Custom Event Props

When creating your components, it is often useful to pass custom event props that resemble event handlers, even though they are just callbacks and not actual DOM events. Component boundaries in Qwik must be serializable for the optimizer to split them up into separate chunks. Functions are not serializable unless they are converted to a QRL.

For example, listening for triple click events, which html cannot do by default, would require creating an `onTripleClick$` custom event prop.
<CodeSandbox src="/src/routes/demo/events/custom-event/index.tsx" style={{ height: '10em' }}>
```tsx
import { component$, Slot, useStore } from '@qwik.dev/core';

export default component$(() => {
  return (
    <Button onTripleClick$={() => alert('TRIPLE CLICKED!')}>
      Triple Click me!
    </Button>
  );
});

type ButtonProps = {
  onTripleClick$: QRL<() => void>;
};

export const Button = component$<ButtonProps>(({ onTripleClick$ }) => {
  const state = useStore({
    clicks: 0,
    lastClickTime: 0,
  });
  return (
    <button
      onClick$={() => {
        // triple click logic
        const now = Date.now();
        const timeBetweenClicks = now - state.lastClickTime;
        state.lastClickTime = now;
        if (timeBetweenClicks > 500) {
          state.clicks = 0;
        }
        state.clicks++;
        if (state.clicks === 3) {
          // handle custom event
          onTripleClick$();
          state.clicks = 0;
        }
      }}
    >
      <Slot />
    </button>
  );
});
```
</CodeSandbox>

<Note>Notice the use of the `QRL` type in `onTripleClick$: QRL<() => void>;`. It is like wrapping a function in `$()` but at the type level. If you had `const greet = $(() => "hi");` and hovered over 'greet', you would see that 'greet' is of type `QRL<() => "hi">`</Note>

Event names are case sensitive, but all DOM events except for `DOMContentLoaded` are lowercase. For a better DX, event names are always lowercased, so `onTripleClick$` becomes `tripleclick` under the hood.

To listen for a custom event with uppercase letters, you add a `-` after `on`. For example, to listen for a custom event named `CustomEvent`, you would use `on-CustomEvent$`. For a window event named `Hi-There`, you would use `window:on-Hi-There$`.

## Window and Document Events

So far, the discussion has focused on listening to events originating from elements. There are events such as `scroll` and `mousemove` that need to be listened to on the `window` or `document`. Qwik allows this by providing the `document:on` and `window:on` prefixes when listening for events.

The `window:on`/`document:` prefixes are used to register an event at the current DOM location of the component while allowing it to receive events from the `window`/`document`. There are two advantages to this:

1. The events can be registered declaratively in your JSX.
2. The events get automatically cleaned up when the component is destroyed (No explicit bookkeeping and cleanup is needed).

## `useOn[window|document]` Hook

- `useOn()`: listen to events on the current component's root element.
- `useOnWindow()`: listen to events on the `window` object.
- `useOnDocument()`: listen to events on the `document` object.

`useOn[window|document]()` hook will add a DOM-based event listener at the component level programmatically. This is often useful when you want to create your own use hooks or if you don't know the event name at the time of compilation.

Use `useOn()` when you need the ergonomics of JSX event handlers, but the listener must be registered from a reusable hook or helper instead of directly in markup. The event still stays lazy-loadable because the callback is wrapped in `$()`.

As a rule of thumb:

- Prefer JSX such as `<button onClick$={...}>` when the listener belongs directly to that element in the component's template.
- Use `useOn()` when you are building a custom `use*()` hook and the hook should attach to the current component's host element.
- Use `useOnDocument()` or `useOnWindow()` when the event source is global rather than the component's own host element.

The "host element" for `useOn()` is the top-level DOM element rendered by the current component. That means `useOn('click', ...)` behaves like attaching a listener to that root node, not to every child in the component.

All three hooks also accept an optional third argument for event modifiers:

- `{ passive: true }` registers a passive listener for events such as `touchmove`, `wheel`, or `scroll` when the handler does not need to call `preventDefault()`.
- `{ capture: true }` runs the listener during the capture phase instead of the bubbling phase.
- `{ preventdefault: true }` applies the same behavior as JSX `preventdefault:event`, so Qwik calls `event.preventDefault()` before the handler runs.
- `{ stoppropagation: true }` applies the same behavior as JSX `stoppropagation:event`, so Qwik stops propagation before the handler runs.

Avoid combining `passive: true` with `preventdefault: true`, because passive listeners cannot cancel the browser's default behavior.

<CodeSandbox src="/src/routes/demo/events/use-on/index.tsx" style={{ height: '10em' }}>
```tsx
import { $, component$, useOnDocument, useStore } from '@qwik.dev/core';

// Assume reusable use method that does not have access to JSX
// but needs to register event handlers.
function useMousePosition() {
  const position = useStore({ x: 0, y: 0 });
  useOnDocument(
    'mousemove',
    $((event) => {
      const { x, y } = event as MouseEvent;
      position.x = x;
      position.y = y;
    })
  );
  return position;
}

export default component$(() => {
  const pos = useMousePosition();
  return (
    <div>
      MousePosition: ({pos.x}, {pos.y})
    </div>
  );
});
```
</CodeSandbox>

In the example above, `useMousePosition()` is a reusable hook. It cannot write `onMouseMove$` into JSX because it does not render any DOM itself, so it registers the listener programmatically with `useOnDocument()`. This is the main use case for the `useOn*()` family.

```tsx
import { $, component$, useOnWindow, useSignal } from '@qwik.dev/core';

export default component$(() => {
  const scrollCount = useSignal(0);

  useOnWindow(
    'scroll',
    $(() => {
      scrollCount.value++;
    }),
    { passive: true }
  );

  return <p>Scroll events: {scrollCount.value}</p>;
});
```

```tsx
import { $, component$, useOn, useSignal } from '@qwik.dev/core';

export default component$(() => {
  const logs = useSignal<string[]>([]);

  useOn(
    'click',
    $(() => {
      logs.value = [...logs.value, 'captured on host'];
    }),
    { capture: true, stoppropagation: true }
  );

  return (
    <div>
      <button>Click me</button>
      <pre>{logs.value.join('\n')}</pre>
    </div>
  );
});
```
