Implementing Vue Modal Route

@vmrh/modal-route is a package based on Vue 3 and vue-router, implementing the concept of modal routes. It does not include any Modal UI components—only handling visibility states, state passing, etc. This makes it possible to integrate with any library that provides Modal components. What is a modal route? When I say "modal route," I mean a scenario where opening a modal causes the URL to change. You might have heard of or experienced what's called a Twitter-style modal, which is a good example of this. Moreover, when you directly visit that URL, you see the modal's content as a full page instead of a modal. This kind of modal route can be easily implemented in Next.js using Parallel Routes. In Nuxt.js, you can use Nuxt Pages Plus to achieve this. Motivation The idea to implement modal routes came from a user experience I had on an Android phone. When browsing a webpage on mobile, I often encounter modals that cover almost the entire screen. My instinct is to press the /home/modal-a When the user is on /home and clicks a button to open modal A, the URL changes to /home/modal-a. At this point, using "go back" will return to /home, thereby closing modal A. But if the user clicks the "Close" button to close modal A manually, the question is: Should we... push to /home, or back to /home? I choose to back to /home, because this aligns with the browser history state as if the user simply clicked the "back" button. 1. Modal A Open v ----------> V (page) --> /home -> /home/modal-a --- 2. Close modal with `Prev step` V /home -> /home/modal-a --- 2-1. Close modal with `.push` v -------> V (page) --> /home -> /home/modal-a -> /home If .push is used, the user technically returns to /home, but when they try to go "back" again to get to the previous (page), they end up back at A. This creates an inconsistent experience. I believe that when the user closes a modal, they naturally expect to be exiting the modal, not creating a new entry in the history stack. Therefore, when the user "closes" a modal, the app should back to /home. Scenario 2: /home → /home/modal-a → /home/modal-a/modal-c Continuing from Scenario 1, the user first clicks a button on /home to open modal A, changing the URL to /home/modal-a. Then, they click a button inside A to open modal C, updating the URL to /home/modal-a/modal-c. Since /home/modal-a/modal-c is a child route of /home/modal-a, modal A remains visible in the background while modal C appears on top of it. 1. Modal A Open v -----------> V (page) --> /home -> /home/modal-a 2. Modal C Open v -----------> v ---------------------> V (page) --> /home -> /home/modal-a -> /home/modal-a/modal-c This scenario is similar to Scenario 1 but better highlights the inconsistency caused by using .push to close a modal. If .push is used to close C, returning to A creates a situation where the "previous step" no longer closes A, but instead reopens modal C. That’s because "going back" would now return to /home/modal-a/modal-c. 1. Close Modal C by `.push` v -----------> v ---------------------> v --------------> V (page) --> /home -> /home/modal-a -> /home/modal-a/modal-c -> /home/modal-a Therefore, when the user "closes" a modal, we should use back to return to the previous step, ensuring a proper modal closing experience. Scenario 3: /home → /home/modal-a → /home/modal-b Now let’s look at a slightly more unusual case. Both modals A and B exist on the /home page. After entering /home, the user clicks a button that opens modal A. Then, from within modal A, the user clicks another button that opens modal B. Since this navigates away from /home/modal-a, modal A is closed, leaving only modal B visible on the screen. 1. Modal A Open v -----------> V (page) --> /home -> /home/modal-a 2. Modal B Open v -----------> v -------------> V (page) --> /home -> /home/modal-a -> /home/modal-b At this point, what should happen when the user goes "back" or clicks "close"? Following the logic from Scenario 1, both "close" and "back" mean going back one step in history. So, it might make sense to return to /home/modal-a and reopen modal A. But from another perspective, when a modal is opened on a page and the user clicks the "X" to close it, the common user expectation—based on typical web interactions—is to return directly to /home. From a visual experience standpoint, this feels more intuitive. Both behaviors are valid and could make sense in different contexts. However, the latter (returning to /home) aligns better with modern UX conventions. Therefore, @vmrh/modal-route chooses this as the default behavior. Wait a second—return to /home? Didn't we say earlier that "close" or "back" should simply go back one history entry? That would return to /home/modal-a, not /home. So how do we end

May 4, 2025 - 06:02
 0
Implementing Vue Modal Route

@vmrh/modal-route is a package based on Vue 3 and vue-router, implementing the concept of modal routes.

It does not include any Modal UI components—only handling visibility states, state passing, etc. This makes it possible to integrate with any library that provides Modal components.

What is a modal route?

When I say "modal route," I mean a scenario where opening a modal causes the URL to change. You might have heard of or experienced what's called a Twitter-style modal, which is a good example of this. Moreover, when you directly visit that URL, you see the modal's content as a full page instead of a modal.

This kind of modal route can be easily implemented in Next.js using Parallel Routes. In Nuxt.js, you can use Nuxt Pages Plus to achieve this.

Motivation

