Theme
Version
GitHub PyPI Discord
On this page

Dialog

Use CDialog for a task or decision that temporarily blocks the page. It renders a native <dialog>, enters the browser top layer, makes background content inert, contains focus, restores focus, and locks page scrolling.

Dialog at a glance

Use sm for one clear decision, md for ordinary tasks, lg for richer content, and full when the task needs the viewport.

Dialog at a glance
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class DialogAtAGlance(Component):
    template = """
      <section class="dialog-glance">
        <article>
          <p class="dialog-glance__eyebrow">Lunar atlas</p>
          <h2>Mare Imbrium</h2>
          <p>A compact note for one clear decision.</p>
          <c-CDialog size="sm">
            <c-fill name="activator" data="{ activator_attrs }">
              <c-CButton c-attrs="activator_attrs">
                Open field note
              </c-CButton>
            </c-fill>
            <c-fill name="title">
              Mare Imbrium
            </c-fill>
            <c-fill name="default">
              The basin spans more than 1,100 kilometres.
            </c-fill>
          </c-CDialog>
        </article>

        <article>
          <p class="dialog-glance__eyebrow">Deep-sky catalog</p>
          <h2>Orion Nebula</h2>
          <p>A generous surface for richer observations.</p>
          <c-CDialog size="lg">
            <c-fill name="activator" data="{ activator_attrs }">
              <c-CButton variant="outline" c-attrs="activator_attrs">
                Inspect nebula
              </c-CButton>
            </c-fill>
            <c-fill name="title">
              Orion Nebula
            </c-fill>
            <c-fill name="description">
              A stellar nursery visible below Orion's belt.
            </c-fill>
            <c-fill name="default">
              New stars illuminate clouds of hydrogen, dust, and ionized gas.
            </c-fill>
          </c-CDialog>
        </article>
      </section>
    """

    css = """
      :where(.dialog-glance) {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
        gap: 1rem;
        max-width: 64rem;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.dialog-glance article) {
        display: grid;
        gap: 0.75rem;
        align-content: start;
        padding: 1.25rem;
        border: 1px solid light-dark(#c4b5fd, #6d28d9);
        border-radius: 0.875rem;
        background: Canvas;
        box-shadow: 0 0.75rem 2rem rgb(15 23 42 / 10%);
      }

      :where(.dialog-glance h2, .dialog-glance p) {
        margin: 0;
      }

      :where(.dialog-glance__eyebrow) {
        color: light-dark(#6d28d9, #c4b5fd);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }
    """


preview = DialogAtAGlance()

preview  # noqa: B018

Build a Dialog

Provide a required title and body. Spread activator_attrs onto the control that opens it. Spread close_attrs onto explicit completion or cancel actions.

Open a field note
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class OpenFieldNote(Component):
    template = """
      <section class="field-note">
        <p>Tonight's observation</p>
        <h2>Aurora over the northern ridge</h2>
        <c-CDialog>
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton c-attrs="activator_attrs">
              Read field note
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            Aurora over the northern ridge
          </c-fill>
          <c-fill name="description">
            Recorded at 01:42 under a clear sky.
          </c-fill>
          <c-fill name="default">
            <p>
              Green ribbons appeared low on the horizon, then climbed toward
              the zenith in three bright arcs.
            </p>
          </c-fill>
          <c-fill name="actions" data="{ close_attrs }">
            <c-CButton variant="ghost" c-attrs="close_attrs">
              Close note
            </c-CButton>
            <c-CButton>
              Add to atlas
            </c-CButton>
          </c-fill>
        </c-CDialog>
      </section>
    """

    css = """
      :where(.field-note) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 42rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#bae6fd, #0369a1);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.field-note h2, .field-note p) {
        margin: 0;
      }

      :where(.field-note > p) {
        color: light-dark(#0369a1, #7dd3fc);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }
    """


preview = OpenFieldNote()

preview  # noqa: B018
<c-CDialog>
  <c-fill name="activator" data="{ activator_attrs }">
    <c-CButton c-attrs="activator_attrs">
      Read field note
    </c-CButton>
  </c-fill>
  <c-fill name="title">
    Aurora over the northern ridge
  </c-fill>
  <c-fill name="description">
    Recorded at 01:42 under a clear sky.
  </c-fill>
  <c-fill name="default">
    ...
  </c-fill>
  <c-fill name="actions" data="{ close_attrs }">
    <c-CButton c-attrs="close_attrs">
      Close note
    </c-CButton>
  </c-fill>
