Bonds

A powerful pattern for building compound components with shareable state.

What Are Bonds?

Bonds are class-based state containers designed for building compound components that need to share state across multiple parts.

A Bond is a two-part architecture: the BondState manages reactive props and methods, while the Bond manages element references, generates element props, and handles context sharing. This separation keeps concerns clear and code organized.

Bonds leverage Svelte 5's Runes API ($state, $derived, $effect) and Svelte's context API for reactive state management and component communication.

The Bond pattern uses createAttachmentKey() to automatically capture element references, making it easy to manage DOM elements and implement advanced features like focus management, positioning, and animations.

Creating a Bond

Creating a bond requires four key pieces: a Bond class, a BondState class, bond state props definition, and bond HTML element types.

1. Define Bond State Props

import { type BondStateProps } from '@svelte-atoms/core';

// Define the props type for your bond state
export type MyComponentStateProps = BondStateProps & {
  open: boolean;
  disabled: boolean;
  // Add your component-specific props
};

2. Define Bond HTML Elements

// Define the HTML elements your bond will manage
export type MyComponentDomElements = {
  root: HTMLElement;
  trigger: HTMLElement;
  content: HTMLElement;
  // Add your component-specific elements
};

3. Create BondState Class

import { BondState } from '@svelte-atoms/core';

// Create the BondState class to manage reactive state
export class MyComponentState<
  Props extends MyComponentStateProps = MyComponentStateProps
> extends BondState<Props> {
  constructor(props: () => Props) {
    super(props);
  }
  
  // Derived state using $derived rune
  get isOpen() {
    return this.props.open ?? false;
  }
  
  // Methods to modify state
  open() {
    this.props.open = true;
  }
  
  close() {
    this.props.open = false;
  }
  
  toggle() {
    this.props.open = !this.props.open;
  }
}

4. Create Bond Class

import { Bond } from '@svelte-atoms/core';
import { createAttachmentKey } from 'svelte/attachments';
import { getContext, setContext } from 'svelte';

// Create the Bond class to manage elements and props
export class MyComponentBond<
  Props extends MyComponentStateProps = MyComponentStateProps,
  State extends MyComponentState<Props> = MyComponentState<Props>,
  Elements extends MyComponentDomElements = MyComponentDomElements
> extends Bond<Props, State, Elements> {
  static CONTEXT_KEY = '@your-app/bonds/my-component';
  
  constructor(state: State) {
    super(state);
  }
  
  // Generate props for root element
  root() {
    return {
      id: `component-${this.id}`,
      'data-kind': 'component-root',
      [createAttachmentKey()]: (node: HTMLElement) => {
        this.elements.root = node;
      }
    };
  }
  
  // Generate props for trigger element
  trigger() {
    const isOpen = this.state?.props?.open ?? false;
    const isDisabled = this.state?.props?.disabled ?? false;
    
    return {
      id: `component-trigger-${this.id}`,
      role: 'button',
      'aria-expanded': isOpen,
      'aria-disabled': isDisabled,
      onclick: () => this.state.toggle(),
      [createAttachmentKey()]: (node: HTMLElement) => {
        this.elements.trigger = node;
      }
    };
  }
  
  // Generate props for content element
  content() {
    const isOpen = this.state?.props?.open ?? false;
    
    return {
      id: `component-content-${this.id}`,
      'aria-hidden': !isOpen,
      [createAttachmentKey()]: (node: HTMLElement) => {
        this.elements.content = node;
      }
    };
  }
  
  // Share bond via context for compound components
  share(): this {
    return MyComponentBond.set(this) as this;
  }
  
  // Context helpers
  static override get(): MyComponentBond {
    return getContext(MyComponentBond.CONTEXT_KEY);
  }
  
  static override set(bond: MyComponentBond) {
    return setContext(MyComponentBond.CONTEXT_KEY, bond);
  }
}

5. Using the Bond in Components

<script lang="ts">
  import { MyComponentBond, MyComponentState } from './bond.svelte';
  
  let { open = $bindable(false), disabled = false } = $props();
  
  // Create bond state with reactive props
  const bondState = new MyComponentState(() => ({
    open,
    disabled
  }));
  
  // Create and share bond for compound components
  const bond = new MyComponentBond(bondState).share();
</script>

<!-- Root component spreads bond props -->
<div {...bond.root()}>
  <button {...bond.trigger()}>
    {bond.state.isOpen ? 'Close' : 'Open'}
  </button>
  
  <div {...bond.content()}>
    <p>Content goes here</p>
    <button onclick={() => bond.state.close()}>Close</button>
  </div>
</div>

Key Features

Bonds provide several advantages over traditional state management approaches.

Separation of Concerns

BondState manages reactive props and methods, while Bond handles element references, prop generation, and context sharing. Clean architecture by design.

Element Management

Automatic element reference capture via createAttachmentKey(). Access any DOM element through bond.elements for focus, positioning, and more.

Type Safety

Full TypeScript support with generic typing. Define your props, state, and elements once, get complete type inference everywhere.

Context Integration

Built-in context support with .share(), .get(), and .set(). Share state across component trees without prop drilling.

Fine-Grained Reactivity

Built on Svelte 5's Runes API. Reactive props passed as functions keep updates efficient. Only what changed re-renders.

Bond Architecture

Understanding the two-part Bond architecture and how the pieces work together.

BondState: Props and Logic

BondState manages reactive props via a function that returns the props object. This ensures fine-grained reactivity - only tracking what's accessed.

export class TabsBondState extends BondState<TabsBondProps> {
  #items = new SvelteMap<string, TabBond>();
  
