solid-baseui
v0.1.0-alpha.4
Published
WIP: An unofficial port of Base UI for SolidJS
Maintainers
Readme
solid-baseui
WIP: An unofficial port of Base UI for SolidJS
A native Solid 2 port of Base UI. It preserves Base UI's observable behavior and accessibility contracts while replacing React-specific architecture with Solid-native reactivity, rendering, refs, and lifecycle APIs.
This is an unofficial community port. It is not affiliated with, endorsed by, or maintained by MUI or the Base UI team, nor by the SolidJS core team.
Install
Install the current release:
bun add solid-baseuiPeer dependencies:
solid-js ^2.0.0-rc.6
@solidjs/web ^2.0.0-rc.6Solid 2 is itself a release candidate, which is part of why this package is
alpha. Upgrade both runtimes together and use a compatible Solid 2 compiler.
RC.6 includes the portal event-delegation fix required by this port; no local
dependency patch is needed. This workspace uses @solidjs/[email protected]
with the RC.6 compiler.
Usage
import { For } from "solid-js";
import { Toast, useToastManager } from "solid-baseui/toast";
function ToastList() {
const manager = useToastManager();
return (
<For each={manager.toasts()} keyed={(toast) => toast.id}>
{(toast) => (
<Toast.Root toast={toast()}>
<Toast.Content>
<Toast.Title />
<Toast.Description />
</Toast.Content>
<Toast.Close>Dismiss</Toast.Close>
</Toast.Root>
)}
</For>
);
}
function AddButton() {
const { add } = useToastManager();
return <button onClick={() => add({ title: "Saved" })}>Save</button>;
}
// `useToastManager` reads provider context, so both live inside the provider.
export function App() {
return (
<Toast.Provider>
<AddButton />
<Toast.Portal>
<Toast.Viewport>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}Every family is available from the root entry point or its own subpath
(solid-baseui/dialog, solid-baseui/select, and so on).
Polymorphic parts render their native element by default. To replace it, supply
a callback and forward its live DOM props, including ref, to the final element:
import { Button } from "solid-baseui/button";
<Button
nativeButton={false}
render={(props, state) => (
<a {...props} href="/settings" data-disabled={state.disabled ? "" : undefined} />
)}
>
Settings
</Button>;The callback receives live object state, not an accessor. Read state.disabled
and supplied props in JSX or reactive computations. Destructuring or eagerly
copying them during callback setup loses liveness. Always forward props and ref,
including through custom components, so events, accessibility, focus, and
registration reach the final DOM node.
Migrating To 0.1.0-alpha.1
Version 0.1.0-alpha.1 replaces as with callback-only render. There is no compatibility
alias, and JSX-valued render={<a />} is unsupported. Replace an old
<Button as="a" href="/settings" nativeButton={false}> with the callback above.
No Dynamic, dynamic(), or withoutComponentBinding adapter is needed: the
callback writes native or custom-component JSX directly.
- Outer attributes remain typed for the default native element. Put
hrefand other replacement-only attributes inside the callback, not on a native-button outer API. Likewise, supply required custom-component props inside the callback, for examplerender={(props) => <AppLink {...props} to="/settings" />}. - Keep semantic flags such as
nativeButton={false}where supported when the final element is not a button. Selecting an element does not automatically select the component's keyboard or disabled policy. - Render selection happens at mount. Replacing the
renderprop later does not switch the renderer. To change the root, use conditional JSX inside one stable callback and forward props/ref to every possible final element, for examplerender={(props) => <>{compact() ? <span {...props} /> : <div {...props} />}</>}on a visual part that supports those roots. - Put interaction handlers such as
onClickon the outer Base UI component. It composes caller handlers with its internal behavior. Userenderfor the element, styling, and prop forwarding, not to override supplied event handlers. Custom components used insiderendershould also forward those handlers to the final DOM node. - A spread does not compose an overridden ref or class. Preserve supplied values with Solid ref/class arrays when adding your own.
import { Button } from "solid-baseui/button";
<Button
onClick={() => console.info("Save activated")}
render={(props) => (
<button
{...props}
class={[props.class, "action"]}
ref={[props.ref, (element) => { element.dataset.action = "save"; }]}
/>
)}
>
Save
</Button>;Avoid render={(props) => <button {...props} onClick={handleClick} />}:
the explicit handler replaces the supplied handler and can break the component's
internal interaction behavior. Moving the spread last can instead discard your
handler. Put onClick={handleClick} on the outer component so both behaviors are
preserved without manually composing handlers.
Component authors can import useRender from solid-baseui/use-render or the
package root. Its signature is
useRender(props, stateAccessor, nativeFallbackCallback), returning a local JSX
component. For example, `const Render = useRender(props, () => state(), (domProps) =>
See CHANGELOG.md for this alpha's changes.
Status
| Area | Status |
| --- | --- |
| Current release | 0.1.0-alpha.4, available from solid-baseui |
| Components | All 38 component families implemented and exported |
| Framework | Native Solid 2, using the RC.6 runtime and compiler |
| Verification | Typecheck, lint, unit, SSR, hydration, and Chromium/Firefox/WebKit suites passing |
| Current focus | Stabilization, performance, and real-application integrations |
The latest release includes the completed Tooltip, Combobox, Autocomplete, and
Drawer verification work. The optional unstable-use-media-query utility remains
outside the 38-component scope and is not implemented.
See knowledge/README.md for verification scope and
remaining work, and BASE_UI_PORTING_INVENTORY.md
for the upstream porting inventory.
Solid API decisions
This is a port, not a transliteration. Where React idioms do not carry over, the Solid-native equivalent wins:
- Components use
class, notclassName. - Polymorphic components use callback-only
renderwith native JSX defaults. React element cloning and JSX-valued render props are not supported. - Provider hooks return accessors, because replacing a Solid context value is not reactive.
- Refs are ordinary props and compose through Solid ref arrays. There is no
forwardRefand there are no ref objects. - Event callbacks receive native DOM events, carrying Base UI's cancellation methods where applicable.
- Ordinary props use Solid's
mergeandomit; classes and refs use native arrays rather than a general-purpose prop merger. - Lists keyed by identity use
<For each={...} keyed={(item) => item.id}>where Base UI relied on React'skey.
Development
Bun is the only supported package manager.
bun install
bun run test:browser:install
bun run typecheck
bun run lint
bun run test
bun run test:package
bun run buildbun run build emits:
dist/lib/client— hydratable browser artifactsdist/lib/server— hydratable SSR artifactsdist/lib/types— public declarationsdist/client— the static preview application
bun run dev serves the preview app, which has a page per family covering every
scenario from that component's Base UI documentation.
Credits
Base UI by MUI, MIT licensed, is the upstream this port
follows. Its copyright notice is preserved in LICENSE.
License
MIT