</c-CDialog>

Compose a Dialog in Python when its content is already available there:

from citry_ui import CDialog

field_note = CDialog(
    slots={
        "title": "Aurora over the northern ridge",
        "default": note_content,
    },
)

The title becomes the accessible name. Use description for one concise summary. Keep structured or lengthy content in the body so assistive technology does not announce it as one uninterrupted description.

The activator is optional. A controlled owner may open the Dialog without one.

Configure Dialog

Server inputs are passed in Python through <c-CDialog ... /> attributes or a CDialog(...) composition call. Client inputs are passed in the browser through $c-props="{...}".

Configure Dialog
Customize example
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class ConfigureDialog(Component):
    template = """
      <section
        class="dialog-config"
        x-data="{
          size: 'md',
          scroll: 'body',
          dismissible: true,
          close_on_escape: true,
          close_on_outside: true,
        }"
        @citry-ui-preview-controls.window="Object.assign($data, $event.detail)"
      >
        <p>Observation archive</p>
        <h2>Configure the Dialog</h2>
        <c-CDialog
          $c-props="{
            size,
            scroll,
            dismissible,
            closeOnEscape: close_on_escape,
            closeOnOutside: close_on_outside,
          }"
        >
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton c-attrs="activator_attrs">
              Preview configuration
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            Observation archive
          </c-fill>
          <c-fill name="description">
            Test size, scrolling, and passive dismissal.
          </c-fill>
          <c-fill name="default">
            <p>The archive currently holds 384 lunar observations.</p>
            <p>Try Escape, the backdrop, and the explicit action.</p>
          </c-fill>
          <c-fill name="actions" data="{ close_attrs }">
            <c-CButton c-attrs="close_attrs">
              Finish preview
            </c-CButton>
          </c-fill>
        </c-CDialog>
      </section>
    """

    css = """
      :where(.dialog-config) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 52rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#c4b5fd, #6d28d9);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.dialog-config h2, .dialog-config p) {
        margin: 0;
      }

      :where(.dialog-config > p) {
        color: light-dark(#6d28d9, #c4b5fd);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }
    """


preview_controls = (
    {
        "name": "size",
        "label": "Size",
        "type": "select",
        "default": "md",
        "options": (("sm", "Small"), ("md", "Medium"), ("lg", "Large"), ("full", "Full")),
    },
    {
        "name": "scroll",
        "label": "Scroll",
        "type": "select",
        "default": "body",
        "options": (("body", "Body only"), ("dialog", "Complete Dialog")),
    },
    {
        "name": "dismissible",
        "label": "Allow passive dismissal",
        "type": "checkbox",
        "default": True,
    },
    {
        "name": "close_on_escape",
        "label": "Close on Escape",
        "type": "checkbox",
        "default": True,
    },
    {
        "name": "close_on_outside",
        "label": "Close on backdrop press",
        "type": "checkbox",
        "default": True,
    },
)

preview = ConfigureDialog()

preview  # noqa: B018

A valid client input wins over its server value. Removing it restores the server value, except open, which preserves the last committed state and becomes uncontrolled. An invalid open value does the same after reporting a diagnostic. Other invalid client values use their server fallback.

<c-CDialog
  size="md"
  scroll="body"
  $c-props="{
    size: preferredSize,
    scroll: preferredScroll,
    dismissible: allowPassiveClose,
  }"
>
  ...
</c-CDialog>

id, close_label, class_, style, and attrs are server-only because they define rendered identity, text, and native structure.

Control visibility

Pass a Boolean client open input to control visibility. onOpenChange reports user requests; update open to accept one or keep it unchanged to decline it.

