Skip to content

API Reference

Bootstraps a Sygnal application.

function run(
component: RootComponent,
drivers?: Record<string, CycleDriver>,
options?: RunOptions
): SygnalApp
ParameterTypeDescription
componentRootComponentThe root component function (with optional .intent, .model, etc.)
driversRecord<string, CycleDriver>Additional drivers beyond the defaults (optional)
optionsRunOptionsConfiguration options (optional)
OptionTypeDefaultDescription
mountPointstring'#root'CSS selector for the DOM element to render into
fragmentsbooleantrueEnable JSX fragment support in the DOM driver
useDefaultDriversbooleantrueInclude default drivers (DOM, STATE, EVENTS, LOG)
PropertyTypeDescription
sourcesobjectAll driver source objects
sinksobjectAll driver sink streams
dispose() => voidShuts down the application and cleans up listeners
hmr(newComponent?, state?) => voidHot-swap the root component, preserving state
import { run } from 'sygnal'
import RootComponent from './RootComponent.jsx'
// Basic usage
run(RootComponent)
// With custom mount point
run(RootComponent, {}, { mountPoint: '#app' })
// With custom drivers
import myDriver from './myDriver'
run(RootComponent, { MY_DRIVER: myDriver })
// With HMR (Vite)
const { hmr, dispose } = run(RootComponent)
if (import.meta.hot) {
import.meta.hot.accept('./RootComponent.jsx', hmr)
import.meta.hot.dispose(dispose)
}

Auto-configures JSX transform and HMR. Import from sygnal/vite.

import sygnal from 'sygnal/vite'
export default defineConfig({
plugins: [sygnal()],
})
OptionTypeDefaultDescription
disableJsxbooleanfalseSkip automatic JSX configuration
disableHmrbooleanfalseSkip automatic HMR injection

The HMR transform runs only in dev mode (vite / vite dev). Files that already contain import.meta.hot are left untouched.

See Bundler Configuration for details.


Lower-level factory for creating Sygnal components with explicit options. Most users won’t need this — function augmentation (attaching .model, .intent, etc. directly to the view function) is the standard approach. Use component() when you need advanced isolation control or are building components programmatically.

function component(options: ComponentFactoryOptions): Component
OptionTypeDefaultDescription
namestringComponent identifier (used in debug logs)
viewFunctionThe component’s view function
modelobjectAction-to-reducer mapping
intentFunctionMaps sources to action streams
initialStateobjectStarting state
calculatedobjectDerived state field definitions. Values are either (state) => value or [[...deps], (state) => value] for dependency-tracked memoization. Deps can reference base state keys or other calculated field names. Circular dependencies throw at creation time.
storeCalculatedInStatebooleantrueWhether to store calculated fields in state
contextobjectContext values for descendants
peersobjectPeer component definitions
componentsobjectNamed child component definitions
hmrActionsstring | string[]Actions to trigger on HMR
DOMSourceNamestring'DOM'Custom DOM driver name
stateSourceNamestring'STATE'Custom state driver name
debugbooleanfalseEnable debug logging
import { component } from 'sygnal'
const MyComponent = component({
name: 'MyComponent',
view: ({ state }) => <div>{state.count}</div>,
initialState: { count: 0 },
intent: ({ DOM }) => ({
INCREMENT: DOM.select('.btn').events('click')
}),
model: {
INCREMENT: (state) => ({ count: state.count + 1 })
}
})

Renders a list of components from an array on state.

<collection of={ItemComponent} from="items" filter={fn} sort="name" />
import { Collection } from 'sygnal'
<Collection of={ItemComponent} from="items" className="list" />
PropTypeRequiredDescription
ofComponentYesThe component to instantiate for each item
fromstring | LensYesState property name or lens for the source array
filter(item) => booleanNoFilter function — only items returning true are rendered
sortstring | object | array | functionNoSort items — string (field name, "asc", or "desc"), object ({ field: "asc" | "desc" | 1 | -1 }), array (multi-field), or comparator function
classNamestringNoCSS class for the wrapping container element
import { collection } from 'sygnal'
const MyList = collection(ItemComponent, 'items', {
container: 'ul', // HTML element for the container (default: 'div')
containerClass: 'my-list', // CSS class for the container
combineList: ['DOM'], // Sinks to combine (default: ['DOM'])
globalList: ['EVENTS'], // Sinks to merge globally (default: ['EVENTS'])
stateSourceName: 'STATE', // State driver name (default: 'STATE')
domSourceName: 'DOM' // DOM driver name (default: 'DOM')
})

