mergeProps
UtilitiesComposable props
Chain handlers in order while merging class and style values into one spreadable object
Handler trace
Click the merged button to record call order.
Merged output
class: btn w-full justify-center border-brand-500 bg-ink-800 text-brand-400
style: border-width: 2px; padding-block: 0.8rem; letter-spacing: 0.08em; border-radius: 9999px;
Both handlers run in merge order.
<script lang="ts">
import { mergeProps } from 'bits-ui';
let cancelAfterFirst = $state(false);
let calls = $state<string[]>([]);
const behaviorProps = $derived({
class: 'btn w-full justify-center border-brand-500',
style: { borderWidth: '2px', paddingBlock: '0.8rem' },
onclick: (event: MouseEvent) => {
calls = [...calls, 'behavior handler'];
if (cancelAfterFirst) event.preventDefault();
}
});
const consumerProps = $derived({
class: 'bg-ink-800 text-brand-400',
style: 'letter-spacing: 0.08em; border-radius: 9999px;',
onclick: () => {
calls = [...calls, 'consumer handler'];
}
});
const merged = $derived(mergeProps(behaviorProps, consumerProps));
</script>
<div class="grid gap-4">
<button {...merged}>Run merged handlers</button>
<div class="grid gap-3 sm:grid-cols-2">
<section class="panel p-4">
<p class="mb-2 text-xs font-semibold tracking-wider text-ink-400 uppercase">Handler trace</p>
{#if calls.length}
<ol class="grid gap-1 text-sm text-ink-200">
{#each calls as call, index (`${index}-${call}`)}
<li><span class="mr-2 font-mono text-brand-400">{index + 1}</span>{call}</li>
{/each}
</ol>
{:else}
<p class="muted text-sm">Click the merged button to record call order.</p>
{/if}
</section>
<section class="panel p-4">
<p class="mb-2 text-xs font-semibold tracking-wider text-ink-400 uppercase">Merged output</p>
<p class="text-xs leading-relaxed text-ink-200">
<span class="text-ink-400">class:</span> {String(merged.class)}
</p>
<p class="mt-2 text-xs leading-relaxed text-ink-200">
<span class="text-ink-400">style:</span> {String(merged.style)}
</p>
</section>
</div>
<div class="flex flex-wrap items-center gap-2">
<button
class="btn btn-sm"
class:border-accent-500={cancelAfterFirst}
onclick={() => (cancelAfterFirst = !cancelAfterFirst)}
>
preventDefault: {cancelAfterFirst ? 'on' : 'off'}
</button>
<button class="btn btn-ghost btn-sm" onclick={() => (calls = [])}>Clear trace</button>
<p class="muted text-xs">
{cancelAfterFirst ? 'The first handler now cancels the second.' : 'Both handlers run in merge order.'}
</p>
</div>
</div>
mergeProps is the safe boundary between a headless component's behavior props and consumer props. Event handlers run in argument order, but calling event.preventDefault() stops every later event handler in the merged chain. Non-event functions are also chained, without cancellation semantics.
Classes are combined with clsx. Styles may be strings or objects and are normalized into one style string; when the same CSS property appears more than once, the later props object wins. This is also the merge behavior Bits UI uses internally for rest props and child-snippet props.