Control Dialog visibility
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class ControlledDialog(Component):
    template = """
      <section
        class="controlled-dialog"
        x-data="{ open: false, accept: false, lastReason: 'none' }"
      >
        <p>Mission control</p>
        <h2>Own every visibility change</h2>
        <label class="controlled-dialog__toggle">
          <input type="checkbox" x-model="accept" />
          Accept Dialog requests
        </label>
        <c-CDialog
          $c-props="{
            open,
            onOpenChange: (nextOpen, detail) => {
              lastReason = detail.reason;
              if (accept) open = nextOpen;
            },
          }"
        >
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton c-attrs="activator_attrs">
              Request flight plan
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            Flight plan
          </c-fill>
          <c-fill name="default">
            Controlled owners may accept or decline this close request.
          </c-fill>
          <c-fill name="actions" data="{ close_attrs }">
            <c-CButton c-attrs="close_attrs">
              Request close
            </c-CButton>
          </c-fill>
        </c-CDialog>
        <p class="controlled-dialog__status" aria-live="polite">
          Last request: <strong x-text="lastReason">none</strong>
        </p>
      </section>
    """

    css = """
      :where(.controlled-dialog) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 42rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#bae6fd, #0369a1);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.controlled-dialog h2, .controlled-dialog p) {
        margin: 0;
      }

      :where(.controlled-dialog > p:first-child) {
        color: light-dark(#0369a1, #7dd3fc);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }

      :where(.controlled-dialog__toggle) {
        display: flex;
        gap: 0.5rem;
        align-items: center;
      }

      :where(.controlled-dialog__status) {
        color: color-mix(in srgb, currentColor 72%, transparent);
        font-size: 0.875rem;
      }
    """


preview = ControlledDialog()

preview  # noqa: B018
<c-CDialog
  $c-props="{
    open,
    onOpenChange: (nextOpen, detail) => {
      if (mayApply(nextOpen, detail)) open = nextOpen;
    },
  }"
>
  ...
</c-CDialog>

The callback detail identifies the trigger, close-button, action, escape, outside, or native reason. It also includes controlled ownership, the browser source, and the Dialog return value. Owner commits do not notify again.

When no client open input is supplied, CDialog commits requests itself and then notifies. Passing null or removing the input releases control without resetting the current state.

Choose dismissal rules

dismissible=True shows the built-in close Button and permits passive dismissal. close_on_escape and close_on_outside refine which passive paths are allowed. All three have matching client inputs.

Require an explicit decision
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class ExplicitDecision(Component):
    template = """
      <section class="explicit-dialog">
        <p>Telescope alignment</p>
        <h2>Require an explicit decision</h2>
        <c-CDialog c-dismissible="False">
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton intent="warn" c-attrs="activator_attrs">
              Recalibrate telescope
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            Recalibrate telescope?
          </c-fill>
          <c-fill name="description">
            Observation pauses for about two minutes.
          </c-fill>
          <c-fill name="default">
            Escape, backdrop presses, and the built-in close control are unavailable.
          </c-fill>
          <c-fill name="actions" data="{ close_attrs }">
            <c-CButton variant="outline" c-attrs="close_attrs">
              Keep current alignment
            </c-CButton>
            <c-CButton c-attrs="close_attrs">
              Begin recalibration
            </c-CButton>
          </c-fill>
        </c-CDialog>
      </section>
    """

    css = """
      :where(.explicit-dialog) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 44rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#fde68a, #a16207);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.explicit-dialog h2, .explicit-dialog p) {
        margin: 0;
      }

      :where(.explicit-dialog > p) {
        color: light-dark(#a16207, #fde68a);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }
    """


preview = ExplicitDecision()

preview  # noqa: B018

With dismissible=False, Escape, backdrop presses, and the built-in close control are unavailable. Actions with close_attrs still work, so a deliberate workflow can always complete.

Outside dismissal requires a press that starts and ends on this Dialog's backdrop. Dragging from content to the backdrop does not close it.

Place initial focus

The server initial_focus input and matching client initialFocus input accept auto or title.

Place initial focus
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class DialogInitialFocus(Component):
    template = """
      <section class="dialog-focus-grid" x-data>
        <article>
          <p>Quick observation</p>
          <h2>Focus a control</h2>
          <c-CDialog initial_focus="auto">
            <c-fill name="activator" data="{ activator_attrs }">
              <c-CButton
                c-attrs="activator_attrs"
                @click="$refs.cometName.setAttribute('autofocus', '')"
              >
                Name a comet
              </c-CButton>
            </c-fill>
            <c-fill name="title">
              Name a comet
            </c-fill>
            <c-fill name="default">
              <label for="comet-name">Catalog name</label>
              <input id="comet-name" x-ref="cometName" />
            </c-fill>
          </c-CDialog>
        </article>

        <article>
          <p>Long report</p>
          <h2>Focus the title</h2>
          <c-CDialog initial_focus="title">
            <c-fill name="activator" data="{ activator_attrs }">
              <c-CButton variant="outline" c-attrs="activator_attrs">
                Read eclipse report
              </c-CButton>
            </c-fill>
            <c-fill name="title">
              Total eclipse report
            </c-fill>
            <c-fill name="default">
              <p>
                Focusing the title starts reading at the top without jumping
                past structured content.
              </p>
              <button type="button">Continue reading</button>
            </c-fill>
          </c-CDialog>
        </article>
      </section>
    """

    css = """
      :where(.dialog-focus-grid) {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
        gap: 1rem;
        max-width: 60rem;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.dialog-focus-grid article) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        padding: 1.25rem;
        border: 1px solid light-dark(#c4b5fd, #6d28d9);
        border-radius: 0.875rem;
        background: Canvas;
      }

      :where(.dialog-focus-grid h2, .dialog-focus-grid p) {
        margin: 0;
      }

      :where(.dialog-focus-grid article > p) {
        color: light-dark(#6d28d9, #c4b5fd);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }
    """


