Headless module
stems-ui/direction-provider
DirectionProvider sets the text direction (ltr or rtl) for a subtree, so Stems widgets flip
their horizontal keyboard navigation and positioning.
Base UI equivalent: Direction Provider.
Usage
import { DirectionProvider } from 'stems-ui/direction-provider'
import * as slider from 'stems-ui/slider'
function App() {
return () => (
<DirectionProvider direction="rtl">
<slider.Context defaultValue={25}>{/* ... */}</slider.Context>
</DirectionProvider>
)
}
If your app already sets dir on <html> or a container, you don't need the provider: Stems
reads dir directly.
stems-ui/direction-provider
<DirectionProvider direction?>:directionis'ltr'(default) or'rtl'.getDirection(node): the direction that applies to a DOM node: the nearestdir="ltr|rtl"attribute, then the computed CSSdirection, then'ltr'. This is what Stems widgets call at event time.getDirectionFromContext(handle): the nearest provider's direction for a component or mixinhandle, orundefinedwithout a provider. Base UI'suseDirection. Use it for render-time decisions; it doesn't seedirattributes the app set itself.- Types:
TextDirection,DirectionProviderProps,DirectionContextValue.
Design: the provider renders dir
Stems widgets resolve direction with getDirection(node) inside event handlers, where they only
have a DOM node, not a component handle. So the provider has to be visible in the DOM.
DirectionProvider renders:
<div dir="rtl" style="display: contents" data-direction-provider>…children…</div>
getDirectionfinds it throughclosest('[dir]'), so every existing widget (tabs, accordion, radio group, toggle button group, slider, menu, select, combobox, OTP field, scroll area…) honors it with no changes, and nested providers ordirattributes override it.display: contentsgenerates no box, so layout is unchanged: children stay flex or grid items of the provider's parent. A plaindivhas no role, so nothing is added to the accessibility tree (the olddisplay: contentsaccessibility bugs affected elements with semantics, such as buttons and tables).- It's in the server-rendered HTML, so direction is right before hydration.
- Difference from Base UI: Base UI's provider only changes component behavior and you must also
set
diryourself. The Stems provider also sets the CSS direction of its subtree (text alignment, logical properties, bidi), which is what you want in practice. Base UI needs the context mainly for portaled popups; Stems popups render in place (top layer, no portals), sodirinherits naturally. - The one structural cost is the extra element: selectors like
.parent > .childand:first-childsee it. Setdiron your own element instead if that matters.
Utilities
Base UI's other utilities and how they map to Stems:
- CSP Provider: blocked upstream. Server-rendered
<style>tags from@remix-run/componenthave no nonce, so a provider has nothing to configure yet (docs/upstream-issues.md§6). Stems modules don't inject style elements; they use inlinestyleattributes only. mergeProps: not needed. Remix composes behavior withmixarrays (mix={[tabs.tab({ value }), mix]}), andattrs()merges attributes so explicit host props win.useRender: not needed. Mixins apply to whatever element you put them on, so there is norenderprop to replace the element.