Items are keyed by their id property if present, otherwise by array index:

itemKey: (state, index) => state.id !== undefined ? state.id : index

An item removes itself from the collection by returning undefined from a state reducer:

Item.model = {
REMOVE: () => undefined
}

Conditionally renders one component from a set based on a name.

<switchable
of={{ tab1: Component1, tab2: Component2 }}
current={state.activeTab}
/>
import { Switchable } from 'sygnal'
<Switchable of={{ tab1: Component1, tab2: Component2 }} current={state.activeTab} />
PropTypeRequiredDescription
ofRecord<string, Component>YesMaps names to components
currentstringYesName of the currently visible component
statestring | LensNoState slice for the switched components
import { switchable } from 'sygnal'
// With a state property name
const MySwitchable = switchable(
{ tab1: Component1, tab2: Component2 },
'activeTab', // State property to watch
'tab1', // Initial/default value
{ switched: ['DOM'], stateSourceName: 'STATE' }
)
// With a mapping function
const MySwitchable = switchable(
{ tab1: Component1, tab2: Component2 },
state => state.tabs.current, // Function to extract current name from state
'tab1'
)
// With a stream
const MySwitchable = switchable(
{ tab1: Component1, tab2: Component2 },
name$, // Observable stream of component names
'tab1'
)
OptionTypeDefaultDescription
switchedstring[]['DOM']Which sinks switch with the active component
stateSourceNamestring'STATE'State driver name
  • Switched sinks (default: DOM) — Only the active component’s output is used
  • Non-switched sinks — Merged from all components (they all remain active)

Renders children into a different DOM container.

import { Portal } from 'sygnal'
<Portal target="#modal-root">
<div className="modal">Content</div>
</Portal>
PropTypeDescription
targetstringRequired. CSS selector for the destination container
childrenVNode[]Content to render in the target

Portal content is outside the component’s DOM event delegation scope. Use DOM.select('document').events('click').filter(...) to capture events on portal elements.


CSS-based enter/leave animations using a Vue-style name prop that generates six CSS classes.

import { Transition } from 'sygnal'
<Transition name="fade">
{state.visible && <div>Animated</div>}
</Transition>
PropTypeDefaultDescription
namestring'v'Base name for generated CSS classes ({name}-enter-from, {name}-enter-active, {name}-enter-to, {name}-leave-from, {name}-leave-active, {name}-leave-to)
durationnumberExplicit timeout in ms. If omitted, listens for transitionend event

See Transitions guide for the full class lifecycle and CSS examples.


Shows fallback UI while children are not ready.

import { Suspense } from 'sygnal'
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
PropTypeDescription
fallbackVNode | stringUI to show while children are pending
childrenVNode[]Children that may signal not-ready via the READY sink

Components control Suspense via the built-in READY sink:

// Component starts as not-ready, signals ready when data loads
MyComponent.model = {
DATA_LOADED: {
STATE: (state, data) => ({ ...state, data }),
READY: () => true,
},
}

Components without explicit READY model entries auto-emit true on instantiation.


Marks named content regions for child components to render in specific locations.

import { Slot } from 'sygnal'
<Card state="card">
<Slot name="header"><h2>Title</h2></Slot>
<Slot name="actions"><button>Save</button></Slot>
<p>Default content</p>
</Card>
PropTypeDescription
namestringSlot name. If omitted, content goes to the default slot
childrenVNode[]Content for this slot

The child component receives a slots object in its view parameters:

function Card({ state, slots }) {
return (
<div>
<header>{...(slots.header || [])}</header>
<main>{...(slots.default || [])}</main>
<footer>{...(slots.actions || [])}</footer>
</div>
)
}

Unnamed children (not wrapped in <Slot>) go to slots.default. The children parameter continues to work as before — it contains the same elements as slots.default.

See Slots guide for reactive updates and fallback patterns.


Code-split a component via dynamic import.

