nilshendriks.com

Astro Dialog component

Published on:

As a developer, creating reusable and accessible components is crucial for maintaining a scalable and manageable codebase. Recently, I implemented a dialog component using Astro, taking advantage of the native HTML <dialog> element. Below is a detailed guide on how I approached this task and the considerations I addressed along the way.

Initial Setup

To begin with, I defined an interface for the component’s props to ensure type safety and clarity on the expected inputs. These props included options for controlling the dialog’s initial open state, specifying the text for the button that triggers the dialog, and setting the dialog’s title.

interface Props {
  open?: boolean;
  openButtonText?: string;
  title?: string;
}

const {
  open = false,
  openButtonText = "Open Dialog",
  title
} = Astro.props;

const id = Math.random().toString(36).slice(2, 11);

HTML Structure

The HTML structure of the component revolves around the <dialog> element, dynamically generating a unique ID to ensure each instance is distinct. Within the dialog, there’s a header containing a button group for control buttons and an optional title. The dialog content is slotted to accommodate flexible content.

HTML
<dialog
  id={id}
  {...open ? { open: true } : {}}
  class="dialog"
>
  <header class="dialog__header">
    <div class="button-group dialog__button-group">
      <Button
        element="button"
        id={`${id}-close`}
        iconPosition="right"
        shape="circle"
      >
        <XCircle slot="icon" />;
      </Button>
    </div>
    {title && <span class="dialog__title">{title}</span>}
  </header>
  <div class="dialog__content">
    <slot />
  </div>
</dialog>
<Button
  text={openButtonText}
  element="button"
  variant="primary"
  id={`${id}-open`}
/>

Styling the Component

Styling the dialog ensures it is centered and smoothly transitions when opening and closing. Additionally, styles for the backdrop enhance visual appeal and accessibility.

CSS
dialog.dialog {
  padding: 0;
  min-width: 33vw;
  max-height: 90%;
  overflow: auto;
  transition-property: display opacity;
  transition-duration: 0.3s;
}

dialog.dialog:not([open]) {
  display: none;
  opacity: 0;
  translate: 0 5vh;
}

dialog.dialog[open] {
  display: fixed;
  opacity: 1;
}

dialog.dialog::backdrop {
  background-color: var(--color-ui-background-light-alpha);
  backdrop-filter: blur(8px);
}

.dialog__header {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 4px;
  border-bottom: 1px solid var(--color-ui-border-default, #ccc);
}

.dialog__button-group {
  position: absolute;
  top: 4px;
  left: 4px;
}

.dialog__title {
  font-size: 16px;
  font-weight: 500;
}

.dialog__content {
  padding: 16px;
}

.dialog__content :global(*:last-child) {
  margin-bottom: 0;
}

body:has(dialog[open]) {
  overflow: hidden;
}

@media (prefers-color-scheme: dark) {
  dialog.dialog::backdrop {
    background-color: var(--color-ui-background-dark-alpha);
  }
}

Adding Functionality with JavaScript

For the dialog to function correctly, JavaScript is used to handle opening and closing events. This includes listening for button clicks to toggle the dialog’s visibility and ensuring it closes when clicking outside of its boundaries.

JS
<script define:vars={{ id }}>
  document.addEventListener("DOMContentLoaded", () => {
    const dialog = document.getElementById(`${id}`);
    const showButton = document.getElementById(`${id}-open`);
    const closeButton = document.getElementById(`${id}-close`);

    if (dialog && showButton && closeButton) {
      // "Show the dialog" button opens the dialog modally
      showButton.addEventListener("click", () => {
        dialog.showModal();
      });

      // "Close" button closes the dialog
      closeButton.addEventListener("click", () => {
        dialog.close();
      });

      // "Click" outside the dialog closes it
      dialog.addEventListener("click", event => {
        // console.log("clicked: ", event.target, event.currentTarget);
        if (event.target === dialog && event.currentTarget === dialog) {
          dialog.close();
        }
      });
    } else {
      console.error("Dialog elements not found:", {
        dialog,
        showButton,
        closeButton
      });
    }
  });
</script>

Key Considerations

  • Accessibility: Ensure the dialog can be navigated using keyboard controls and provide clear visual feedback.
  • Flexibility: Use slots to allow for flexible content inside the dialog.
  • Scalability: Generate a unique ID for each dialog instance to support multiple dialogs on a single page.

By leveraging Astro’s capabilities and the native <dialog> element, I created a reusable, accessible, and flexible dialog component. This approach simplifies the codebase while enhancing the user experience with a modern and visually appealing dialog interface.