# versecafe/quoin

[View on GitCafe](https://git.cafe/versecafe/quoin)

Repository: [versecafe/quoin](https://git.cafe/versecafe/quoin)

Visibility: public

Headless UI tooling for SolidJS v2

Default branch: main

## Resources

- [Source](https://git.cafe/versecafe/quoin/tree/refs%2Fheads%2Fmain?format=markdown)

- [Commit history](https://git.cafe/versecafe/quoin/commits/refs%2Fheads%2Fmain?format=markdown)

- [Issues](https://git.cafe/versecafe/quoin/issues?format=markdown)

- [Pull requests](https://git.cafe/versecafe/quoin/pulls?format=markdown)

- [Stacks](https://git.cafe/versecafe/quoin/stacks?format=markdown)

- [Branches](https://git.cafe/versecafe/quoin/branches?format=markdown)

- [Tags](https://git.cafe/versecafe/quoin/tags?format=markdown)

Snapshot commit: 7545d17e8aecad98f126e982a34b9b5973519311

## README

[README.md](https://git.cafe/versecafe/quoin/blob/7545d17e8aecad98f126e982a34b9b5973519311/README.md?format=markdown)

---

# quoin

[![Socket Badge](https://badge.socket.dev/npm/package/quoin-ui/0.3.0)](https://badge.socket.dev/npm/package/quoin-ui/0.3.0)

An unstyled, accessible UI component library for [SolidJS](https://www.solidjs.com) — a faithful port of [Base UI](https://base-ui.com), built on Solid 2.0's fine-grained reactivity.

You bring the styles and the markup; quoin brings the behavior, accessibility, and positioning. Every component is a set of composable parts you assemble yourself, so you keep full control over the DOM.

> [!WARNING]
> quoin is pre-release software (`0.3.x`). The API is still settling and breaking changes should be expected. It targets the `solid-js@2.0.0-beta` line and currently carries one development-build context patch (see `patches/`).

## Installation

```bash
pnpm install quoin-ui
# or
bun add quoin-ui
```

quoin targets **Solid 2.0** and expects the following peers:

```bash
bun install solid-js@2.0.0-beta.28 @solidjs/web@2.0.0-beta.28 @solidjs/signals@2.0.0-beta.28
```

### Required patches (Solid beta)

quoin relies on one small fix to the Solid 2.0 beta: the `createContext`
provider must read its value outside the provider root's tracking scope.
Until that lands upstream, **bun** consumers should apply the included patch
to avoid strict-read diagnostics and accidental provider-root subscriptions.

The patch files ship inside the package. Copy them into your project and
register them in your `package.json`:

```bash
mkdir -p patches
cp node_modules/quoin/patches/* patches/
```

```jsonc
// package.json
{
  "patchedDependencies": {
    "solid-js@2.0.0-beta.28": "patches/solid-js@2.0.0-beta.28.patch"
  }
}
```

```bash
bun install   # applies the patches
```

The Portal and async rejection patches required by beta.14 are incorporated
upstream in beta.28 and are no longer carried by quoin.

## Two ways to build

quoin exposes every component at two layers. Reach for whichever fits the job — they share the same underlying state machine, so you can mix them.

### Compound components

The **Base UI / React-style** API: a namespace of composable parts you nest to build the markup. Batteries included — portal, focus trap, scroll lock, positioning, and transitions are wired up for you. This is what you want most of the time.

```tsx
import { createSignal } from "solid-js";
import { Dialog } from "quoin-ui/dialog";

function Example() {
  const [open, setOpen] = createSignal(false);

  return (
    <Dialog.Root open={open()} onOpenChange={setOpen}>
      <Dialog.Trigger>View notifications</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Backdrop />
        <Dialog.Popup>
          <Dialog.Title>Notifications</Dialog.Title>
          <Dialog.Description>
            You are all caught up. Good job!
          </Dialog.Description>
          <Dialog.Close>Close</Dialog.Close>
        </Dialog.Popup>
      </Dialog.Portal>
    </Dialog.Root>
  );
}
```

Every component family is also a subpath export (`quoin/dialog`, `quoin/select`, …) so you only ship what you import. Components are unstyled — bring your own CSS, CSS modules, or Tailwind. Every part forwards `class`, `style`, and the standard data attributes (`data-open`, `data-disabled`, …) for styling state.

### Headless primitives

The **`createX` API**: a Solid-idiomatic primitive that owns the state, modality, and dismissal logic but renders nothing. You provide the markup and read the state from any descendant with `useX()`. The compound `<Dialog.Root>` is itself a thin wrapper over `createDialog` — drop to this layer when you need full control of the rendered tree.

```tsx
import { createSignal, Show } from "solid-js";
import { createDialog, useDialog } from "quoin-ui/dialog";

function Example() {
  const [open, setOpen] = createSignal(false);
  // Controlled — `setOpen`/`toggle` emit through `onOpenChange`; you own the signal.
  const dialog = createDialog({ open, onOpenChange: setOpen });

  return (
    <dialog.Provider>
      <button onClick={() => dialog.toggle()}>View notifications</button>
      <Show when={dialog.open()}>
        <div role="dialog" aria-modal={dialog.modal() === true}>
          <h2>Notifications</h2>
          <p>You are all caught up. Good job!</p>
          <CloseButton />
        </div>
      </Show>
    </dialog.Provider>
  );
}

function CloseButton() {
  const dialog = useDialog(); // reads the nearest <dialog.Provider>
  return <button onClick={() => dialog.setOpen(false)}>Close</button>;
}
```

> Note: the primitive manages state and behavior only — DOM machinery like the portal, focus trap, and scroll lock lives in the compound parts. With `createDialog` alone you render (and wire up) the surface yourself.

For state that lives outside your JSX — opened from an event handler or another component — the `Dialog` namespace also exposes an imperative `Dialog.createHandle()` you connect via `<Dialog.Root handle={…}>` and drive with `handle.open(triggerId)` / `handle.close()` / `handle.openWithPayload({ … })`.

## Components

|                |                    |                 |                |
| -------------- | ------------------ | --------------- | -------------- |
| **Overlays**   | Dialog             | Alert Dialog    | Popover        |
|                | Tooltip            | Preview Card    | Drawer         |
| **Menus**      | Menu               | Submenu         | Context Menu   |
|                | Menubar            | Navigation Menu | Toolbar        |
| **Selection**  | Select             | Combobox        | Autocomplete   |
| **Form**       | Field              | Fieldset        | Form           |
|                | Input              | Number Field    | OTP Field      |
| **Controls**   | Button             | Checkbox        | Checkbox Group |
|                | Radio              | Radio Group     | Switch         |
|                | Toggle             | Toggle Group    | Slider         |
| **Disclosure** | Accordion          | Collapsible     | Tabs           |
| **Display**    | Avatar             | Progress        | Meter          |
|                | Scroll Area        | Separator       |                |
| **Providers**  | Direction Provider | CSP Provider    |                |

## How it works

Quoin heavily references Base UI's component model in idiomatic Solid:

- **Composable parts.** Each component is a namespace of parts (`Dialog.Root`, `Dialog.Trigger`, `Dialog.Popup`, …) that you nest to build the markup you want.
- **Controlled by default.** Root components take controlled props (`open`/`onOpenChange`, `value`/`onValueChange`) — pass a signal and wire the callback.
- **Floating UI positioning.** Popovers, menus, selects, and tooltips position with [`@floating-ui/dom`](https://floating-ui.com).
- **SSR & hydration ready.** Ships a dual build — a DOM-compiled bundle (`import`/`default`) plus a JSX-preserved bundle (the `solid` condition) so Solid-aware bundlers can recompile for your target.

## Project status

This is an experimental Solid 2.0 port and is not affiliated with the Base UI team. It tracks Base UI's behavior and test suite where practical, with adjustments for Solid's reactivity model.

## Acknowledgements

Built on the work of the [Base UI](https://base-ui.com) team (from the creators of Radix, Floating UI, and Material UI) and the [SolidJS](https://www.solidjs.com) project.

## Required Solid patch

> [!IMPORTANT]
> quoin carries one fix against `solid-js@2.0.0-beta.28`. Package-manager patch settings do not transfer from a published dependency, so consumers that run Solid's development build should apply the included patch themselves.

The patch wraps the context value write in `untrack`, preventing the provider
root from subscribing to signals read through `props.value`. Beta.28 already
contains the former Portal and async rejection fixes.

### Applying them

quoin ships the patch files in [`patches/`](/versecafe/quoin/blob/7545d17e8aecad98f126e982a34b9b5973519311/patches?format=markdown). Copy them into your project and register them with your package manager.

**bun** — copy `patches/` to your project root and add to `package.json`:

```jsonc
{
  "patchedDependencies": {
    "solid-js@2.0.0-beta.28": "patches/solid-js@2.0.0-beta.28.patch"
  }
}
```

**pnpm** — same, under the `pnpm.patchedDependencies` key. **npm / yarn** — these have no native equivalent; use [`patch-package`](https://www.npmjs.com/package/patch-package) with the same diffs.

After installing, verify that `solid-js/dist/dev.js` contains
`untrack(() => setContext(provider, props.value))`.

> [!NOTE]
> This patch is pinned to `2.0.0-beta.28`. Re-confirm and re-derive or remove it on every Solid bump. Full rationale lives in [`solid/patches.md`](/versecafe/quoin/blob/7545d17e8aecad98f126e982a34b9b5973519311/solid/patches.md?format=markdown).