preview = DialogInitialFocus()

preview  # noqa: B018
  • auto keeps native [autofocus] and browser Dialog focus steps. Put autofocus on the control that should receive focus first.
  • title focuses the visible title. Use it for long or structured content so reading starts at the top without jumping to a later control.

Tab and Shift+Tab stay within the nearest open Dialog. Nested Dialog controls do not enter a parent's focus loop. Closing returns focus to the element that was active before opening when it remains available. A workflow that needs a different destination can focus it after the close callback.

Do not add tabindex to the native Dialog. CDialog owns its focus contract and rejects that attribute.

Scroll long content

The server scroll input and matching client scroll input accept body or dialog.

Scroll long Dialog content
Show code
from dataclasses import dataclass

import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


@dataclass(frozen=True, slots=True)
class ObservationEntry:
    title: str
    text: str


class DialogLongContent(Component):
    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <section class="dialog-scroll-demo">
        <p>Expedition archive</p>
        <h2>Choose what scrolls</h2>
        <c-CDialog scroll="body" size="lg">
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton c-attrs="activator_attrs">
              Keep actions visible
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            Seven nights at the ridge
          </c-fill>
          <c-fill name="description">
            Body scrolling keeps this header and the actions fixed.
          </c-fill>
          <c-fill name="default">
            <c-for each="entry in entries">
              <article class="dialog-scroll-demo__entry">
                <strong>{{ entry.title }}</strong>
                <span>{{ entry.text }}</span>
              </article>
            </c-for>
          </c-fill>
          <c-fill name="actions" data="{ close_attrs }">
            <c-CButton c-attrs="close_attrs">
              Finish reading
            </c-CButton>
          </c-fill>
        </c-CDialog>
      </section>
    """

    css = """
      :where(.dialog-scroll-demo) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 46rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#bae6fd, #0369a1);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.dialog-scroll-demo h2, .dialog-scroll-demo p) {
        margin: 0;
      }

      :where(.dialog-scroll-demo > p) {
        color: light-dark(#0369a1, #7dd3fc);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }

      :where(.dialog-scroll-demo__entry) {
        display: grid;
        gap: 0.25rem;
        padding-block: 0.75rem;
        border-block-end: 1px solid color-mix(in srgb, currentColor 16%, transparent);
      }

      :where(.dialog-scroll-demo__entry span) {
        color: color-mix(in srgb, currentColor 72%, transparent);
      }
    """

    def template_data(
        self,
        kwargs: Kwargs,  # noqa: ARG002
        slots: Slots,  # noqa: ARG002
    ) -> dict[str, object]:
        return {
            "entries": tuple(
                ObservationEntry(
                    title=f"Night {index}",
                    text="A clear horizon revealed Jupiter, four bright moons, and a faint silver arc.",
                )
                for index in range(1, 10)
            )
        }


preview = DialogLongContent()

preview  # noqa: B018
  • body keeps the header and actions visible while the body scrolls.
  • dialog scrolls the complete surface.

Both modes stay inside the dynamic viewport. full fills that viewport and removes ordinary radius, border, and shadow. Long titles and actions wrap.

Use a native Dialog Form

A native <form method="dialog"> requests closure and sets the Dialog return value to the accepted submitter's value.

Use a native Dialog Form
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class DialogForm(Component):
    template = """
      <section
        class="dialog-form-demo"
        x-data="{ result: 'No constellation selected' }"
      >
        <p>Star chart</p>
        <h2>Use a native Dialog Form</h2>
        <c-CDialog
          $c-props="{
            onOpenChange: (open, detail) => {
              if (!open && detail.returnValue) result = detail.returnValue;
            },
          }"
        >
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton c-attrs="activator_attrs">
              Choose constellation
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            Choose a constellation
          </c-fill>
          <c-fill name="description">
            Native submitter values become the Dialog return value.
          </c-fill>
          <c-fill name="default">
            <form method="dialog" class="dialog-form-demo__choices">
              <button value="Orion">Orion</button>
              <button value="Cassiopeia">Cassiopeia</button>
              <button value="Cygnus">Cygnus</button>
            </form>
          </c-fill>
        </c-CDialog>
        <p class="dialog-form-demo__result" aria-live="polite">
          Selected: <strong x-text="result">No constellation selected</strong>
        </p>
      </section>
    """

    css = """
      :where(.dialog-form-demo) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 44rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#c4b5fd, #6d28d9);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.dialog-form-demo h2, .dialog-form-demo p) {
        margin: 0;
      }

      :where(.dialog-form-demo > p:first-child) {
        color: light-dark(#6d28d9, #c4b5fd);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }

      :where(.dialog-form-demo__choices) {
        display: grid;
        gap: 0.5rem;
      }

      :where(.dialog-form-demo__choices button) {
        padding: 0.75rem 1rem;
        border: 1px solid color-mix(in srgb, currentColor 24%, transparent);
        border-radius: 0.5rem;
        background: transparent;
        color: inherit;
        font: inherit;
        text-align: start;
        cursor: pointer;
      }

      :where(.dialog-form-demo__result) {
        color: color-mix(in srgb, currentColor 72%, transparent);
        font-size: 0.875rem;
      }
    """


preview = DialogForm()

preview  # noqa: B018
<c-CDialog
  $c-props="{
    onOpenChange: (open, detail) => {
      if (detail.reason === 'native') result = detail.returnValue;
    },
  }"
>
  <c-fill name="title">
    Choose a constellation
  </c-fill>
  <c-fill name="default">
    <form method="dialog">
      <button value="Orion">Orion</button>
      <button value="Cygnus">Cygnus</button>
    </form>
  </c-fill>
</c-CDialog>

In uncontrolled mode, the browser performs the native close. In controlled mode, CDialog intercepts only that final close so the owner can accept or decline it through onOpenChange. Validation, the submit event, reset, FormData, and Citry Events remain native.

For asynchronous work, control open, show loading on the submit Button, keep the Dialog open on validation or transport failure, and close after success. Do not put close_attrs on a submit Button when closing before the result would lose feedback.

Nest Dialogs

Nest one CDialog inside another body when a focused subtask genuinely needs a second modal layer.

Nest Dialogs
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class NestedDialogs(Component):
    template = """
      <section class="nested-dialog-demo">
        <p>Observatory archive</p>
        <h2>Open a chart inside a report</h2>
        <c-CDialog>
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton c-attrs="activator_attrs">
              Open transit report
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            Europa transit report
          </c-fill>
          <c-fill name="default">
            <p>The moon crossed Jupiter's face shortly after midnight.</p>
            <c-CDialog size="sm">
              <c-fill name="activator" data="{ activator_attrs }">
                <c-CButton variant="outline" c-attrs="activator_attrs">
                  Open transit chart
                </c-CButton>
              </c-fill>
              <c-fill name="title">
                Transit chart
              </c-fill>
              <c-fill name="default">
                Europa entered the western limb at 00:14 and cleared it at 02:37.
              </c-fill>
              <c-fill name="actions" data="{ close_attrs }">
                <c-CButton c-attrs="close_attrs">
                  Return to report
                </c-CButton>
              </c-fill>
            </c-CDialog>
          </c-fill>
          <c-fill name="actions" data="{ close_attrs }">
            <c-CButton c-attrs="close_attrs">
              Close report
            </c-CButton>
          </c-fill>
        </c-CDialog>
      </section>
    """

    css = """
      :where(.nested-dialog-demo) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 46rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#bae6fd, #0369a1);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.nested-dialog-demo h2, .nested-dialog-demo p) {
        margin: 0;
      }

      :where(.nested-dialog-demo > p) {
        color: light-dark(#0369a1, #7dd3fc);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }
    """


preview = NestedDialogs()

preview  # noqa: B018

Each Dialog owns only its nearest activators, close actions, focus loop, and scroll-lock claim. Closing the nested Dialog leaves its parent open and returns focus to the nested trigger. Closing a parent also closes its open descendants, so an invisible nested modal cannot retain page inertness. Escape affects the top Dialog.

Avoid deep modal stacks. A page, expansion, or inline disclosure is usually easier to understand after one nested task.

Theme and customize Dialog

Dialog follows the surrounding color-scheme even in the browser top layer. Set documented --cui-dialog-* variables on an ancestor or the Dialog root. Use public data-citry-ui-part selectors for targeted region styling.

Theme Dialog
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class DialogThemeCustomization(Component):
    template = """
      <section class="moonlit-dialog">
        <p>Moonlit observatory</p>
        <h2>Customize tokens and parts</h2>
        <c-CDialog>
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton c-attrs="activator_attrs">
              Open moon map
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            Moon map
          </c-fill>
          <c-fill name="description">
            Public variables tune the surface; public selectors tune regions.
          </c-fill>
          <c-fill name="close">
            <span aria-hidden="true"></span>
          </c-fill>
          <c-fill name="default">
            The terminator currently crosses the eastern rim of Copernicus.
          </c-fill>
        </c-CDialog>
      </section>
    """

    css = """
      :where(.moonlit-dialog) {
        --cui-dialog-backdrop: rgb(15 23 42 / 78%);
        --cui-dialog-background: light-dark(#f5f3ff, #172033);
        --cui-dialog-foreground: light-dark(#2e1065, #e0e7ff);
        --cui-dialog-border-color: light-dark(#a78bfa, #818cf8);
        --cui-dialog-radius: 1.25rem;
        --cui-dialog-shadow: 0 1.75rem 5rem rgb(49 46 129 / 36%);

        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 44rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#c4b5fd, #6d28d9);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.moonlit-dialog h2, .moonlit-dialog p) {
        margin: 0;
      }

      :where(.moonlit-dialog > p) {
        color: light-dark(#6d28d9, #c4b5fd);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }

      :where(.moonlit-dialog [data-citry-ui-part="title"]) {
        letter-spacing: 0.03em;
      }

      :where(.moonlit-dialog [data-citry-ui-part="close"]) {
        color: light-dark(#6d28d9, #c4b5fd);
      }
    """


preview = DialogThemeCustomization()

preview  # noqa: B018
.moonlit-observatory {
  --cui-dialog-backdrop: rgb(15 23 42 / 78%);
  --cui-dialog-background: #172033;
  --cui-dialog-foreground: #e0e7ff;
  --cui-dialog-border-color: #818cf8;
  --cui-dialog-radius: 1.25rem;
}

.moonlit-observatory [data-citry-ui-part="title"] {
  letter-spacing: 0.03em;
}

The optional close slot replaces only the icon inside the built-in accessible Button. Keep its content non-interactive. CDialog retains its label, behavior, and public close selector.

The documented variables, selectors, and reflected attributes are public CSS API. .cui-* classes, --_cui-* variables, and behavior markers are private.

Support narrow viewports and zoom

Use a full Dialog
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class NarrowDialog(Component):
    template = """
      <section class="narrow-dialog-demo">
        <p>Mobile star atlas</p>
        <h2>Fill a narrow viewport</h2>
        <c-CDialog size="full">
          <c-fill name="activator" data="{ activator_attrs }">
            <c-CButton c-attrs="activator_attrs">
              Open full atlas
            </c-CButton>
          </c-fill>
          <c-fill name="title">
            The complete guide to circumpolar constellations
          </c-fill>
          <c-fill name="description">
            Full size uses the dynamic viewport and keeps actions reachable.
          </c-fill>
          <c-fill name="default">
            <p>
              Ursa Major, Ursa Minor, Cassiopeia, Cepheus, and Draco remain
              above the horizon throughout the year at northern latitudes.
            </p>
          </c-fill>
          <c-fill name="actions" data="{ close_attrs }">
            <c-CButton variant="outline" c-attrs="close_attrs">
              Return to chart
            </c-CButton>
            <c-CButton>
              Mark visible stars
            </c-CButton>
          </c-fill>
        </c-CDialog>
      </section>
    """

    css = """
      :where(.narrow-dialog-demo) {
        display: grid;
        gap: 0.75rem;
        justify-items: start;
        max-width: 40rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#bae6fd, #0369a1);
        border-radius: 0.875rem;
        background: Canvas;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.narrow-dialog-demo h2, .narrow-dialog-demo p) {
        margin: 0;
      }

      :where(.narrow-dialog-demo > p) {
        color: light-dark(#0369a1, #7dd3fc);
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }
    """


preview = NarrowDialog()

preview  # noqa: B018

Dialog uses logical properties, wraps actions, and constrains ordinary sizes to the dynamic viewport. It supports nested light and dark scopes, RTL content, forced colors, text spacing, and high zoom without requiring motion.

The built-in close Button is at least 2.5rem square and always has an accessible name. A non-dismissible Dialog must provide an explicit action or another clear completion path.

Without JavaScript, open=False keeps content in a closed native Dialog. open=True shows non-modal content because only browser showModal() enters the top layer. Client activation upgrades it immediately.

API reference

Inputs

CDialog server inputs

Server inputs are passed in a template through <c-CDialog ... /> or in Python through CDialog(...).

InputTypeDefaultEffect
idstr | NonegeneratedSets native identity and title, description, and activator relationships.
openboolFalseSets the server-visible initial open state. A valid client open input controls later state.
dismissibleboolTrueShows the built-in close control and permits passive dismissal. Explicit action bindings remain available when false.
close_on_escapeboolTruePermits Escape and equivalent platform cancel requests when dismissible.
close_on_outsideboolTruePermits a press that begins and ends on this Dialog's backdrop when dismissible.
initial_focus"auto" | "title" (CDialogInitialFocus)"auto"Preserves native autofocus and Dialog focus steps, or focuses the fixed title after opening.
size"sm" | "md" | "lg" | "full" (CDialogSize)"md"Sets the responsive surface size.
scroll"body" | "dialog" (CDialogScroll)"body"Scrolls only body content or the complete Dialog surface.
close_labelnon-empty str"Close"Sets the built-in close Button's accessible name.
class_str | Mapping[str, bool] | Sequence[CClassValue] | None (CClassValue)NoneAdds native Dialog classes from a string, conditional mapping, or nested sequence and merges them with attrs.
stylestr | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None (CStyleValue)NoneAdds native Dialog inline styles from CSS text, a property mapping, or nested sequence and merges them with attrs.
attrsMapping[str, object] | NoneNoneAdds allowed native Dialog, ARIA, Alpine, and data attributes. It may also contribute class and style values; prefer the top-level inputs for those.

CDialog client inputs

Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CDialog />.

InputTypeOmitted behaviorEffect
openboolean | nullContinues uncontrolled from the current committed state. null has the same effect.Controls visible open state while supplied as a Boolean. An invalid value reports once and releases control from the current state.
dismissiblebooleanUses the server input.Controls built-in close visibility and passive dismissal.
closeOnEscapebooleanUses the server input.Controls Escape and platform cancel dismissal.
closeOnOutsidebooleanUses the server input.Controls backdrop-press dismissal.
initialFocus"auto" | "title" (CDialogInitialFocus)Uses the server input.Controls focus placement on the next opening.
size"sm" | "md" | "lg" | "full" (CDialogSize)Uses the server input.Controls data-size and responsive geometry.
scroll"body" | "dialog" (CDialogScroll)Uses the server input.Controls data-scroll and overflow behavior.
onOpenChangefunctionDoes not notify a component callback.Receives user-authored open requests and unavoidable native close reconciliation.

Slots

Slots are passed as nested content or <c-fill> tags in a template, or through the slots={...} argument in Python.

CDialog slots

SlotRequiredDataFallback
activatorno{activator_attrs: dict[str, object]} (CDialogActivatorSlotData)No activator.
titleyes{} (CDialogTitleSlotData)none
descriptionno{} (CDialogDescriptionSlotData)Omitted, with no aria-describedby.
defaultyes{} (CDialogDefaultSlotData)none
actionsno{close_attrs: dict[str, object]} (CDialogActionsSlotData)omitted
closeno{} (CDialogCloseSlotData)Built-in multiplication-sign icon inside the accessible close Button.

Events

Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.

CDialog events

EventSignatureTrigger and timingDetailControlled and cancellation behavior
onOpenChange(requestedOpen: boolean, detail: CDialogOpenChangeDetail) => void (CDialogOpenChangeDetail)An owned trigger, built-in close, explicit action, Escape, outside press, or native close requests a different open state.{reason: "trigger" | "close-button" | "action" | "escape" | "outside" | "native", controlled: boolean, source: Element | EventTarget | null, returnValue: string} (CDialogOpenChangeDetail)Uncontrolled requests commit before notification. Controlled requests, including successful Dialog Form submission, wait for the owner. An external native close is reconciled immediately and a stale true input cannot reopen it. Owner commits do not notify.

Methods

-

CSS

CSS variables to theme the components. Set them on an ancestor or the component itself.

CDialog CSS variables

Apply these variables to CDialog or one of its ancestors.

VariableTypePurposeDefault
--cui-dialog-backdropcolorTop-layer backdrop color.rgb(15 23 42 / 58%)
--cui-dialog-backgroundcolorSurface background.Canvas
--cui-dialog-foregroundcolorSurface text.CanvasText
--cui-dialog-border-colorcolorSurface boundary.Subtle CanvasText mix.
--cui-dialog-radiuslengthSurface corner radius.0.875rem
--cui-dialog-shadowshadowSurface elevation.0 1.5rem 4rem rgb(15 23 42 / 28%)
--cui-dialog-inline-sizelengthResponsive preferred width.Size-derived; 36rem at md.
--cui-dialog-max-block-sizelengthMaximum non-full height.calc(100dvb - 2rem)
--cui-dialog-paddinglengthSurface region padding.1.25rem
--cui-dialog-gaplengthGap between Dialog regions.1rem
--cui-dialog-close-sizelengthBuilt-in close Button target size.2.5rem
--cui-dialog-close-radiuslengthBuilt-in close Button radius.0.5rem

Attributes

HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.

CDialog attributes

AttributeElementTypeMeaning
data-openNative Dialogpresent | absentMirrors effective native open state.
data-sizeNative Dialog"sm" | "md" | "lg" | "full"Mirrors effective responsive size.
data-scrollNative Dialog"body" | "dialog"Mirrors effective overflow mode.

Selectors

Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.

CDialog selectors

SelectorElementPurpose
[data-citry-ui-part="dialog"]Native DialogModal root and attrs destination.
[data-citry-ui-part="surface"]SurfaceVisual Dialog surface.
[data-citry-ui-part="header"]HeaderTitle and built-in close layout.
[data-citry-ui-part="title"]TitleRequired accessible visible title.
[data-citry-ui-part="description"]DescriptionOptional concise described-by content.
[data-citry-ui-part="close"]Close ButtonBuilt-in accessible dismissal control.
[data-citry-ui-part="body"]BodyRequired default content region.
[data-citry-ui-part="actions"]ActionsOptional explicit-action region.

Interfaces

Aliases and data shapes referenced above.

Input type aliases

InterfaceDefinition
CClassValuestr | Mapping[str, bool] | Sequence[CClassValue]
CStyleValuestr | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue]
CDialogInitialFocusLiteral["auto", "title"]
CDialogSizeLiteral["sm", "md", "lg", "full"]
CDialogScrollLiteral["body", "dialog"]

CDialogActivatorSlotData

FieldTypeDefaultMeaning
activator_attrsdict[str, object]-Owned trigger marker plus aria-haspopup, aria-controls, and synchronized aria-expanded.

CDialogTitleSlotData

Empty dataclass: {}.

CDialogDescriptionSlotData

Empty dataclass: {}.

CDialogDefaultSlotData

Empty dataclass: {}.

CDialogActionsSlotData

FieldTypeDefaultMeaning
close_attrsdict[str, object]-Explicit-close marker for an action control. A Button value becomes the requested return value.

CDialogCloseSlotData

Empty dataclass: {}.

CDialogOpenChangeDetail

FieldTypeDefaultMeaning
reason"trigger" | "close-button" | "action" | "escape" | "outside" | "native"-Source of the open or close request.
controlledboolean-Whether a valid client open value currently owns state.
sourceElement | EventTarget | null-Browser source associated with the request.
returnValuestring-Explicit action value or native Dialog Form return value; empty for other requests.

Translation keys

Catalog keys used by this family. An explicit component input or slot listed in Override takes precedence over the catalog for that instance.

CDialog translation keys

KeyPurposeVariablesOverrideBrowser updates
citry-ui-dialog-closeNames the generated close control.Noneclose_label input or close slot$c-tr updates aria-label.