function lazy(loadFn: () => Promise<{ default: Component }>): Component
import { lazy } from 'sygnal'
const HeavyChart = lazy(() => import('./HeavyChart.jsx'))

Renders a <div data-sygnal-lazy="loading"> placeholder until the import resolves. Static properties (intent, model, etc.) are copied from the loaded module’s default export.


Creates a ref object for DOM element access.

function createRef<T extends Element = Element>(): { current: T | null }
import { createRef } from 'sygnal'
const myRef = createRef()
<div ref={myRef}>...</div>
// In model:
myRef.current?.offsetWidth // Access the DOM element

The ref prop sets .current to the DOM element on mount and null on unmount.


Creates a stream-based ref that emits the DOM element.

function createRef$<T extends Element = Element>(): Stream<T | null>
import { createRef$ } from 'sygnal'
const el$ = createRef$()
<div ref={el$}>...</div>
// In intent:
MyComponent.intent = () => ({
ELEMENT: el$,
})

Creates an imperative command channel for parent-to-child communication.

function createCommand(): Command
PropertyTypeDescription
send(type: string, data?: any) => voidSend a named command with optional data

When a Command object is passed as any prop to a child component, the child receives a commands$ source in intent:

MethodTypeDescription
select(type: string) => Stream<any>Returns a stream that emits the data from each matching command
import { createCommand, ABORT } from 'sygnal'
const cmd = createCommand()
// Parent passes as prop and sends commands
<VideoPlayer commands={cmd} />
cmd.send('play')
cmd.send('seek', { time: 30 })
// Child reads via commands$ source in intent
VideoPlayer.intent = ({ commands$ }) => ({
PLAY: commands$.select('play'),
SEEK: commands$.select('seek'), // emits { time: 30 }
})

See Commands guide for usage patterns.


A built-in sink for side-effect-only model entries. Runs the reducer function but produces no state change and emits nothing to any driver.

Component.model = {
ACTION_NAME: {
EFFECT: (state, data, next, props) => void
}
}
ParameterTypeDescription
stateSTATECurrent component state (with calculated fields)
dataanyData from the triggering action
next(action, data?, delay?) => voidDispatch a follow-up action
propsobjectCurrent props, children, slots, context
// Send a command without changing state
App.model = {
PLAY: {
EFFECT: () => playerCmd.send('play'),
},
}
// Route to different actions based on state
App.model = {
ROUTE: {
EFFECT: (state, data, next) => {
if (state.mode === 'a') next('DO_A', data)
else next('DO_B', data)
},
},
}
// Combine with other sinks
App.model = {
SUBMIT: {
STATE: (state) => ({ ...state, submitting: true }),
EFFECT: () => formCmd.send('validate'),
},
}

Returns a console.warn if the reducer returns a value — EFFECT handlers should not return anything.

See Effect Handlers guide for more patterns.


Compact syntax for model entries that target a single sink. Use 'ACTION | SINK' as the key:

Component.model = {
'ACTION | SINK': reducer
}
// Equivalent to:
Component.model = {
ACTION: { SINK: reducer }
}
App.model = {
'PLAY | EFFECT': () => playerCmd.send('play'),
'ALERT | EVENTS': (state) => ({ type: 'notify', data: state.msg }),
'DELETE | PARENT': (state) => ({ type: 'DELETE', id: state.id }),
'FETCH | HTTP': (state) => ({ url: `/api/${state.id}` }),
}

The | separator requires the key to be a quoted string. Whitespace around | is optional. Intent action names containing | throw an error.

See Model Shorthand guide for more details.


Render a Sygnal component in isolation for testing. Creates a minimal Cycle.js runtime with mocked DOM, event bus, and state drivers.

