website/docs/cookbook/declarative-dialogs.md
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:
None to hide it.This keeps dialog logic in the same state flow as the rest of a declarative app:
page.update() call in the component.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.
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.
You can call ft.use_dialog() more than once in the same component. That
makes follow-up flows straightforward, for example:
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 timingDialogControl.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:
page.show_dialog() insteadft.use_dialog() is a better fit inside @ft.component
functions and other declarative flows.
page.show_dialog() is still a good option when:
In practice, the two APIs serve different styles: