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.
When Not to Use Bonds
$state variable is often clearer. Don't over-engineer.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?