function renderComponent(
component: ComponentFunction,
options?: RenderOptions
): RenderResult
OptionTypeDefaultDescription
initialStateanyComponent’s .initialStateOverride the component’s initial state
mockConfigobject{}Mock DOM events — maps selectors to event streams
driversobject{}Additional drivers beyond the defaults
PropertyTypeDescription
state$Stream<any>Live stream of state values
dom$Stream<any>Live stream of rendered VNode trees
events$EventsSourceEvent bus source (.select(type))
sinksobjectAll driver sink streams
sourcesobjectAll driver source objects
statesany[]Collected state values
simulateAction(name: string, data?: any) => voidPush an action into the model
waitForState(predicate, timeout?) => Promise<any>Resolve when state matches
dispose() => voidTear down the component
import { renderComponent } from 'sygnal'
// Basic usage
const t = renderComponent(Counter, { initialState: { count: 0 } })
t.simulateAction('INCREMENT')
await t.waitForState(s => s.count === 1)
t.dispose()
// With mock DOM events
import xs from 'xstream'
const t = renderComponent(Counter, {
mockConfig: { '.inc': { click: xs.of({}) } },
})

See Testing guide for full usage patterns.


Render a Sygnal component to an HTML string for server-side rendering. Recursively renders sub-components, Collections, and special components.

function renderToString(
component: ComponentFunction,
options?: RenderToStringOptions
): string
OptionTypeDefaultDescription
stateanyComponent’s .initialStateState for the root component
propsRecord<string, any>{}Props to pass to the component
contextRecord<string, any>{}Parent context to merge with
hydrateStateboolean | stringEmbed state in <script> tag for client hydration
import { renderToString } from 'sygnal'
// Basic usage
const html = renderToString(App, { state: { count: 0 } })
// With hydration state
const html = renderToString(App, {
state: { count: 5 },
hydrateState: true,
})
// Appends: <script>window.__SYGNAL_STATE__={"count":5}</script>

See Server-Side Rendering guide for full usage patterns.


A built-in model action that fires automatically when the component is about to unmount. This is the preferred way to handle component cleanup.

MyComponent.model = {
DISPOSE: {
EFFECT: (state) => {
clearInterval(state.intervalId)
},
},
}

Works with all sinks (EFFECT, EVENTS, PARENT, STATE) and supports model shorthand:

MyComponent.model = {
'DISPOSE | EFFECT': (state) => clearInterval(state.intervalId),
}

The reducer receives the current state, so you can access component data during cleanup.


A source stream available in every component’s intent. Emits true once when the component unmounts. Use this for advanced cases that need stream composition. For most cleanup tasks, the DISPOSE model action is simpler.

MyComponent.intent = ({ DOM, dispose$ }) => ({
CLEANUP: dispose$,
})
MyComponent.model = {
CLEANUP: {
WEBSOCKET: () => ({ type: 'close' }),
},
}

Not imported — automatically available as a source in intent.


Error boundary handler for a component.

Component.onError = (error: Error, info: { componentName: string }) => VNode | undefined
MyComponent.onError = (error, { componentName }) => (
<div>Error in {componentName}: {error.message}</div>
)

If not defined, errors render an empty <div data-sygnal-error> and log to console.error.


Required when a sub-component declares .initialState. Prevents accidental parent state overwrite.

Widget.initialState = { count: 0 }
Widget.isolatedState = true // Required — without this, Sygnal throws an error

When isolatedState = true and the parent state doesn’t have the child’s state slice, the child’s initialState seeds it automatically.


Creates a Cycle.js driver for HTML5 drag-and-drop that works across isolated components.

function makeDragDriver(): (sink$: Stream<DragDriverRegistration | DragDriverRegistration[]>) => DragDriverSource
import { run, makeDragDriver } from 'sygnal'
import RootComponent from './RootComponent.jsx'
run(RootComponent, { DND: makeDragDriver() })

Configuration objects emitted via the model sink to register drag categories:

type DragDriverRegistration = {
category: string // Required: name for this group of drag elements
draggable?: string // CSS selector for draggable elements
dropZone?: string // CSS selector for drop zones
accepts?: string // Only accept drops from this dragging category
dragImage?: string // CSS selector for custom drag preview (resolved via .closest())
}

Register categories from BOOTSTRAP in the model. Wrap in { configs: [...] } because model sinks cannot return bare arrays:

RootComponent.model = {
BOOTSTRAP: {
DND: () => ({
configs: [
{ category: 'task', draggable: '.task-card' },
{ category: 'lane', dropZone: '.lane-drop-zone', accepts: 'task' },
],
}),
},
}

The source object returned by the driver, available in intent as DND:

