Skip to content

Declarative dialogs#

ft.use_dialog() lets a component show and update dialogs declaratively. Instead of imperatively calling page.show_dialog() and later remembering to close or remove the dialog, you render a DialogControl from component state:

  • pass a dialog instance to show it;
  • pass None to hide it.

This keeps dialog logic in the same state flow as the rest of a declarative app:

  • state decides whether the dialog is visible;
  • dialog content updates when state changes;
  • there is no page.update() call in the component.

Basic pattern#

Call ft.use_dialog() on every render. When the dialog should be open, return a dialog control; otherwise return None.

import flet as ft


@ft.component
def App():
    show, set_show = ft.use_state(False)

    ft.use_dialog(
        ft.AlertDialog(
            modal=True,
            title=ft.Text("Delete report.pdf?"),
            content=ft.Text("This cannot be undone."),
            actions=[
                ft.TextButton("Delete", on_click=lambda: set_show(False)),
                ft.TextButton("Cancel", on_click=lambda: set_show(False)),
            ],
            on_dismiss=lambda: set_show(False),
        )
        if show
        else None
    )

    return ft.Column(
        controls=[
            ft.TextButton("Open dialog", on_click=lambda: set_show(True)),
        ]
    )


ft.run(lambda page: page.render(App))

The important part is that show is the source of truth. The dialog is not opened by mutating the page tree directly; it appears because the component renders it.

Updating dialog content from state#

Because the dialog is declarative, its content can react to state changes while it is open. This is useful for confirmations, form validation, and async workflows:

import asyncio
import flet as ft


@ft.component
def App():
    show, set_show = ft.use_state(False)
    deleting, set_deleting = ft.use_state(False)

    async def handle_delete():
        set_deleting(True)
        await asyncio.sleep(2)
        set_deleting(False)
        set_show(False)

    ft.use_dialog(
        ft.AlertDialog(
            modal=True,
            title=ft.Text("Delete report.pdf?"),
            content=ft.Text(
                "Deleting, please wait..." if deleting else "This cannot be undone."
            ),
            actions=[
                ft.Button(
                    "Deleting..." if deleting else "Delete",
                    disabled=deleting,
                    on_click=handle_delete,
                ),
                ft.TextButton(
                    "Cancel",
                    disabled=deleting,
                    on_click=lambda: set_show(False),
                ),
            ],
            on_dismiss=lambda: set_show(False),
        )
        if show
        else None
    )

    return ft.TextButton("Delete file", on_click=lambda: set_show(True))

ft.run(lambda page: page.render(App))

This pattern works well with asyncio and other async APIs in Flet apps. For more background, see Async apps.

Chaining dialogs#

You can call ft.use_dialog() more than once in the same component. That makes follow-up flows straightforward, for example:

  • confirmation dialog;
  • async action;
  • success dialog after the first dialog fully closes.
import flet as ft


@ft.component
def App():
    show_confirm, set_show_confirm = ft.use_state(False)
    show_success, set_show_success = ft.use_state(False)
    should_chain = ft.use_ref(False)

    def confirm_delete():
        should_chain.current = True
        set_show_confirm(False)

    def on_confirm_dismiss():
        if should_chain.current:
            should_chain.current = False
            set_show_success(True)

    ft.use_dialog(
        ft.AlertDialog(
            title=ft.Text("Delete file?"),
            actions=[
                ft.TextButton("Delete", on_click=confirm_delete),
                ft.TextButton("Cancel", on_click=lambda: set_show_confirm(False)),
            ],
            on_dismiss=on_confirm_dismiss,
        )
        if show_confirm
        else None
    )

    ft.use_dialog(
        ft.AlertDialog(
            title=ft.Text("Done"),
            content=ft.Text("The file was deleted."),
            actions=[
                ft.TextButton("OK", on_click=lambda: set_show_success(False)),
            ],
        )
        if show_success
        else None
    )

    return ft.TextButton("Open", on_click=lambda: set_show_confirm(True))

ft.run(lambda page: page.render(App))

ft.use_ref() is helpful here because the value survives re-renders without causing another render by itself.

on_dismiss timing#

DialogControl.on_dismiss fires after the dialog close animation completes, not immediately when open changes to False. This makes it safe to start follow-up UI after the dialog has actually finished closing.

Use on_dismiss for logic that should happen after the dialog is fully gone, such as:

  • opening the next dialog in a chain;
  • resetting temporary dialog-local state;
  • starting a follow-up animation or toast.

When to use page.show_dialog() instead#

ft.use_dialog() is a better fit inside @ft.component functions and other declarative flows.

page.show_dialog() is still a good option when:

  • the app is written imperatively;
  • dialog lifecycle is handled outside the component tree;
  • you need to trigger a dialog from existing page-level event code and do not want to convert that part of the app to declarative style yet.

In practice, the two APIs serve different styles:

  • use declarative dialogs when UI should follow component state;
  • use imperative dialogs when UI is managed by direct page mutation.