Tip 37/72

How to create a native dialog

Learn how to create a native dialog using TailwindCSS v4.

Add Delivery Address

The refund will be reflected in the customer’s bank account 2 to 3 business days after processing.

Native dialogs (<dialog>) are a lightweight way to display modals without relying on external JavaScript libraries. They come with built-in browser support for methods like showModal() and close(). TailwindCSS makes styling these dialogs a breeze.

Step 1: Write and Style the Dialog

Start by creating a dialog and a button to trigger it. We’ll use Tailus UI HTML classes to style the dialog and button.

<div>
    <button 
        data-dialog-trigger="dialog1" 
        class="btn sz-md variant-outlined"
    >
        <span class="btn-label">Open Dialog</span>
    </button>

    <dialog 
        role="dialog" 
        id="dialog1" 
        aria-labelledby="dialog1_label" 
        class="shadow-xl size-fit p-8 m-auto max-w-lg card variant-outlined origin-bottom backdrop:bg-gray-950/50"
    >
        <h2 id="dialog1_label" class="text-title font-medium">Add Delivery Address</h2>
        <p class="text-body text-sm mt-2">The refund will be reflected in the customer’s bank account 2 to 3 business days after processing.</p>

        <form action="post" id="userAddress" class="mt-6">
            <div data-shade="950" class="field">
                <label class="text-title font-medium text-sm" for="address">Address</label>
                <input type='text' id="address" placeholder="Lubumbashi" required class="input variant-mixed sz-md rounded-btn"/>
            </div>
        </form>
        
        <div class="mt-8 flex justify-end gap-3">
            <button data-dialog-close class="btn sz-sm variant-outlined">
                <span class="btn-label">Cancel</span>
            </button>
            <button class="btn sz-sm variant-primary" type="submit" form="userAddress">
                <span class="btn-label">Save</span>
            </button>
        </div>
    </dialog>
</div>

Attribute Breakdown:

  • data-dialog-trigger: Links the button to the dialog by matching this attribute’s value (dialog1) with the id of the dialog. Example:

    <button data-dialog-trigger="dialog1">Open Dialog</button>
    
  • data-dialog-close: Adds a close action to buttons inside the dialog. Example:

    <button data-dialog-close>Close</button>
    

Step 2 : Add transition

Enhance the dialog with smooth animations for opening and closing. We’ll use the open and starting variants with TailwindCSS’s @starting-style support.

Add the following classes to the <dialog> element:

<dialog 
    role="dialog" 
    id="dialog1" 
    aria-labelledby="dialog1_label" 
    class="shadow-xl size-fit p-8 m-auto max-w-lg card variant-outlined origin-bottom backdrop:bg-gray-950/50
          transition-all duration-300 transition-discrete opacity-0 scale-95 open:opacity-100 open:scale-100 starting:open:opacity-0 starting:open:scale-95"
>

Explanation of Classes:

  • opacity-0 and scale-95: Initial state when the dialog is not visible.
  • open:opacity-100 and open:scale-100: Final state when the dialog is open.
  • starting:open:opacity-0 and starting:open:scale-95: Ensures transitions begin from a visually smooth starting point.

Why @starting-style is Used for Dialog Transitions

In CSS, it’s not possible to transition the display property (e.g., from none to block), as it’s not animatable. Native dialogs (<dialog>) rely on the open attribute to control their visibility, and this can cause an abrupt appearance/disappearance when using showModal() or close().

The @starting-style feature solves this problem by allowing you to define a temporary “starting state” for animations. This ensures smooth transitions when the open attribute is toggled.

Step 3: Add the JavaScript

The followind code enables dialog functionality for multiple dialogs using the attributes defined above.

const dialogTriggers = document.querySelectorAll('[data-dialog-trigger]');

dialogTriggers.forEach((dialogTrigger) => {
    const dialogId = dialogTrigger.getAttribute('data-dialog-trigger');

    if (!dialogId) {
        console.error('Dialog trigger is missing a valid "data-dialog-trigger" attribute.');
        return;
    }

    const dialog = document.getElementById(dialogId);

    if (!dialog) {
        console.error(`Dialog with ID "${dialogId}" not found.`);
        return;
    }

    const dialogClose = dialog.querySelector('[data-dialog-close]');

    if (!dialogClose) {
        console.error(`Close button not found in dialog with ID "${dialogId}".`);
        return;
    }

    dialogTrigger.addEventListener('click', () => {
        document.body.classList.add('overflow-hidden');
        dialog.showModal()
    });

    dialogClose.addEventListener('click', () => {
        document.body.classList.remove('overflow-hidden');
        dialog.close()
    });
});

This code:

  • Finds all buttons with data-dialog-trigger.
  • Matches the button’s trigger ID to a dialog id.
  • Handles opening and closing dialogs using showModal() and close().
  • Prevents scrolling when a dialog is open by adding/removing overflow-hidden to/from the <body>.

This setup is scalable and reusable, so you can easily add more dialogs without extra scripts.

Common Mistakes to Avoid

  • Not linking the button and dialog correctly with data-dialog-trigger.
  • Forgetting to add data-dialog-close to the close button.
  • Not handling the overflow-hidden class on the <body> element to prevent background scrolling.

Conclusion

Native dialogs are a great way to create modals without relying on external libraries. TailwindCSS makes it easy to style these dialogs with its utility classes. By following the steps above, you can create a native dialog that is both functional and visually appealing.