type DragDriverSource = {
select(category: string): DragDriverCategory
dragstart(category: string): Stream<DragStartPayload>
dragend(category: string): Stream<null>
drop(category: string): Stream<DropPayload>
dragover(category: string): Stream<any>
dispose(): void
}

The shorthand methods (dragstart, dragend, drop, dragover) are equivalent to select(category).events(eventName).

Returned by DND.select(category):

type DragDriverCategory = {
events(eventType: 'dragstart'): Stream<DragStartPayload>
events(eventType: 'dragend'): Stream<null>
events(eventType: 'drop'): Stream<DropPayload>
events(eventType: string): Stream<any>
}
type DragStartPayload = {
element: HTMLElement // The dragged element
dataset: Record<string, string> // The element's data-* attributes
}
type DropPayload = {
dropZone: HTMLElement // The drop zone element
insertBefore: HTMLElement | null // Sibling element at the cursor (for ordering)
}
RootComponent.intent = ({ DND }) => ({
DRAG_START: DND.dragstart('task'),
DROP: DND.drop('lane'),
DRAG_END: DND.dragend('task'),
})
RootComponent.model = {
DRAG_START: (state, { dataset }) => ({
...state,
dragging: { taskId: dataset.taskId },
}),
DROP: (state, { dropZone, insertBefore }) => {
const toLaneId = dropZone.dataset.laneId
// ... move the dragged task
},
DRAG_END: (state) => ({ ...state, dragging: null }),
}

Extracts form field values from a form DOM source.

function processForm(
target: FormSource,
options?: { events?: string | string[]; preventDefault?: boolean }
): Stream<FormData>
ParameterTypeDescription
targetFormSourceA DOM source for a form element (from DOM.select('.my-form'))
options.eventsstring | string[]Events to listen for (default: ['input', 'submit'])
options.preventDefaultbooleanCall preventDefault() on events (default: true)

A stream that emits objects containing:

PropertyTypeDescription
[fieldName]anyEach form field’s value, keyed by its name attribute
eventEventThe raw DOM event
eventTypestringThe event type (e.g., 'input', 'submit')

If a submit button with a name attribute is focused, its name and value are also included.

import { processForm } from 'sygnal'
MyForm.intent = ({ DOM }) => ({
// All field changes and submits
FORM_DATA: processForm(DOM.select('.my-form')),
// Submit only
SUBMITTED: processForm(DOM.select('.my-form'), { events: 'submit' }),
// Custom events, no preventDefault
CHANGES: processForm(DOM.select('.my-form'), {
events: ['input', 'change'],
preventDefault: false
})
})

Creates a Cycle.js driver from a Promise-returning function.

function driverFromAsync(
promiseReturningFunction: (...args: any[]) => Promise<any>,
options?: DriverFromAsyncOptions
): CycleDriver
ParameterTypeDescription
promiseReturningFunctionFunctionAn async function or function returning a Promise
optionsDriverFromAsyncOptionsConfiguration (optional)
OptionTypeDefaultDescription
selectorstring'category'Property name used to categorize and filter responses
argsstring | string[] | Function'value'How to extract function arguments from incoming commands
returnstring'value'Property name to wrap the return value in
pre(incoming) => incomingIdentityPre-process incoming sink values before argument extraction
post(result, incoming) => resultIdentityPost-process results before sending to source

The driver source exposes:

source.select(selector?: string | Function): Stream
  • select() with no arguments returns all responses
  • select('name') filters responses where [selectorProperty] === 'name'
  • select(fn) filters responses using a custom predicate function
import { driverFromAsync } from 'sygnal'
// Create a driver from a fetch function
const apiDriver = driverFromAsync(
async (url, method = 'GET') => {
const res = await fetch(url, { method })
return res.json()
},
{
selector: 'endpoint',
args: (incoming) => [incoming.url, incoming.method],
return: 'data',
post: (result) => ({ success: true, payload: result })
}
)
// Register it
run(RootComponent, { API: apiDriver })
// Use in intent
MyComponent.intent = ({ API }) => ({
USERS_LOADED: API.select('users')
})
// Use in model
MyComponent.model = {
FETCH_USERS: {
API: () => ({ endpoint: 'users', url: '/api/users', method: 'GET' })
}
}

