
A discussion thread for anyone who's built or used React compound-component libraries (Radix, Headless UI, Mantine, Reach UI, Ariakit, etc.) — interested in how others resolve prop priority when the same configuration can be set in multiple places.
I've been maintaining a small audio-component library and recently added customization knobs (triggerType, placement) to a couple of compound slots. The same value can come from three places:
- Compound prop —
<Player.Volume placement="bottom" /> - UIContext at the provider —
<Player placement={{ volumeSlider: "bottom" }} /> - Component default — hardcoded fallback inside the slot itself
I landed on compound prop > UIContext > component default as the resolution order. Reasoning:
- Per-instance overrides should win over global defaults (closest-to-the-user wins)
- UIContext is the "set once, apply to all instances" knob, so it's the middle layer
- Component default catches the case where neither is set
Representative snippet:
function VolumeSlot({
triggerType: triggerTypeProp,
placement: placementProp,
}: VolumeProps) {
const ctx = useUIContext();
const triggerType =
triggerTypeProp ?? ctx.triggerType?.volumeSlider ?? "hover";
const placement =
placementProp ?? ctx.placement?.volumeSlider ?? "auto";
// ...
}
Three specific questions I keep going back and forth on:
Reverse priority for "centralized config wins" — has anyone reached for
UIContext > compound propinstead, so app-wide config can lock down per-instance overrides? Feels backwards to me, but I can imagine a design-system team wanting it.Partial overrides — if the compound prop sets
placementbut nottriggerType, and UIContext sets both, do you merge per-key (current behavior) or treat the compound prop as all-or-nothing? The per-key merge is convenient but makes the resolution path harder to reason about by reading one file.Multiple instances of the same slot under one provider — if I mount two
<Player.Volume />slots, they share one UIContext entry. Do you key the context value to slot identity (e.g. viaidprop) or treat all instances of the same slot type as equivalent? I went with the latter for simplicity but it bites when one instance wants a different default.
Curious how Radix / Headless UI / Mantine / Reach UI / Ariakit internals handle this. If anyone has links to their resolution logic in source, I'd appreciate the pointer.
For context (not the topic of the post): the library is an audio player with a compound-slot layout — https://github.com/slash9494/react-modern-audio-player. The 2.3.1 release notes have the actual API surface this thread is about.