Composition
View as MarkdownBuild complex interfaces by combining simple parts. This is the core pattern in Fluid Primitives.
The asChild Prop
Permalink to heading "The asChild Prop"Sometimes you need a component's behavior on your own element. Pass asChild="{true}" to merge the component's attributes into its first child instead of rendering the default element.
<ui:tooltip.root>
<ui:tooltip.trigger asChild="{true}">
<a href="https://example.com">Link with tooltip</a>
</ui:tooltip.trigger>
<ui:tooltip.content>Tooltip for this link</ui:tooltip.content>
</ui:tooltip.root>
The <a> tag receives all necessary attributes (aria-describedby, event handlers, etc.) while keeping its link behavior.
This pattern comes from Radix UI's asChild API.
Adding asChild to your own component
Permalink to heading "Adding asChild to your own component"asChild is opt-in per component - every Fluid Primitives component already opts in wherever it
makes sense, but your own components only get it if you ask for it. Add {ui:asChild()} inline,
inside the opening bracket of whichever tag should receive the merged attributes - the same way you
already place {ui:ref(...)}/{ui:attributes()}:
<button
{f:if(condition: class, then: 'class="{class}"' )}
{ui:attributes()}
{ui:asChild()}>
<f:slot />
</button>
That's it - no separate declaration needed, {ui:asChild()} both registers the prop and marks the
exact tag it should target.
Limitations
Permalink to heading "Limitations"- Opt-in per part.
asChildonly works on parts that add{ui:asChild()}to their own wrapper element - root parts that render nothing but their slot content (e.g. Dialog, Popover, Tooltip'sroot) have no element to merge attributes onto, so they don't offer it. Check a part's Arguments table in its component docs page to confirm. - Single child element required. Text nodes or multiple elements won't work.
- Child attributes take precedence. If the child already has an attribute, it won't be overwritten.
- No context access inside asChild. The slot content can't access the component's context. See TYPO3/Fluid#1132.
Accessibility Note
Permalink to heading "Accessibility Note"When using asChild, ensure your element remains accessible. A <div> replacing a <button> needs proper role, tabindex, and keyboard handlers that the original element provided natively.
Sharing IDs Between Components
Permalink to heading "Sharing IDs Between Components"When multiple components share one element (like a collapsible trigger that also has a tooltip), both write an id onto it - share that ID so they agree and the accessibility bindings stay intact.
<f:variable name="sharedTriggerId" value="{ui:id()}" />
<ui:collapsible.root ids="{trigger: sharedTriggerId}">
<ui:tooltip.root ids="{trigger: sharedTriggerId}">
<ui:collapsible.trigger asChild="{true}">
<ui:tooltip.trigger>Toggle with tooltip</ui:tooltip.trigger>
</ui:collapsible.trigger>
<ui:tooltip.content>Click to expand</ui:tooltip.content>
</ui:tooltip.root>
<ui:collapsible.content>
<p>The expanded content.</p>
</ui:collapsible.content>
</ui:collapsible.root>
Both the collapsible and tooltip now reference the same trigger element, keeping aria-controls, aria-describedby, and other attributes in sync.
The ids prop accepts an object where keys are part names (trigger, content, etc.) and values are the IDs to use.