Creates a Cycle.js driver that registers a service worker and exposes lifecycle events as streams. (PWA Helpers guide)

function makeServiceWorkerDriver(
scriptUrl: string,
options?: ServiceWorkerOptions
): (sink$: Stream<ServiceWorkerCommand>) => ServiceWorkerSource
ParameterTypeDescription
scriptUrlstringPath to the service worker file (e.g., '/sw.js')
optionsServiceWorkerOptionsOptional configuration
OptionTypeDescription
scopestringRegistration scope for the service worker
source.select(type?: string): Stream
Event TypeEmitsDescription
'installed'trueWorker finished installing
'activated'trueWorker activated
'waiting'ServiceWorkerNew version waiting to activate
'controlling'trueWorker took control of the page
'error'ErrorRegistration or lifecycle error
'message'anyData from postMessage
CommandDescription
{ action: 'skipWaiting' }Tell waiting worker to activate immediately
{ action: 'postMessage', data: any }Send a message to the active worker
{ action: 'unregister' }Unregister the service worker
import { run, makeServiceWorkerDriver } from 'sygnal'
run(App, { SW: makeServiceWorkerDriver('/sw.js') })
App.intent = ({ SW, DOM }) => ({
UPDATE_READY: SW.select('waiting'),
APPLY_UPDATE: DOM.click('.update-btn'),
})
App.model = {
UPDATE_READY: (state) => ({ ...state, updateAvailable: true }),
APPLY_UPDATE: {
SW: () => ({ action: 'skipWaiting' }),
EFFECT: () => window.location.reload(),
},
}

A stream of booleans reflecting the browser’s online/offline state. (PWA Helpers guide)

const onlineStatus$: Stream<boolean>

Emits navigator.onLine immediately, then true/false on online/offline window events. SSR-safe — emits true once if window is undefined.

import { onlineStatus$ } from 'sygnal'
App.intent = () => ({
ONLINE_CHANGED: onlineStatus$,
})
App.model = {
ONLINE_CHANGED: (state, isOnline) => ({ ...state, isOffline: !isOnline }),
}

Captures the beforeinstallprompt browser event and exposes it reactively. (PWA Helpers guide)

function createInstallPrompt(): InstallPrompt
MethodReturnsDescription
select(type)Stream<any>Stream filtered by 'beforeinstallprompt' or 'appinstalled'
prompt()Promise | undefinedTriggers the deferred install prompt
import { createInstallPrompt } from 'sygnal'
const installPrompt = createInstallPrompt()
App.intent = ({ DOM }) => ({
CAN_INSTALL: installPrompt.select('beforeinstallprompt'),
INSTALL: DOM.click('.install-btn'),
})
App.model = {
CAN_INSTALL: (state) => ({ ...state, canInstall: true }),
INSTALL: {
EFFECT: () => installPrompt.prompt(),
STATE: (state) => ({ ...state, canInstall: false }),
},
}

The xstream Observable library, re-exported for convenience.

import { xs } from 'sygnal'
MethodDescription
xs.of(...values)Create a stream from values
xs.never()A stream that never emits
xs.empty()A stream that immediately completes
xs.periodic(ms)Emits incrementing numbers at an interval
xs.merge(...streams)Combine multiple streams — emits whenever any stream emits
xs.combine(...streams)Combine latest values from multiple streams
xs.fromPromise(promise)Create a stream from a Promise
MethodDescription
.map(fn)Transform emitted values
.mapTo(value)Replace all emissions with a constant value
.filter(fn)Only pass values where the predicate returns true
.startWith(value)Emit an initial value before the stream’s first emission
.remember()Cache the last emitted value for late subscribers
.flatten()Unwrap a stream of streams
.compose(operator)Apply a stream operator
.fold(fn, seed)Accumulate values (like reduce for streams)
.drop(n)Skip the first N emissions
.take(n)Only emit the first N values
.last()Emit only the final value
.endWhen(other$)Complete when another stream emits

See the full xstream documentation for more.


Sygnal re-exports commonly used xstream extra operators:

import { debounce, throttle, delay, dropRepeats, sampleCombine } from 'sygnal'

Wait for a pause in emissions before passing the latest value through.

const search$ = input$.compose(debounce(300))

Emit at most once per time period.

