The Problems Cereb Solves

Note: These are the problems Cereb solves.

1. No Abstraction for Event Flow

DOM events are just callbacks—there’s no structure for managing state, expressing dependencies between events, or composing multiple inputs. Consider a multi-input zoom implementation:

// Before: Scattered handlers, shared state, duplicated logic
let currentScale = 1;
let isZoomMode = false;
let initialPinchDistance = 0;
window.addEventListener('keydown', e => {
if (e.key === 'z') { isZoomMode = true; }
if (isZoomMode && (e.key === '+' || e.key === '-')) {
e.preventDefault();
currentScale = Math.max(MIN, Math.min(MAX, currentScale * ...));
render(currentScale);
}
});
window.addEventListener('keyup', e => { /* isZoomMode = false ... */ });
box.addEventListener('wheel', e => {
if (!isZoomMode) return;
currentScale = Math.max(MIN, Math.min(MAX, ...)); // duplicated
render(currentScale);
}, { passive: false });
// Pinch: touchstart/touchmove/touchend with distance calculation...
box.addEventListener('touchstart', e => { /* ... */ });
box.addEventListener('touchmove', e => { /* distance, ratio, min/max again */ });
box.addEventListener('touchend', () => { /* cleanup */ });
// 8+ handlers, 3+ shared states, min/max duplicated everywhere

Cereb models events as streams, creating readable and composable pipelines:

// After: Stream abstraction—composable, stateless, explicit
import { keydown, keyheld, wheel, pinch } from "cereb";
import { zoom, when, extend, spy } from "cereb/operators";
const zoomMode$ = keyheld(window, { code: "KeyZ" })
.pipe(extend((signal) => ({ opened: signal.value.held })));
const zoomOp = () => zoom({ minScale: 0.5, maxScale: 3.0, baseScale: getScale });
// Pinch zoom
pinch(element)
.pipe(zoomOp())
.on(applyScale);
// z + wheel zoom
wheel(element, { passive: false })
.pipe(
when(zoomMode$),
spy((signal) => signal.value.originalEvent.preventDefault()),
extend((signal) => ({ ratio: Math.exp(-signal.value.deltaY * 0.005) })),
zoomOp(),
)
.on(applyScale);
// z + '+/-' zoom
keydown(window, { code: ["Equal", "Minus"] })
.pipe(
when(zoomMode$),
extend((signal) => ({ ratio: signal.value.code === "Equal" ? 1.2 : 1 / 1.2 })),
zoomOp(),
)
.on(applyScale);

2. Lightweight Bundle Size

Benchmark: Equivalent pan gesture implementation

MinifiedGzipped
cereb (pan)4.58 KB1.73 KB
Hammer.js20.98 KB7.52 KB

~77% smaller than Hammer.js for equivalent pan gesture functionality.

3. Performance & Resource Efficiency

Event Listener Reuse

// Before: Multiple addEventListener calls
window.addEventListener('keydown', handler1);
window.addEventListener('keydown', handler2);
window.addEventListener('keydown', handler3);
// After: Single native listener, multiple observers
const key$ = keyboard(window).pipe(share());
key$.on(handler1);
key$.on(handler2);
key$.on(handler3);

Single Responsibility Operators

pan(element)
.pipe(
offset({ target }), // Element-relative coordinates
axisLock() // Lock to horizontal/vertical
)

Each operator does one thing well, and you compose them as needed.