The idea to implement modal routes came from a user experience I had on an Android phone. When browsing a webpage on mobile, I often encounter modals that cover almost the entire screen. My instinct is to press the < (back) button to exit the modal, but it ends up exiting the entire page instead, which is frustrating.

On mobile web, the back action usually means going to the "previous page." To avoid this issue, the modal toggle should be tied to the page URL. Opening or closing a modal should be treated the same as navigating to or away from a page.

This is why I need modal routes.

Also, modals often need routing capabilities. For instance, have you ever had a requirement where a modal should automatically open when a user visits a URL with a specific query string? Or wanted to implement a multi-step process inside a modal but found it hard to integrate with router-view?

If we have modal routes, all of the above situations can be handled much more elegantly.

Features and Implementation

Although I mentioned Twitter-style modal implementations, @vmrh/modal-route is not a Twitter-style modal.

Like Twitter-style modals, the URL changes when the modal is opened. However, when you directly visit that URL, the content is still shown as a modal (possibly configurable in the future). In many situations, I think it’s more desirable to still present it as a modal instead of a full page. This feels more consistent with how most websites behave.

Secondly, @vmrh/modal-route allows you to pass complex objects when opening a modal—not just URL-compatible data. In other words, you can call openModal({ user: UserObject }) and pass any kind of data.

I aim to make this package capable of handling all modal scenarios. Having some modals behave like routes and others not creates inconsistent user experiences, which is not ideal.

Also, modals often require passing around various types of data, and what the URL can carry alone is not enough.

Third, beyond the typical one-route-one-modal setup, @vmrh/modal-route provides three types of modal routes to handle different modal scenarios:

  • Path modal: A modal specific to one page, similar to a typical modal route with a fixed URL.

  • Global modal: A modal that can appear on any page, like a login modal. Its route is dynamically appended to the current route.

  • Query modal: A functional modal (e.g., alert or confirm). When triggered, it shows a corresponding query string.

The History Challenge

One major challenge with modal routes is dealing with window.history. While the goal is to integrate with the browser history for a smoother UX, this integration also causes complications.

When a modal opens, the corresponding URL should reflect that change. To allow the user to close the modal using the "Back" button, we use router.push when opening. But how should we close it?

Let’s go through a few scenarios using three modals: A, B, and C on the /home page.

  • A: /home/A
  • B: /home/B
  • C: /home/A/C

Scenario 1: /home -> /home/modal-a

When the user is on /home and clicks a button to open modal A, the URL changes to /home/modal-a. At this point, using "go back" will return to /home, thereby closing modal A. But if the user clicks the "Close" button to close modal A manually, the question is:

Should we...

  • push to /home, or

  • back to /home?

I choose to back to /home, because this aligns with the browser history state as if the user simply clicked the "back" button.

1. Modal A Open

              v ----------> V
(page) --> /home -> /home/modal-a

---
2. Close modal with `Prev step`

             V <----------- v
(page) --> /home -> /home/modal-a

--- 

2-1. Close modal with `.push`

                            v -------> V
(page) --> /home -> /home/modal-a -> /home

If .push is used, the user technically returns to /home, but when they try to go "back" again to get to the previous (page), they end up back at A. This creates an inconsistent experience.

I believe that when the user closes a modal, they naturally expect to be exiting the modal, not creating a new entry in the history stack.

Therefore, when the user "closes" a modal, the app should back to /home.

Scenario 2: /home → /home/modal-a → /home/modal-a/modal-c

Continuing from Scenario 1, the user first clicks a button on /home to open modal A, changing the URL to /home/modal-a. Then, they click a button inside A to open modal C, updating the URL to /home/modal-a/modal-c.

Since /home/modal-a/modal-c is a child route of /home/modal-a, modal A remains visible in the background while modal C appears on top of it.

1. Modal A Open

            v -----------> V
(page) --> /home -> /home/modal-a

2. Modal C Open

             v -----------> v ---------------------> V
(page) --> /home -> /home/modal-a -> /home/modal-a/modal-c

This scenario is similar to Scenario 1 but better highlights the inconsistency caused by using .push to close a modal.

If .push is used to close C, returning to A creates a situation where the "previous step" no longer closes A, but instead reopens modal C. That’s because "going back" would now return to /home/modal-a/modal-c.

1. Close Modal C by `.push`

             v -----------> v ---------------------> v --------------> V
(page) --> /home -> /home/modal-a -> /home/modal-a/modal-c -> /home/modal-a

Therefore, when the user "closes" a modal, we should use back to return to the previous step, ensuring a proper modal closing experience.

Scenario 3: /home → /home/modal-a → /home/modal-b

Now let’s look at a slightly more unusual case.

Both modals A and B exist on the /home page. After entering /home, the user clicks a button that opens modal A. Then, from within modal A, the user clicks another button that opens modal B. Since this navigates away from /home/modal-a, modal A is closed, leaving only modal B visible on the screen.

1. Modal A Open

              v -----------> V