const scroll$ = scrollEvents$.compose(throttle(200))

Delay all emissions by a fixed duration.

const delayed$ = click$.compose(delay(500))

Drop consecutive duplicate values. Optionally provide a custom equality function.

const unique$ = values$.compose(dropRepeats())
const customUnique$ = objects$.compose(dropRepeats((a, b) => a.id === b.id))

When the source emits, combine with the latest value from other streams.

const withState$ = click$.compose(sampleCombine(state$))
// Emits [clickEvent, latestState] each time click$ fires

The DOM source wraps @cycle/dom’s MainDOMSource with a Proxy that adds shorthand event methods. Any property access that doesn’t already exist on the source becomes an event listener factory:

type SygnalDOMSource = MainDOMSource & {
[eventName: string]: (selector: string) => Stream<Event>
}
// DOM.eventName(selector) is equivalent to DOM.select(selector).events(eventName)
DOM.click('.btn') // DOM.select('.btn').events('click')
DOM.dblclick('.title') // DOM.select('.title').events('dblclick')
DOM.keydown('.input') // DOM.select('.input').events('keydown')
DOM.blur('.field') // DOM.select('.field').events('blur')
DOM.submit('.form') // DOM.select('.form').events('submit')
DOM.mouseenter('.card') // DOM.select('.card').events('mouseenter')

Any valid DOM event name works. The original .select().events() API is unchanged.

All DOM event streams (from .events() or shorthands) have chainable convenience methods:

DOM.input('.field').value() // e.target.value
DOM.change('.checkbox').checked() // e.target.checked
DOM.click('.item').data('id') // e.target.dataset.id (walks up via closest())
DOM.keydown('.input').key() // e.key
DOM.click('.btn').target() // e.target

Each method optionally accepts a transform function:

DOM.input('.count').value(Number) // Parse as number
DOM.click('.item').data('id', Number) // Parse data attribute as number
MethodExtractsNotes
.value(fn?)e.target.valueFor input/textarea/select
.checked(fn?)e.target.checkedFor checkboxes
.data(name, fn?)e.target.dataset[name]Walks up via closest([data-name])
.key(fn?)e.keyFor keyboard events
.target(fn?)e.targetThe DOM element

Returns enriched streams — chainable with .compose(), .filter(), etc.

The DND driver source provides equivalent shorthands as explicit methods:

DND.dragstart('task') // DND.select('task').events('dragstart')
DND.dragend('task') // DND.select('task').events('dragend')
DND.drop('lane') // DND.select('lane').events('drop')
DND.dragover('lane') // DND.select('lane').events('dragover')

See makeDragDriver() for full DND source documentation.


Declarative JSX props for managing element focus. These are handled by the pragma layer and never reach the DOM.

<input autoFocus={true} />

When the element is inserted into the DOM, .focus() is called on it. Works on any focusable element (input, textarea, select, button, elements with tabindex, etc.).

<input autoFocus={true} autoSelect={true} value={state.title} />

When used alongside autoFocus, .select() is called after .focus(), selecting all text in the element. Only meaningful on elements that support text selection (input, textarea).

  • Props are removed from the element before rendering — they do not become HTML attributes
  • A snabbdom insert hook is injected automatically
  • If you set your own hook={{ insert: fn }}, both hooks run (yours first, then focus)
  • autoSelect without autoFocus still triggers focus (both imply focusing the element)
function EditableTitle({ state }) {
return (
<div>
{state.isEditing
? <input
autoFocus={true}
autoSelect={true}
value={state.title}
className="title-input"
/>
: <h2 className="title">{state.title}</h2>
}
</div>
)
}

Sygnal re-exports all DOM helpers from @cycle/dom:

import { h, div, span, input, button, form, a, ul, li, p, ... } from 'sygnal'

Create virtual DOM nodes without JSX:

import { h } from 'sygnal'
// h(selector, data?, children?)
h('div.my-class', { style: { color: 'red' } }, [
h('h1', 'Hello'),
h('button.btn', 'Click me')
])
import { div, h1, button, input } from 'sygnal'
div('.container', [
h1('Hello'),
button('.btn', 'Click me'),
input('.text-input', { attrs: { type: 'text', placeholder: 'Enter name' } })
])