Skip to main content

Overview

Attachments are utilities that attach behavior to DOM elements using Svelte 5’s attachment system. They provide clean, reusable patterns for common DOM interactions like click-outside detection, event handling, portal rendering, and resize observation.

Installation

npm install @svelte-atoms/core

Import

import { clickout, on, resizeObserver } from '@svelte-atoms/core/attachments';

API Reference

clickout

Detects clicks outside of an element. Useful for closing dropdowns, modals, and popovers.

Signature

function clickout<T extends Element>(
  onclick?: (ev: PointerEvent, node?: T) => void,
  options?: AddEventListenerOptions
): (node: T) => () => void

Parameters

onclick
Function
Callback function invoked when a click occurs outside the element
ev
PointerEvent
The pointer event
node
T extends Element
The attached element
options
AddEventListenerOptions
Standard event listener options (capture, passive, once, signal)

Returns

Returns an attachment function that can be used with Svelte’s {@attach} directive.

Example

<script>
  import { clickout } from '@svelte-atoms/core/attachments';

  let isOpen = $state(false);

  function handleClickOut(ev, node) {
    console.log('Clicked outside', node);
    isOpen = false;
  }
</script>

{#if isOpen}
  <div {@attach clickout(handleClickOut)}>
    <p>Click outside to close</p>
  </div>
{/if}

Use Cases

  • Closing dropdowns when clicking outside
  • Dismissing modals on backdrop click
  • Hiding tooltips on outside interaction
  • Context menu management

on

Generic event handler attachment utility. Provides a clean way to attach event listeners to elements.

Signature

function on<E extends Event = Event, T extends EventTarget = Element>(
  type: keyof HTMLElementEventMap,
  handler: (ev: E & { currentTarget: EventTarget & T }) => void,
  options?: boolean | AddEventListenerOptions
): (node: T) => () => void

Parameters

type
keyof HTMLElementEventMap
The event type (e.g., ‘click’, ‘mouseover’, ‘keydown’)
handler
Function
Event handler function with properly typed event and currentTarget
ev
E & { currentTarget: EventTarget & T }
Event object with typed currentTarget
options
boolean | AddEventListenerOptions
Event listener options or useCapture boolean

Returns

Returns an attachment function for use with {@attach}.

Example

<script>
  import { on } from '@svelte-atoms/core/attachments';

  function handleMouseEnter(ev) {
    console.log('Mouse entered', ev.currentTarget);
  }

  const mouseEnterAttachment = on('mouseenter', handleMouseEnter);
</script>

<div {@attach mouseEnterAttachment}>
  Hover over me
</div>

Multiple Events

<script>
  import { on } from '@svelte-atoms/core/attachments';

  const clickHandler = on('click', (ev) => console.log('Clicked'));
  const mouseHandler = on('mouseenter', (ev) => console.log('Mouse enter'));
</script>

<div {@attach clickHandler} {@attach mouseHandler}>
  Interactive element
</div>

resizeObserver

Observes size changes of an element using ResizeObserver API.

Signature

function resizeObserver(
  callback: ResizeObserverCallback,
  options?: ResizeObserverOptions
): (element: Element) => () => void

Parameters

callback
ResizeObserverCallback
Callback invoked when element resizes
type ResizeObserverCallback = (
  entries: ResizeObserverEntry[],
  observer: ResizeObserver
) => void
options
ResizeObserverOptions
ResizeObserver options
box
'content-box' | 'border-box' | 'device-pixel-content-box'
Which box model to observe

Returns

Returns an attachment function for observing element size changes.

Example

<script>
  import { resizeObserver } from '@svelte-atoms/core/attachments';

  let dimensions = $state({ width: 0, height: 0 });

  function handleResize(entries) {
    for (const entry of entries) {
      const { width, height } = entry.contentRect;
      dimensions = { width, height };
    }
  }

  const observe = resizeObserver(handleResize);
</script>

<div {@attach observe}>
  <p>Width: {dimensions.width}px</p>
  <p>Height: {dimensions.height}px</p>
</div>

Advanced Usage

<script>
  import { resizeObserver } from '@svelte-atoms/core/attachments';

  let size = $state({ width: 0, height: 0 });

  const observe = resizeObserver(
    (entries) => {
      const entry = entries[0];
      
      // Access different box sizes
      console.log('Content box:', entry.contentBoxSize);
      console.log('Border box:', entry.borderBoxSize);
      console.log('Device pixel:', entry.devicePixelContentBoxSize);

      size = {
        width: entry.contentRect.width,
        height: entry.contentRect.height
      };
    },
    { box: 'border-box' }
  );
</script>

<textarea {@attach observe}>
  Resize me
</textarea>

Advanced Patterns

Combining Attachments

<script>
  import { clickout, on, resizeObserver } from '@svelte-atoms/core/attachments';

  let isOpen = $state(false);
  let size = $state({ width: 0, height: 0 });

  const handleClickOut = clickout(() => isOpen = false);
  const handleClick = on('click', () => console.log('Clicked'));
  const handleResize = resizeObserver((entries) => {
    size = {
      width: entries[0].contentRect.width,
      height: entries[0].contentRect.height
    };
  });
</script>

<div 
  {@attach handleClickOut}
  {@attach handleClick}
  {@attach handleResize}>
  Multi-attachment element
</div>

Creating Custom Attachments

Follow this pattern to create your own attachments:
export function customAttachment(options) {
  return (node: HTMLElement) => {
    // Setup logic
    const cleanup = setupLogic(node, options);

    // Return cleanup function
    return () => {
      cleanup();
    };
  };
}
Example:
import { on } from 'svelte/events';

export function longpress(duration = 500) {
  return (node: HTMLElement) => {
    let timer: ReturnType<typeof setTimeout>;

    const handleMouseDown = () => {
      timer = setTimeout(() => {
        node.dispatchEvent(new CustomEvent('longpress'));
      }, duration);
    };

    const handleMouseUp = () => clearTimeout(timer);

    const cleanup1 = on(node, 'mousedown', handleMouseDown);
    const cleanup2 = on(node, 'mouseup', handleMouseUp);

    return () => {
      cleanup1();
      cleanup2();
      clearTimeout(timer);
    };
  };
}

Type Safety

All attachments are fully typed with TypeScript:
// Type-safe event handler
const handleClick = on<MouseEvent, HTMLButtonElement>(
  'click',
  (ev) => {
    // ev.currentTarget is typed as HTMLButtonElement
    ev.currentTarget.disabled = true;
  }
);

// Type-safe clickout
const handleClickOut = clickout<HTMLDivElement>(
  (ev, node) => {
    // node is typed as HTMLDivElement
    console.log(node.className);
  }
);

Browser Compatibility

  • clickout: All modern browsers
  • on: All browsers
  • portal: All browsers
  • resizeObserver: ResizeObserver API required (polyfill available for older browsers)

Performance

  • Event listeners are automatically cleaned up when elements unmount
  • ResizeObserver efficiently batches updates
  • Portal operations are optimized to minimize reflows

Build docs developers (and LLMs) love