references/mixins-styling-events.md
references/mixins-styling-events.mdBrowse 12 files
5,811 bytes
Token encoding: o200k_base
Snapshot 8beb6cd
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 withstyle - Imperative DOM access via
ref(...) - Navigation behavior on non-anchor elements with
link(...) - Native click, pointer, and keyboard behavior with
on(...), plus attributes withattrs(...) - 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:
<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:
<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:
<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:
<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:
<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:
<article mix={link('/courses/intro')}>
<h3>Introduction</h3>
</article>
Options match NavigationOptions: src, target, history
('push' | 'replace'), resetScroll.
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:
<button mix={on('click', () => doAction())}>Action</button>
For gesture-specific behavior, compose the pointer or keyboard events the interaction actually needs:
<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:
<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:
{
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:
{
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.