(page) --> /home -> /home/modal-a

2. Modal B Open

             v -----------> v -------------> V
(page) --> /home -> /home/modal-a -> /home/modal-b

At this point, what should happen when the user goes "back" or clicks "close"?

Following the logic from Scenario 1, both "close" and "back" mean going back one step in history. So, it might make sense to return to /home/modal-a and reopen modal A.

But from another perspective, when a modal is opened on a page and the user clicks the "X" to close it, the common user expectation—based on typical web interactions—is to return directly to /home. From a visual experience standpoint, this feels more intuitive.

Both behaviors are valid and could make sense in different contexts. However, the latter (returning to /home) aligns better with modern UX conventions. Therefore, @vmrh/modal-route chooses this as the default behavior.

Wait a second—return to /home? Didn't we say earlier that "close" or "back" should simply go back one history entry? That would return to /home/modal-a, not /home. So how do we end up at /home?

@vmrh/modal-route rewrites the history stack during modal opening and closing through a sequence of actions to fulfill this behavior:

1. Modal A Open

              v -----------> V
(page) --> /home -> /home/modal-a

2. Modal B Open - 1. `.back()` to `/home`

             V <----------- v 
(page) --> /home -> /home/modal-a

3. Modal B Open - 2. `.push()` to `/home/modal-b`

             v ----------> V 
(page) --> /home -> /home/modal-b

@vmrh/modal-route calculates the common ancestor route of the two modals, first using .back() to return to the shared base (/home), then .push() to navigate to the target modal route. As a result, when you open /home/modal-b, closing it or going "back" will return you to /home

Scenario 4: /home/modal-a/modal-c

What if we skip A and directly go to C? Both A and C show up since C is a child of A.

1. Modal C Open

              v ------------------> V
(page) --> /home -> /home/modal-a/modal-c

Should closing C go back to A or to /home?

Default behavior is to go back to A. This is achieved by inserting intermediate history entries:

1. Modal C Open - (1) `.push` to `/home/modal-a`

              v ----------> V
(page) --> /home -> /home/modal-a

2. Modal C Open - (2) `.push` to `/home/modal-a/modal-c`

              v ----------> v ---------------------> V
(page) --> /home -> /home/modal-a -> /home/modal-a/modal-c

So "Back" from C still lands on A.

If A is closed while on a child route, it still goes back to /home.

1. Modal A Open - `.push` to `/home/modal-a`
              3             4
              v ----------> V
(page) --> /home -> /home/modal-a

2. Go to A's child path - `.push` to `/home/modal-a/child`
              3             4                        5
              v ----------> v ---------------------> V
(page) --> /home -> /home/modal-a -> /home/modal-a/child

3. Close A: `.back(5 - 3)`
              3              4                       5
              v <----------------------------------- V
(page) --> /home -> /home/modal-a -> /home/modal-a/child

Scenario 5: /home/modal-b

What if the user enters the site directly at /home/modal-b?

1. Enter `/home/modal-b`
                 V
(??) --> /home/modal-b

We likely want the user to remain on the site after closing the modal. But if we just back, they’ll leave the site.

You might think we could manipulate history when entering:

1. Modal b Open - (1) enter website

                 V
(??) --> /home/modal-b

2. Modal b Open - (2) replace `/home/modal-b` to `/home`

              V
(page) --> /home

3. Modal b Open - (3) `.push()` to `/home/modal-b`

              v ----------> V
(page) --> /home -> /home/modal-b

But due to browser optimization, .push becomes .replace if no user interaction has occurred. So history remains:

                   V
(page) --> /home/modal-b

Thus, @vmrh/modal-route modifies history when closing the modal instead:
@vmrh/modal-route in the above scenario rewrites the history when the modal is "closed".

1. Modal B Open - enter website

                 V
(??) --> /home/modal-b

--- 
2. Modal B Close - (1) replace `/home/modal-b` with `/home`

              V
(page) --> /home

--- 
3. Modal B Close - (2) `.push()` to `/home/modal-b`

              v ----------> V
(page) --> /home -> /home/modal-b

--- 
4. Modal B Close - (3) `.back()` to `/home`

              v ----------> V
(page) --> /home -> /home/modal-b

This makes it possible to "close" modal B while still staying within the website.

However, when modal B is open, pressing "back" may exit the website. This is because Vue Router cannot prevent the "back" action from leaving the site.

Wrapping up

Although @vmrh/modal-route involves some special history manipulations under the hood, its default behavior is almost indistinguishable from a regular modal toggle on a webpage. Therefore, it can be used as a typical modal state controller — with the added feature of modal-route functionality: enabling "back to close" behavior.

Optional behaviors can also be enabled, such as allowing the modal to open when directly entering the webpage. However, enabling such features introduces new details that need to be considered and handled. In the future, this package will offer more of these customizable behaviors.

https://github.com/shunnNet/vue-modal-route