The Power Of Dialog/ue - Part 3

Using powerful elements to enhance communication

In part 2 of our series we found a more reflective way of dialoguing with an agitated client while also exploring some of the flexibility in the dialog html element to match the user experience needed in a website. Let’s tackle some more challenging communication.

At a meeting a co-worker reveals the following: “When the client said ‘I don’t care so much what it looks like; it just needs to work’ I felt insulted and demoralized. I was shocked and silent and no one said anything about it”. Ouch. How can we help support our team member without bashing the client while taking shared responsibility for a painful dynamic that has the potential to spiral into shared despair and conflict? Start by pausing to take a breath.


Meanwhile work needs to move forward and there are new technical challenges to tackle on the project. It turns out we have the need for a third dialog and this one is a tricky one because it needs to show up almost anywhere on the page when someone does a right click (i.e. a context-menu event is triggered).

In this case the dialog element opened with a showModal is perfect because we want our context menu to capture and confine the interactions of the user. However, in our use case, we have different links to put in this context menu depending on what gets clicked on but we would like to still use one dialog element. When you need to communicate between different parts of the page wrapped in different stimulus controllers the solution to turn to are outlets.

Our solution involved two stimulus controllers, one for placing and populating the content of the dialog wrapped around the trigger element that is clicked and another controller for opening and closing the actual dialog element. The first context-menu controller is as follows:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["menu"]
  static outlets = ["context-menu-dialog"]

  open(e) {
    e.preventDefault();
    e.stopPropagation();
    this.element.classList.add("active");
    this.positionMenu(e);
    this.contextMenuDialogOutlet.open(this.element);
  }

  positionMenu(e) {
    const dialog = this.contextMenuDialogOutlet.dialogTarget
    const menuDimensions = this.getDimensions(this.menuTarget);
    dialog.style.left = `${this.clampValue(
      e.pageX,
      window.innerWidth,
      menuDimensions.width
    )}px`;
    dialog.style.top = `${this.clampValue(
      e.pageY,
      document.documentElement.scrollHeight,
      menuDimensions.height
    )}px`;
    dialog.innerHTML = this.menuTarget.innerHTML
  }

  clampValue(value, maxValue, elementDimension) {
    let viewportDimension = maxValue - elementDimension;
    return value > viewportDimension ? viewportDimension : value;
  }

  getDimensions(element) {
    let dimensions = {};
    dimensions.width = element.offsetWidth;
    dimensions.height = element.offsetHeight;
    return dimensions;
  }
}

and the related context_menu_dialog controller:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["dialog"]

  connect() {
    this.outsideClose = this.outsideClose.bind(this);
    document.addEventListener("click", this.outsideClose);
  }

  disconnect() {
    document.removeEventListener("click", this.outsideClose);
  }

  open(trigger) {
    this.trigger = trigger
    this.dialogTarget.close()
    this.dialogTarget.showModal()
    this.dialogTarget.focus()
  }

  close() {
    this.dialogTarget.close()
    this.trigger?.classList.remove('active')
  }

  outsideClose(e) {
    if (this.dialogTarget.contains(e.target) === false) {
      this.dialogTarget.close()
      this.trigger?.classList.remove('active')
    }
  }
}

Notice how we are able to connect the trigger and target across space and time through the contextMenuDialogOutlet. We set the innerHTML and position the dialog and can pass the trigger into the call to open so that the dialog can do some css class management remotely on close. Ultimately it might make sense to use a library like FloatingUI to place the dialog on the page but we just rolled our own simple positionMenu code for now.

Our context menu dialog is simple:

     <div data-controller="context-menu-dialog"
          data-action="click->context-menu-dialog#close"
          id="context-menu-dialog">
       <dialog class="absolute context-menu outline-none rounded-md w-30"
               data-context-menu-dialog-target="dialog">
       </dialog>
     </div>

The context menu trigger component is a bit more complicated and uses a JumpstartComponent which is a home baked version of ViewComponent but ultimately populates some menu links that renders this partial:

<div data-controller="context-menu"
     data-context-menu-context-menu-dialog-outlet="#context-menu-dialog"
     data-action="contextmenu->context-menu#open"
     class="group">
  <%= component.content %>

  <div data-context-menu-target="menu" class="hidden">
    <%= tag.div(
      data: component.data,
      class: "flex flex-col w-full bg-white rounded-md border-gray-600 border py-1"
    ) do %>
      <% %w[set focus do].map do |action| %>
        <%= render(component.menu_link(action)) %>
      <% end %>
    <% end %>
  </div>
</div>

And here is the result:

Context menu dialog component


So now how do we handle the communication with our distraught co-worker so that we can transcend space and time and build a lasting working relationship? We might start by reflecting back the experience but let’s also try an even deeper form of dialogue called mourning in NVC. Here is a brief clip of Marshall Rosenberg the originator of NVC giving a live role play demo of this process:

Rather than apologize for our silence we might check in with ourself and offer: “When I reflect on my silence when the client requested bypassing design effort I feel disappointed because I would have liked to have had more awareness and demonstrated more support and respect for our company’s design team experience and expertise crafting thoughtful user experiences.” Hopefully this comes through as both an honest self reflection while also meeting our co-workers need to be seen and considered.

Whether designing and developing great user interfaces or navigating the complex web of relationships while building projects, good communication takes a lot of practice and dedication. Fortunately there are great tools out there that have grown out of years of learning from trial and error. I highly recommend trying out the dialog element on your next project and hopefully you might be inspired to try dialoguing with your client and teammates in new ways as well.

If you’re looking for a team to help you discover the right thing to build and help you build it, get in touch.

Published on August 15, 2025