Files
dotfiles/dot_agents/skills/remix/references/mixins-styling-events.md
T
Prad Nukala 42d17e825e chs: re-add drifted files
dot_agents/dot_skill-lock.json
2026-08-14 13:53:55 -04:00

198 lines
5.9 KiB
Markdown

# Mixins, Styling, and Events
## What This Covers
How to attach behavior, styles, and DOM-aware setup to host elements with `mix`. Read this when the task involves:
- DOM event handling with `on(...)`
- Static styling with `css(...)` and dynamic styling with `style`
- Imperative DOM access via `ref(...)`
- Navigation behavior on non-anchor elements with `link(...)`
- Native click, pointer, and keyboard behavior with `on(...)`, plus attributes with `attrs(...)`
- Element-level animation mixins from `remix/ui/animation`
For richer animation work (springs, tweens, layout transitions), see `animate-elements.md`. For authoring custom mixins, see `create-mixins.md`. For component lifecycle and updates, see `component-model.md`.
Compose behavior on host elements with `mix`. Pass a single mixin directly (`mix={on(...)}`), or an array when composing multiple mixins (`mix={[css(...), on(...)]}`). Core mixins are imported from `remix/ui`; animation mixins are imported from `remix/ui/animation`.
## `on(type, handler, capture?)`
Attaches a typed DOM event handler. The handler receives the event and an `AbortSignal` that aborts when the handler is re-entered or the component is removed — this prevents race conditions:
```tsx
<input
mix={on('input', async (event, signal) => {
let query = event.currentTarget.value
loading = true
handle.update()
let response = await fetch(`/search?q=${query}`, { signal })
let data = await response.json()
if (signal.aborted) return
results = data.results
loading = false
handle.update()
})}
/>
```
Multiple events on the same element:
```tsx
<form
mix={on('submit', (event) => {
event.preventDefault()
let formData = new FormData(event.currentTarget)
})}
>
```
## `css(styles)`
Applies generated class names for CSS object styles. Produces static CSS rules inserted into the document. Supports pseudo-selectors, pseudo-elements, attribute selectors, descendant selectors, and media queries using `&` to reference the current element:
```tsx
<button
mix={css({
color: 'white',
backgroundColor: 'blue',
padding: '12px 24px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
'&:hover': { backgroundColor: 'darkblue' },
'&:active': { transform: 'scale(0.98)' },
'&:disabled': { opacity: 0.5, cursor: 'not-allowed' },
'& .title': { fontSize: '20px', fontWeight: 'bold' },
'@media (max-width: 768px)': { width: '100%' },
})}
/>
```
### `css(...)` vs `style` prop
Use `css(...)` for static styles, selectors, and media queries. Use `style` for dynamic values that change often. Prefer CSS nested selectors for parent-state-affects-children over managing hover/focus state in JavaScript:
```tsx
<div
mix={css({
backgroundColor: 'blue', // static
'&:hover': { '& .title': { color: 'blue' } }, // parent hover → child
})}
style={{ width: `${progress}%` }} // dynamic
/>
```
## `ref(callback)`
Calls a callback when an element is inserted. The callback receives the DOM node and an `AbortSignal` that aborts when the element is removed:
```tsx
<input mix={ref((node) => node.focus())} />
<div mix={ref((node, signal) => {
let observer = new ResizeObserver((entries) => {
dimensions.width = Math.round(entries[0].contentRect.width)
handle.update()
})
observer.observe(node)
signal.addEventListener('abort', () => observer.disconnect())
})} />
```
The `ref` callback runs once when the element is first rendered, not on every update.
## `link(href, options?)`
Adds client-side navigation behavior to any element. Makes non-anchor elements behave like Remix navigation links:
```tsx
<article mix={link('/courses/intro')}>
<h3>Introduction</h3>
</article>
```
Options match `NavigationOptions`: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
On a native anchor, the `history` option renders the corresponding `rmx-history="push|replace"` attribute so the enhanced navigation uses the same history behavior.
## Native press and keyboard interactions
Use native DOM events directly with `on(...)`. For buttons and links, `click` already includes keyboard activation when the element has the right semantics:
```tsx
<button mix={on('click', () => doAction())}>Action</button>
```
For gesture-specific behavior, compose the pointer or keyboard events the interaction actually needs:
```tsx
<button
mix={[
on('pointerdown', (event) => {
event.currentTarget.setPointerCapture(event.pointerId)
}),
on('pointerup', () => doAction()),
]}
>
Action
</button>
<div
tabIndex={0}
mix={on('keydown', (event) => {
if (event.key === 'Escape') close()
if (event.key === 'Enter' || event.key === ' ') doAction()
})}
/>
```
## `attrs()`
Sets HTML attributes through the mixin system.
## Animation Mixins
### `animateEntrance(config)`
Animates an element when it is inserted into the DOM. Config specifies the **starting** style:
```tsx
<div mix={animateEntrance({ opacity: 0, transform: 'translateY(8px)', duration: 180 })} />
```
### `animateExit(config)`
Animates an element when it is removed. Config specifies the **ending** style. The element is kept in the DOM until the animation completes:
```tsx
{
isVisible && (
<div
key="panel"
mix={[
animateEntrance({ opacity: 0, transform: 'scale(0.98)', ...spring('smooth') }),
animateExit({ opacity: 0, duration: 120, easing: 'ease-in' }),
]}
/>
)
}
```
### `animateLayout(config?)`
Animates layout changes (position/size) using FLIP-style transforms:
```tsx
{
items.map((item) => (
<li key={item.id} mix={animateLayout({ duration: 220, easing: 'ease-out' })} />
))
}
```
Options: `duration` (default 200ms), `easing` (default spring snappy), `size` (boolean, default true — include scale projection for size changes).
Always key elements you expect to animate. Use `...spring(preset)` to spread `duration` and `easing` into any animation config.