Composition
Combine styling, semantics and behavior while keeping one operable control for each action.
Choose a semantic element
Use a link for navigation and a button for an action. Styling a link to look like a button does not make it a submit control. Avoid nesting interactive controls inside each other; a button inside a trigger button creates conflicting keyboard and pointer behavior.
| Need | Use | Avoid |
|---|---|---|
| Navigate to a source route | An anchor or router link with href | A div that sets location on click |
| Submit a form | Button type="submit" | A link with a submit-like label |
| Expand details | CollapsibleTrigger with its panel | A clickable heading without expanded state |
| Open an overlay | The overlay’s trigger part | A second independently managed focus trap |
| Group content | Non-interactive layout surface | Making every card a button because it has a shadow |
Base UI render composition
Base UI parts exposed by this package accept their primitive’s render prop. The trigger supplies behavioral props to the rendered element. In the checked example, CollapsibleTrigger renders the library Button rather than wrapping another button inside it.
Current upstream Base UI documentation uses the newer @base-ui/react package name. This workspace is pinned to @base-ui-components/react 1.0.0-rc.0. Read the installed types and this wrapper’s API before copying newer upstream imports or options.
Example details collapsed.
'use client';
import { useState } from 'react';
import { Button, Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@mobtranslate/ui';
export function CompositionExample() {
const [open, setOpen] = useState(false);
return <Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger render={<Button variant="outline" type="button" />}>
Example source details
</CollapsibleTrigger>
<CollapsiblePanel>
<p className="mt-4">This is interface placeholder text, not an attested source record.</p>
</CollapsiblePanel>
<p role="status" className="mt-3 text-sm">Example details {open ? 'expanded' : 'collapsed'}.</p>
</Collapsible>;
}
Forward props and refs
- A custom render component must forward the ref to the actual operable DOM element and pass through supplied ARIA attributes, event handlers and state attributes.
- Do not replace a primitive’s onKeyDown or onClick without preserving its behavior. Consult the installed version’s event composition contract.
- Keep accessible names on the real control. Decorative icons should not add duplicate announcements.
- Preserve stable keys during reorder so React retains the intended element identity. Do not key mutable lists by position.
Controlled and uncontrolled state
Use controlled open/value with the matching change handler when the surrounding task owns the state. Use defaultOpen/defaultValue for an uncontrolled initial state. Do not switch modes mid-lifecycle or initialize controlled text inputs with undefined.
| State | Owner | Verification |
|---|---|---|
| Uncontrolled disclosure | Primitive initialized by defaultOpen | Can open and close without a parent update |
| Controlled disclosure | Parent open + onOpenChange | Parent reflects the requested new state |
| Validated input | Form/application state | Error, retained value and accessible description agree |
| Pending request | Request lifecycle | Duplicate submission is prevented; cancel/recovery remains available |
| Persistent result | Backend plus visible application state | Verify the actual stored outcome, not just a local success label |
Forms and native props
- Set button type explicitly inside forms. A custom action should usually use type="button" unless it intentionally submits.
- Input size selects the component size; htmlSize controls the native input size attribute. Do not conflate these APIs.
- Connect labels and descriptions to the input. A wrapper’s visual error style does not by itself announce an error.
- Verify submitted names and values from the actual form. Not every styled selection widget is automatically a native successful form control.
Portal ownership
A portal keeps React ownership but changes DOM placement. Theme, direction and scoped styles follow the DOM. Overlay primitives also manage focus and dismissal; do not add a second independent global handler that fights them.
- Give dialogs a real title and description when the description is useful.
- Restore focus to a surviving trigger or another deliberate destination after close.
- Check nested overlays, Escape behavior and scroll containment.
- A visual backdrop is not proof that background interaction and focus are correctly controlled.
Current wrapper limits
Button asChild now forwards DOM props, merges handlers/styles/classes and both refs through the installed Base UI utilities, and enforces disabled/loading activation blocking. It preserves the child’s element semantics. Do not assume every other wrapper implements asChild; primitive render remains the appropriate composition path for Base UI parts.
Some exports are native styled elements and others are Base UI parts. Their composition APIs differ. Check the actual exported type instead of applying one wrapper recipe to every component.
Verification
- Inspect the checked disclosure: exactly one trigger button, a changing aria-expanded value and a panel linked to that trigger.
- Activate with Enter, Space and pointer input. Controlled state and the local status should agree.
- Inspect a composed control’s accessible name and forwarded ref/focus behavior.
- Test form submission, overlay close and any default browser action separately. A visual click response does not prove the whole task.