Dialog
A modal dialog: it takes focus, makes the page behind it inert and stops it scrolling, and gives focus back when it closes.
The preview needs JavaScript. The code is in the Code tab.
<script>
const open = state(false)
</script>
<template>
<div>
<Button @press="open = true">Delete project</Button>
<Dialog :open="open" @dismiss="open = false">
<span slot="title">Delete this project?</span>
This removes its files and cannot be undone.
<div slot="actions">
<Button tone="neutral" emphasis="outline" @press="open = false">Cancel</Button>
<Button tone="danger" @press="open = false">Delete</Button>
</div>
</Dialog>
</div>
</template>
Installation
npx @pixelscriptio/tessera add Dialogpnpm exec tessera add Dialogyarn tessera add Dialogbunx @pixelscriptio/tessera add Dialog- Also installs Button, which it uses.
- Works with Tessera 0.9.0 or newer.
Copy and paste the code into your project
Put it in
system/components. Add Button the same way.system/components/Dialog.tess <contract> { "name": "Dialog", "role": "dialog", "description": "A modal dialog: it takes focus, makes the page behind it inert and stops it scrolling, and gives focus back when it closes.", "since": "0.5.0", "parts": ["root", "panel", "title", "body", "actions"], "variants": { "size": ["md", "sm", "lg"] // first value is the default }, "states": ["open"], "behavior": "dialog", "slots": { "title": "the heading, which is also the dialog's accessible name", "default": "the content", "actions": "the buttons, such as Cancel and Save" }, "events": { "dismiss": "fired with { reason: 'escape' | 'backdrop' | 'native' } when the person asks to close it; listen and set open to false" }, "contrast": [ { "foreground": "body", "background": "panel" }, { "foreground": "title", "background": "panel" } ], "preview": { "slots": { "title": "Delete project?", "default": "This cannot be undone. The project and its files are removed for everyone.", "actions": [ { "component": "Button", "props": { "tone": "neutral", "emphasis": "outline" }, "slots": { "default": "Cancel" } }, { "component": "Button", "props": { "tone": "danger" }, "slots": { "default": "Delete" } } ] } } } </contract> <script> const { open, persistent } = props({ open: false, persistent: false }) // The component owns whether it is open, starts from the prop and follows it when it changes. const isOpen = state(open.value) effect(() => { isOpen.value = open.value }) // An id of its own, so the title can name the dialog however many there are. const titleId = `t-dialog-${Math.random().toString(36).slice(2, 9)}` function dismiss(detail) { // A persistent dialog only reports the request, so its owner can confirm first and close it by setting open. if (!persistent.value) isOpen.value = false emit('dismiss', detail) } </script> <template> <dialog class="root" :data-open="isOpen ? 'true' : null" :aria-labelledby="titleId" @dismiss="dismiss($event)" > <div class="panel"> <h2 class="title" :id="titleId"><slot name="title" /></h2> <div class="body"><slot /></div> <div class="actions"><slot name="actions" /></div> </div> </dialog> </template> <style> .root { max-width: calc(100vw - 2rem); padding: 0; border: 1px solid $color.border.default; border-radius: $radius.lg; background: $color.surface.default; color: $color.text.default; box-shadow: $shadow.md; font-family: var(--t-font-family-sans); &::backdrop { /* No text goes on the backdrop; this says what would be readable on it, for the contrast check. */ color: $color.surface.default; background: $color.surface.inverse; opacity: $opacity.disabled; } } .panel { display: flex; flex-direction: column; gap: $space.4; box-sizing: border-box; width: 28rem; max-width: 100%; padding: $space.6; background: $color.surface.default; } .root[data-size='sm'] .panel { width: 20rem; } .root[data-size='lg'] .panel { width: 40rem; } .title { margin: 0; color: $color.text.default; font-size: $font.size.lg; font-weight: $font.weight.bold; } .body { color: $color.text.muted; font-size: $font.size.base; line-height: 1.6; } .actions { display: flex; justify-content: flex-end; gap: $space.2; } .actions:empty { display: none; } </style>Check your design tokens
Your design system needs these.
tessera addreports the ones that are missing; by hand, runnpx @pixelscriptio/tessera check.color.border.defaultcolor.surface.defaultcolor.surface.inversecolor.text.defaultcolor.text.mutedfont.size.basefont.size.lgfont.weight.boldopacity.disabledradius.lgshadow.mdspace.2space.4space.6
Usage
Using Dialog
Use a dialog for something that needs an answer before the person goes on: confirming a deletion, a short form. For information that can wait, use the page.
- Fill the
titleslot (it is the dialog's name for assistive technology), the default slot with the content, and theactionsslot with the buttons. - It is closed until
openis true. It takes focus, makes the page behind it inert, stops the page scrolling, keeps Tab inside, and gives focus back to what opened it when it closes. - Escape and a click outside the panel fire
dismisswith areason. The dialog closes itself; listen fordismissand setopento false to keep your state in step. Setpersistentto keep it open until you setopen, for example to confirm before closing. - Always give a visible way to close it in
actions: Escape and the backdrop are shortcuts.
The preview needs JavaScript. The code is in the Code tab.
<script>
const open = state(false)
</script>
<template>
<div>
<Button @press="open = true">Delete project</Button>
<Dialog :open="open" @dismiss="open = false">
<span slot="title">Delete this project?</span>
This removes its files and cannot be undone.
<div slot="actions">
<Button tone="neutral" emphasis="outline" @press="open = false">Cancel</Button>
<Button tone="danger" @press="open = false">Delete</Button>
</div>
</Dialog>
</div>
</template>
API reference
Variants
| Variant | Values |
|---|---|
size | md default sm lg |
Slots
| Slot | Content |
|---|---|
title | the heading, which is also the dialog's accessible name |
default | the content |
actions | the buttons, such as Cancel and Save |
Events
| Event | When |
|---|---|
dismiss | fired with { reason: 'escape' | 'backdrop' | 'native' } when the person asks to close it; listen and set open to false |
Anatomy
| Role | dialog |
| Behavior | dialog |
| Parts | root panel title body actions |
| States | open |
| Since | 0.5.0 |
Design tokens it uses
Your design system needs these. tessera add checks that it has them, and can add the ones it lacks.
color.border.default color.surface.default color.surface.inverse color.text.default color.text.muted font.size.base font.size.lg font.weight.bold opacity.disabled radius.lg shadow.md space.2 space.4 space.6
Checksum
a07fb388b78003b2ba2de4f0cce5de10c0cb2e27d889c1667a9e35c6091b50fd