# Creating Mixins ## What This Covers How to author your own reusable host-element behavior with `createMixin`. Read this when the task involves: - Combining multiple low-level events or DOM hooks into one semantic mixin - Dispatching custom DOM events from a host node - Encapsulating imperative DOM setup that several components share - Typing custom events on `HTMLElementEventMap` for use with `on(...)` For the built-in mixins most code should use, see `mixins-styling-events.md`. Use `createMixin` from `remix/ui` to author reusable host-element behavior. Most app code should use built-in core mixins (`on`, `css`, `ref`, `link`, `attrs`) and animation mixins from `remix/ui/animation`. Create custom mixins when combining multiple low-level events into one semantic event, or when the pattern is reused across components. ## Core Semantics 1. A mixin handle is tied to one mounted host node lifecycle. 2. `insert` is the host-node availability point for imperative setup. 3. `remove` is teardown for that same lifecycle. 4. `queueTask` runs post-commit and receives `(node, signal)` for mixins. 5. Mixin render functions should stay pure; side effects belong in `insert`, `remove`, or queued work. ```tsx import { createMixin } from 'remix/ui' let myMixin = createMixin((handle) => { handle.addEventListener('insert', (event) => { // event.node is the mounted host node }) handle.addEventListener('remove', () => { // Clean up listeners, timers, observers }) return (props) => { handle.queueTask((node) => { // Post-commit work that needs the concrete host node }) return } }) ``` ## Patterns ### Pure prop transform ```tsx let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => ( )) ``` ### Lifecycle-managed imperative setup ```tsx let withFocus = createMixin((handle) => { handle.addEventListener('insert', (event) => { event.node.focus() }) return (props) => }) ``` ## Custom Event Mixins Create event mixins when you combine multiple low-level events into one semantic custom event that is reused across components. 1. Namespace custom event names (`myapp:*`) to avoid collisions. 2. Extend `Event` with the data consumers need. 3. Declare the event on `HTMLElementEventMap` for type safety with `on(...)`. 4. Dispatch from the host node inside the mixin. ```tsx import { createMixin, on } from 'remix/ui' export let dragReleaseType = 'myapp:drag-release' as const declare global { interface HTMLElementEventMap { [dragReleaseType]: DragReleaseEvent } } export class DragReleaseEvent extends Event { velocityX: number velocityY: number constructor(init: { velocityX: number; velocityY: number }) { super(dragReleaseType, { bubbles: true, cancelable: true }) this.velocityX = init.velocityX this.velocityY = init.velocityY } } export let dragRelease = createMixin((handle) => { let node: HTMLElement | undefined let tracking = false let velocityX = 0 let velocityY = 0 let lastX = 0 let lastY = 0 let lastT = 0 handle.addEventListener('insert', (event) => { node = event.node }) return () => ( { if (!event.isPrimary) return tracking = true lastX = event.clientX lastY = event.clientY lastT = event.timeStamp node?.setPointerCapture(event.pointerId) }), on('pointermove', (event) => { if (!tracking) return let dt = Math.max(1, event.timeStamp - lastT) velocityX = (event.clientX - lastX) / dt velocityY = (event.clientY - lastY) / dt lastX = event.clientX lastY = event.clientY lastT = event.timeStamp }), on('pointerup', () => { if (!tracking) return tracking = false node?.dispatchEvent(new DragReleaseEvent({ velocityX, velocityY })) }), ]} /> ) }) ``` Consume it: ```tsx
{ console.log('velocity:', event.velocityX, event.velocityY) }), ]} /> ```