Plugins

URL

Learn how to use the URL plugin to sync modal state with the URL

Better Modal supports syncing modal state with the URL, allowing you to share modal state across tabs, persist state on refresh, and create shareable links.

Setup

1. Create URL Sync

modals/url/init.ts
import { createBetterModalUrlSync } from "better-modal/url";

export const { urlSyncPlugin, BetterModalUrlSyncer } =
  createBetterModalUrlSync();

2. Add Plugin

modals/init.ts
import { betterModal } from "better-modal";
import { urlSyncPlugin } from "./url/init";

const m = betterModal({
  // ...
  plugins: [urlSyncPlugin],
});

Adding the plugin will allow you to use the .sync() method on your modals.

Usage

1. Mount the syncer

Import and mount the BetterModalUrlSyncer component. Pass in the settings for the modals you want to sync.

"use client";

import { BetterModalUrlSyncer } from "./modals/url/init";
import { useBetterModal } from "./modals/react"

export default function Page(){
    const bm = useBetterModal();


return (
    <div>
    {/* Your page content */}
    <BetterModalUrlSyncer 
        settings={[
            bm.invoice.add.sync({/* adapter-specific options*/})
        ]}
    />
    </div>
)

}

Opening any defined modal will now sync its state to the URL automatically.

2. Open a modal

You can just use the open method as you would normally.

const bm = useBetterModal();

bm.invoice.add.open({
    name: "Invoice 1",
    description: "Invoice 1 description",
});

Adapters

Use nuqs for type-safe URL state management:

npm install nuqs @better-modal/nuqs
import { createBetterModalUrlSync } from "better-modal/url";
import { createBetterModalsNuqsAdapter } from "@better-modal/nuqs";

const nuqsAdapter = createBetterModalsNuqsAdapter()

export const { urlSyncPlugin, BetterModalUrlSyncer } = createBetterModalUrlSync(
  {
    adapter: nuqsAdapter
  }
);

Make sure you have setup nuqs correctly. Check out their docs for more information.

With nuqs, you must provide a parser for each modal:

bm.user.edit.sync({
    parser: ... // nuqs parser
})

Example

  • We registered a modal with our custom variant dialog for editing an invoice.
  • The variant expects an title and a description. We defined default values for these.
  • The component EditInvoiceForm expects an id to be passed in.
const modals = registry({
    invoice: {
        edit: modal(EditInvoiceForm, "dialog").withDefaultValues({
            title: "Edit Invoice",
            description: "Changes to the invoice will be automatically saved.",
        })
    }
})

Define a schema for the invoice and a parser for it

import { z } from "zod";

const editInvoiceSchema = z.object({
    id: z.string(),
})

const editInvoiceParser = parseAsJson(editInvoiceSchema)

Use the parser in the sync method

// ... 

const bm = useBetterModal();

const syncEditInvoice = bm.invoice.edit.sync({
    parser: editInvoiceParser
})

return (
    // ...
    <BetterModalUrlSyncer 
        settings={[syncEditInvoice]}
    />
)

Open the modal

const bm = useBetterModal();

bm.invoice.edit.open({
    id: "123"
})

The URL will now be updated with the state of the modal

Result: https://your-app.com/invoices?invoice.edit=id=123

Default values are automatically omitted from the URL.

Default Adapter

The default adapter uses native URL search params with JSON.stringify:

import { createBetterModalUrlSync } from "better-modal/url";

export const { urlSyncPlugin, BetterModalUrlSyncer } =
  createBetterModalUrlSync();

Limitations

Server components are not support yet.