  // Derived computed property
  #selectedItem = $derived(
    this.props?.value 
      ? this.#items.get(this.props.value) 
      : undefined
  );
  
  constructor(props: () => TabsBondProps) {
    super(props);  // Pass props function to base
  }
  
  get selectedItem() {
    return this.#selectedItem;
  }
  
  select(id: string) {
    this.props.value = id;  // Direct mutation
  }
}

Bond: Elements and Props

Bond manages element references and generates element props with proper ARIA attributes, IDs, and attachment keys for automatic element capture.

export class DialogBond extends Bond<
  DialogBondProps, 
  DialogBondState, 
  DialogBondElements
> {
  static CONTEXT_KEY = '@atoms/context/dialog';
  
  constructor(state: DialogBondState) {
    super(state);
  }
  
  // Generate props for root element
  root() {
    const isOpen = this.state.props.open ?? false;
    
    return {
      id: `dialog-${this.id}`,
      'aria-modal': true,
      'aria-labelledby': `dialog-title-${this.id}`,
      open: isOpen,
      [createAttachmentKey()]: (node: HTMLDialogElement) => {
        this.elements.root = node;  // Auto-capture
      }
    };
  }
}

Context Sharing

Bonds provide static methods for context management, making it easy to share state across component trees without prop drilling.

// In Bond class
static get(): TreeBond | undefined {
  return getContext(TreeBond.CONTEXT_KEY);
}

static set(bond: TreeBond): TreeBond {
  return setContext(TreeBond.CONTEXT_KEY, bond);
}

share(): this {
  return TreeBond.set(this) as this;
}

// In component
const bond = new TreeBond(state).share();

// In child component
const parentBond = TreeBond.get();

Reactive Props Pattern

Props are passed as a function to BondState, enabling fine-grained reactivity. Use defineState helper for bindable props.

import { defineProperty, defineState } from '@svelte-atoms/core';

let open = $bindable(false);

// Create reactive props with bindables
const bondProps = defineState<DialogBondProps>([
  defineProperty(
    'open',
    () => open,           // Getter
    (v) => { open = v; }  // Setter
  )
], () => ({ 
  disabled: false  // Static props
}));

// Pass as function
const state = new DialogBondState(() => bondProps);

Using Bonds in Components

Bonds are typically created in root components and shared via context to child components.

Root Component Pattern

Root components create the bond, share it via context, and spread bond-generated props onto elements.

<script lang="ts">
  import { TreeBond, TreeBondState } from './bond.svelte';
  
  let { open = $bindable(false), disabled = false } = $props();
  
  // Create reactive props
  const bondProps = defineState<TreeBondProps>([
    defineProperty('open', () => open, (v) => { open = v; })
  ], () => ({ disabled }));
  
  // Create bond
  const bondState = new TreeBondState(() => bondProps);
  const bond = new TreeBond(bondState).share();
</script>

<!-- Spread bond props onto elements -->
<div {...bond.root()}>
  <button {...bond.header()}>Toggle</button>
  <div {...bond.body()}>Content</div>
</div>

Child Component Access

Child components retrieve the bond from context and can access state, methods, and elements.

<script lang="ts">
  import { TreeBond } from './bond.svelte';
  
  // Get bond from context
  const bond = TreeBond.get();
  
  function handleClick() {
    // Access methods
    bond?.state.toggle();
    
    // Access elements
    bond?.elements.root?.focus();
  }
</script>

<button onclick={handleClick}>
  {bond?.state.props.open ? 'Close' : 'Open'}
</button>

Factory Pattern

Components accept a factory prop for custom bond creation, enabling extension and testing.

let {
  factory = _factory,
  open = $bindable(false)
} = $props();

const bondProps = defineState(...);

// Use factory (allows customization)
const bond = factory(bondProps).share();

function _factory(props) {
  const state = new TreeBondState(() => props);
  return new TreeBond(state);
}

Accessing from Parent

Parent components can access child bonds via exported getBond() methods.

<script lang="ts">
  import TreeRoot from './tree-root.svelte';
  
  let treeRef: TreeRoot;
  
  function handleClick() {
    const bond = treeRef.getBond();
    bond.state.toggle();
  }
</script>

<TreeRoot bind:this={treeRef} />
<button onclick={handleClick}>Toggle Tree</button>

When to Use Bonds

Bonds are powerful but not always necessary. Here's when to use them.

Building Compound Components

When creating components with multiple parts that need to share state across separate child components. This is the primary use case for bonds.

Shareable State Across Components

When multiple child components need to access and modify the same state in a coordinated way without prop drilling.

Reusable Logic

When you want to extract and reuse component logic across different parts of your app.

Testing

Bonds are easy to test in isolation since they're just JavaScript objects with methods.

Best Practices

Guidelines for working effectively with Bonds.

Use Props as Functions

Always pass props to BondState as a function (() => props) for fine-grained reactivity. This ensures only accessed properties trigger updates.

Type Your Elements

Define a type for your bond elements to get autocomplete and type safety when accessing bond.elements.root, etc.

Use Attachment Keys

Always use createAttachmentKey() in your element prop methods to automatically capture element references. No manual ref management needed.

Keep Context Keys Unique

Use descriptive, prefixed context keys like '@atoms/context/component-name' to avoid collisions with other context values. When extending from other existing bonds, it's recommended to keep using the same context key to maintain compatibility.

Spread Bond Props

Always spread bond-generated props onto elements: {...bond.root()}. This ensures IDs, ARIA attributes, and attachments work correctly.

Export getBond()

Export a getBond() method from root components to allow parent components to access the bond imperatively when needed.

Learn More

Ready to dive deeper into Bonds and the architecture behind them?

Philosophy

Understand the design principles and architecture behind Bonds.

Read philosophy

Browse Components

See Bonds in action with real component examples.

View components