Files
dotfiles/dot_agents/skills/remix/references/create-mixins.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

4.2 KiB

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.
import { createMixin } from 'remix/ui'

let myMixin = createMixin<HTMLElement>((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 <handle.element {...props} />
  }
})

Patterns

Pure prop transform

let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (
  <handle.element {...props} title={title} />
))

Lifecycle-managed imperative setup

let withFocus = createMixin<HTMLElement>((handle) => {
  handle.addEventListener('insert', (event) => {
    event.node.focus()
  })
  return (props) => <handle.element {...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.
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<HTMLElement>((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 () => (
    <handle.element
      mix={[
        on('pointerdown', (event) => {
          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:

<div
  mix={[
    dragRelease(),
    on(dragReleaseType, (event) => {
      console.log('velocity:', event.velocityX, event.velocityY)
    }),
  ]}
/>