- Prop
open- Type
boolean- Default
- —
- Requirement
- required
- Description
- —
Docs Components Overlays
Dialog
React example follows below.
Vue example follows below.
Angular example follows below.
Svelte example follows below.
Solid example follows below.
- parts
- —
- nodes
- —
- size
- —
- part
- —
ts
import * as React from "react";/** * The base dialog plus one entry per §08 configuration. Each variant is opened * by its own trigger button; the demos keep a single Dialog instance bound to * whichever variant is active. */interface DialogVariantFixture { readonly id: string; /** Trigger button label. */ readonly trigger: string; readonly title: string; readonly description: string; readonly size?: "compact" | "default" | "wide"; /** Title ruled into its own header row, footer on the sunken surface. */ readonly divided?: boolean; /** Footer actions stacked full-width, primary on top. */ readonly stackActions?: boolean;} const dialogFixture = { id: "dialog", trigger: "View deployment", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", body: "The deployment is ready for review.", cancelLabel: "Cancel", closeLabel: "Close", dismissable: true, variants: [ { id: "default", trigger: "Default", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", }, { id: "wide", trigger: "Wide", size: "wide", title: "Tear down ledger-api?", description: "This removes the service, its infra, and all replicas. This cannot be undone.", }, { id: "divided", trigger: "Header + divided footer", size: "wide", divided: true, title: "Move to region", description: "ledger-api will drain connections before the cutover. Expect one minute of elevated latency.", }, { id: "compact", trigger: "Compact — stacked actions", size: "compact", stackActions: true, title: "Tear down ledger-api?", description: "This cannot be undone.", }, ],} satisfies { id: string; trigger: string; title: string; description: string; body: string; cancelLabel: string; closeLabel: string; dismissable: boolean; variants: readonly DialogVariantFixture[];}; import { Button, Dialog } from "@grassroot/ui-react"; const ROW = { display: "flex", flexWrap: "wrap", alignItems: "center", gap: "12px" } as const; export default function DialogDemo() { const [activeId, setActiveId] = React.useState<string | null>(null); // The Dialog stays mounted so `open` transitions false -> true and the panel // animates in; only which variant it is bound to changes. const active = dialogFixture.variants.find((variant) => variant.id === activeId) ?? dialogFixture.variants[0]; return ( <div> <div style={ROW}> {dialogFixture.variants.map((variant) => ( <Button key={variant.id} variant="ghost" onClick={() => setActiveId(variant.id)}> {variant.trigger} </Button> ))} </div> <Dialog open={activeId !== null} onOpenChange={(open) => setActiveId(open ? activeId : null)} title={active.title} description={active.description} size={active.size} divided={active.divided} stackActions={active.stackActions} dismissable={dialogFixture.dismissable} footer={ <> <Button variant="ghost" onClick={() => setActiveId(null)}> {dialogFixture.cancelLabel} </Button> <Button onClick={() => setActiveId(null)}>{dialogFixture.closeLabel}</Button> </> } > {dialogFixture.body} </Dialog> </div> );}- parts
- —
- nodes
- —
- size
- —
- part
- —
ts
<script setup lang="ts">import { computed, ref } from "vue";/** * The base dialog plus one entry per §08 configuration. Each variant is opened * by its own trigger button; the demos keep a single Dialog instance bound to * whichever variant is active. */interface DialogVariantFixture { readonly id: string; /** Trigger button label. */ readonly trigger: string; readonly title: string; readonly description: string; readonly size?: "compact" | "default" | "wide"; /** Title ruled into its own header row, footer on the sunken surface. */ readonly divided?: boolean; /** Footer actions stacked full-width, primary on top. */ readonly stackActions?: boolean;} const dialogFixture = { id: "dialog", trigger: "View deployment", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", body: "The deployment is ready for review.", cancelLabel: "Cancel", closeLabel: "Close", dismissable: true, variants: [ { id: "default", trigger: "Default", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", }, { id: "wide", trigger: "Wide", size: "wide", title: "Tear down ledger-api?", description: "This removes the service, its infra, and all replicas. This cannot be undone.", }, { id: "divided", trigger: "Header + divided footer", size: "wide", divided: true, title: "Move to region", description: "ledger-api will drain connections before the cutover. Expect one minute of elevated latency.", }, { id: "compact", trigger: "Compact — stacked actions", size: "compact", stackActions: true, title: "Tear down ledger-api?", description: "This cannot be undone.", }, ],} satisfies { id: string; trigger: string; title: string; description: string; body: string; cancelLabel: string; closeLabel: string; dismissable: boolean; variants: readonly DialogVariantFixture[];}; import { Button, Dialog } from "@grassroot/ui-vue"; const ROW = "display:flex;flex-wrap:wrap;align-items:center;gap:12px"; const activeId = ref<string | null>(null);// The Dialog stays mounted so `open` transitions false -> true and the panel// animates in; only which variant it is bound to changes.const active = computed( () => dialogFixture.variants.find((variant) => variant.id === activeId.value) ?? dialogFixture.variants[0],);const open = computed({ get: () => activeId.value !== null, set: (next: boolean) => { if (!next) activeId.value = null; },});</script> <template> <div> <div :style="ROW"> <Button v-for="variant in dialogFixture.variants" :key="variant.id" variant="ghost" @click="activeId = variant.id" >{{ variant.trigger }}</Button > </div> <Dialog v-model:open="open" :size="active.size" :divided="active.divided" :stack-actions="active.stackActions" :dismissable="dialogFixture.dismissable" > <template #title>{{ active.title }}</template> <template #description>{{ active.description }}</template> {{ dialogFixture.body }} <template #footer> <Button variant="ghost" @click="activeId = null">{{ dialogFixture.cancelLabel }}</Button> <Button @click="activeId = null">{{ dialogFixture.closeLabel }}</Button> </template> </Dialog> </div></template>- parts
- —
- nodes
- —
- size
- —
- part
- —
ts
import { Component, computed, signal } from "@angular/core";/** * The base dialog plus one entry per §08 configuration. Each variant is opened * by its own trigger button; the demos keep a single Dialog instance bound to * whichever variant is active. */interface DialogVariantFixture { readonly id: string; /** Trigger button label. */ readonly trigger: string; readonly title: string; readonly description: string; readonly size?: "compact" | "default" | "wide"; /** Title ruled into its own header row, footer on the sunken surface. */ readonly divided?: boolean; /** Footer actions stacked full-width, primary on top. */ readonly stackActions?: boolean;} const dialogFixture = { id: "dialog", trigger: "View deployment", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", body: "The deployment is ready for review.", cancelLabel: "Cancel", closeLabel: "Close", dismissable: true, variants: [ { id: "default", trigger: "Default", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", }, { id: "wide", trigger: "Wide", size: "wide", title: "Tear down ledger-api?", description: "This removes the service, its infra, and all replicas. This cannot be undone.", }, { id: "divided", trigger: "Header + divided footer", size: "wide", divided: true, title: "Move to region", description: "ledger-api will drain connections before the cutover. Expect one minute of elevated latency.", }, { id: "compact", trigger: "Compact — stacked actions", size: "compact", stackActions: true, title: "Tear down ledger-api?", description: "This cannot be undone.", }, ],} satisfies { id: string; trigger: string; title: string; description: string; body: string; cancelLabel: string; closeLabel: string; dismissable: boolean; variants: readonly DialogVariantFixture[];}; import { GrassrootButton, GrassrootDialog, GrassrootDialogBody, GrassrootDialogClose, GrassrootDialogDescription, GrassrootDialogFooter, GrassrootDialogHeader, GrassrootDialogTitle,} from "@grassroot/ui-angular"; @Component({ selector: "app-dialog-demo", standalone: true, imports: [ GrassrootButton, GrassrootDialog, GrassrootDialogTitle, GrassrootDialogDescription, GrassrootDialogFooter, GrassrootDialogHeader, GrassrootDialogBody, GrassrootDialogClose, ], template: ` <div> <div style="display:flex;flex-wrap:wrap;align-items:center;gap:12px"> @for (variant of fixture.variants; track variant.id) { <grassroot-button variant="ghost" (click)="activeId.set(variant.id)">{{ variant.trigger }}</grassroot-button> } </div> <grassroot-dialog [open]="activeId() !== null" (openChange)="onOpenChange($event)" [size]="active().size ?? 'default'" [divided]="!!active().divided" [stackActions]="!!active().stackActions" [dismissable]="fixture.dismissable" > @if (active().divided) { <!-- The header/body wrappers are nested, so they travel together through the catch-all slot in author order, which is where the divided layout wants them. aria-labelledby still resolves because the dialog title query is a descendant query. --> <div grassrootDialogHeader> <h2 grassrootDialogTitle>{{ active().title }}</h2> <button grassrootDialogClose type="button" (click)="activeId.set(null)">✕</button> </div> <div grassrootDialogBody> <p grassrootDialogDescription>{{ active().description }}</p> {{ fixture.body }} </div> } @else { <h2 grassrootDialogTitle>{{ active().title }}</h2> <p grassrootDialogDescription>{{ active().description }}</p> {{ fixture.body }} } <div grassrootDialogFooter> <grassroot-button variant="ghost" (click)="activeId.set(null)">{{ fixture.cancelLabel }}</grassroot-button> <grassroot-button (click)="activeId.set(null)">{{ fixture.closeLabel }}</grassroot-button> </div> </grassroot-dialog> </div> `,})export class DialogDemoComponent { protected readonly fixture = dialogFixture; protected readonly activeId = signal<string | null>(null); // The dialog stays mounted so `open` transitions false -> true and the panel // animates in; only which variant it is bound to changes. protected readonly active = computed( () => dialogFixture.variants.find((variant) => variant.id === this.activeId()) ?? dialogFixture.variants[0], ); protected onOpenChange(open: boolean): void { if (!open) this.activeId.set(null); }} export default DialogDemoComponent;- parts
- —
- nodes
- —
- size
- —
- part
- —
ts
<script lang="ts"> import { Button, Dialog } from "@grassroot/ui-svelte"; /** * The base dialog plus one entry per §08 configuration. Each variant is opened * by its own trigger button; the demos keep a single Dialog instance bound to * whichever variant is active. */ interface DialogVariantFixture { readonly id: string; /** Trigger button label. */ readonly trigger: string; readonly title: string; readonly description: string; readonly size?: "compact" | "default" | "wide"; /** Title ruled into its own header row, footer on the sunken surface. */ readonly divided?: boolean; /** Footer actions stacked full-width, primary on top. */ readonly stackActions?: boolean; } const dialogFixture = { id: "dialog", trigger: "View deployment", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", body: "The deployment is ready for review.", cancelLabel: "Cancel", closeLabel: "Close", dismissable: true, variants: [ { id: "default", trigger: "Default", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", }, { id: "wide", trigger: "Wide", size: "wide", title: "Tear down ledger-api?", description: "This removes the service, its infra, and all replicas. This cannot be undone.", }, { id: "divided", trigger: "Header + divided footer", size: "wide", divided: true, title: "Move to region", description: "ledger-api will drain connections before the cutover. Expect one minute of elevated latency.", }, { id: "compact", trigger: "Compact — stacked actions", size: "compact", stackActions: true, title: "Tear down ledger-api?", description: "This cannot be undone.", }, ], } satisfies { id: string; trigger: string; title: string; description: string; body: string; cancelLabel: string; closeLabel: string; dismissable: boolean; variants: readonly DialogVariantFixture[]; }; const ROW = "display:flex;flex-wrap:wrap;align-items:center;gap:12px"; let activeId = $state<string | null>(null); // The Dialog stays mounted so `open` transitions false -> true and the panel // animates in; only which variant it is bound to changes. const active = $derived( dialogFixture.variants.find((variant) => variant.id === activeId) ?? dialogFixture.variants[0], );</script> {#snippet title()}{active.title}{/snippet}{#snippet description()}{active.description}{/snippet}{#snippet footer()}<Button variant="ghost" onClick={() => (activeId = null)} >{dialogFixture.cancelLabel}</Button ><Button onClick={() => (activeId = null)}>{dialogFixture.closeLabel}</Button>{/snippet}{#snippet children()}{dialogFixture.body}{/snippet} <div> <div style={ROW}> {#each dialogFixture.variants as variant (variant.id)} <Button variant="ghost" onClick={() => (activeId = variant.id)}>{variant.trigger}</Button> {/each} </div> <Dialog open={activeId !== null} onOpenChange={(next) => { if (!next) activeId = null; }} size={active.size} divided={active.divided} stackActions={active.stackActions} dismissable={dialogFixture.dismissable} {title} {description} {footer} {children} /></div>- parts
- —
- nodes
- —
- size
- —
- part
- —
ts
/** @jsxImportSource solid-js */import { For, createMemo, createSignal } from "solid-js";import { Button, Dialog } from "@grassroot/ui-solid";/** * The base dialog plus one entry per §08 configuration. Each variant is opened * by its own trigger button; the demos keep a single Dialog instance bound to * whichever variant is active. */interface DialogVariantFixture { readonly id: string; /** Trigger button label. */ readonly trigger: string; readonly title: string; readonly description: string; readonly size?: "compact" | "default" | "wide"; /** Title ruled into its own header row, footer on the sunken surface. */ readonly divided?: boolean; /** Footer actions stacked full-width, primary on top. */ readonly stackActions?: boolean;} const dialogFixture = { id: "dialog", trigger: "View deployment", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", body: "The deployment is ready for review.", cancelLabel: "Cancel", closeLabel: "Close", dismissable: true, variants: [ { id: "default", trigger: "Default", title: "Deploy ledger-api?", description: "The rolling restart updates six replicas.", }, { id: "wide", trigger: "Wide", size: "wide", title: "Tear down ledger-api?", description: "This removes the service, its infra, and all replicas. This cannot be undone.", }, { id: "divided", trigger: "Header + divided footer", size: "wide", divided: true, title: "Move to region", description: "ledger-api will drain connections before the cutover. Expect one minute of elevated latency.", }, { id: "compact", trigger: "Compact — stacked actions", size: "compact", stackActions: true, title: "Tear down ledger-api?", description: "This cannot be undone.", }, ],} satisfies { id: string; trigger: string; title: string; description: string; body: string; cancelLabel: string; closeLabel: string; dismissable: boolean; variants: readonly DialogVariantFixture[];}; const ROW = "display:flex;flex-wrap:wrap;align-items:center;gap:12px"; export default function DialogDemo() { const [activeId, setActiveId] = createSignal<string | null>(null); // The Dialog stays mounted so `open` transitions false -> true and the panel // animates in; only which variant it is bound to changes. const active = createMemo( () => dialogFixture.variants.find((variant) => variant.id === activeId()) ?? dialogFixture.variants[0], ); return ( <div> <div style={ROW}> <For each={dialogFixture.variants}> {(variant) => ( <Button variant="ghost" onClick={() => setActiveId(variant.id)}> {variant.trigger} </Button> )} </For> </div> <Dialog open={activeId() !== null} onOpenChange={(open) => setActiveId(open ? activeId() : null)} title={active().title} description={active().description} size={active().size} divided={active().divided} stackActions={active().stackActions} dismissable={dialogFixture.dismissable} footer={ <> <Button variant="ghost" onClick={() => setActiveId(null)}> {dialogFixture.cancelLabel} </Button> <Button onClick={() => setActiveId(null)}>{dialogFixture.closeLabel}</Button> </> } > {dialogFixture.body} </Dialog> </div> );}Installation
API Reference
Import
import * as React from "react";
import { Button, Dialog } from "@grassroot/ui-react";
Props
Prop
Type
Default
Requirement
Description
openboolean—
required
—
onOpenChange(open: boolean) => void—
required
—
dismissableboolean | undefinedtrueoptional
Close when the scrim is clicked.
sizeDialogSize | undefined"default"optional
Panel width.
dividedboolean | undefined—
optional
Rule the title into its own header row and the footer onto the sunken surface, both reaching the panel's edges.
stackActionsboolean | undefined—
optional
Stack the footer's actions full-width, primary on top.
closeLabelstring | undefined"Close"optional
Accessible name for the divided header's close button.
classNamestring | undefined—
optional
—
- Prop
onOpenChange- Type
(open: boolean) => void- Default
- —
- Requirement
- required
- Description
- —
- Prop
dismissable- Type
boolean | undefined- Default
true- Requirement
- optional
- Description
- Close when the scrim is clicked.
- Prop
size- Type
DialogSize | undefined- Default
"default"- Requirement
- optional
- Description
- Panel width.
- Prop
divided- Type
boolean | undefined- Default
- —
- Requirement
- optional
- Description
- Rule the title into its own header row and the footer onto the sunken surface, both reaching the panel's edges.
- Prop
stackActions- Type
boolean | undefined- Default
- —
- Requirement
- optional
- Description
- Stack the footer's actions full-width, primary on top.
- Prop
closeLabel- Type
string | undefined- Default
"Close"- Requirement
- optional
- Description
- Accessible name for the divided header's close button.
- Prop
className- Type
string | undefined- Default
- —
- Requirement
- optional
- Description
- —
Events
| Event | Payload |
|---|---|
onOpenChange | (open: boolean) => void |
Content regions
childrendescriptionfootertitle
Import
import { computed, ref } from "vue";
import { Button, Dialog } from "@grassroot/ui-vue";
Props
Prop
Type
Default
Requirement
openboolean—
required
dismissablebooleantrueoptional
sizeDialogSize | undefinedundefinedoptional
dividedbooleanfalseoptional
stackActionsbooleanfalseoptional
closeLabelstring"Close"optional
- Prop
open- Type
boolean- Default
- —
- Requirement
- required
- Prop
dismissable- Type
boolean- Default
true- Requirement
- optional
- Prop
size- Type
DialogSize | undefined- Default
undefined- Requirement
- optional
- Prop
divided- Type
boolean- Default
false- Requirement
- optional
- Prop
stackActions- Type
boolean- Default
false- Requirement
- optional
- Prop
closeLabel- Type
string- Default
"Close"- Requirement
- optional
Events
| Event | Payload |
|---|---|
update:open | (_open: boolean) => true |
Content regions
defaultdescriptionfootertitle
Import
import { Component, computed, signal } from "@angular/core";
import {
GrassrootButton,
GrassrootDialog,
GrassrootDialogBody,
GrassrootDialogClose,
GrassrootDialogDescription,
GrassrootDialogFooter,
GrassrootDialogHeader,
GrassrootDialogTitle,
} from "@grassroot/ui-angular";
Props
Prop
Type
Default
Requirement
openboolean—
required
dismissablebooleantrueoptional
sizeDialogSize | undefinedundefinedoptional
dividedbooleanfalseoptional
stackActionsbooleanfalseoptional
closeLabelstring"Close"optional
classNamestring—
optional
- Prop
open- Type
boolean- Default
- —
- Requirement
- required
- Prop
dismissable- Type
boolean- Default
true- Requirement
- optional
- Prop
size- Type
DialogSize | undefined- Default
undefined- Requirement
- optional
- Prop
divided- Type
boolean- Default
false- Requirement
- optional
- Prop
stackActions- Type
boolean- Default
false- Requirement
- optional
- Prop
closeLabel- Type
string- Default
"Close"- Requirement
- optional
- Prop
className- Type
string- Default
- —
- Requirement
- optional
Events
| Event | Payload |
|---|---|
openChange | boolean |
Content regions
defaultdescriptionfootertitle
Import
import { Button, Dialog } from "@grassroot/ui-svelte";
Props
Prop
Type
Default
Requirement
openboolean—
required
dismissablebooleantrueoptional
onOpenChange(open: boolean) => void—
required
sizeDialogSize—
optional
dividedbooleanfalseoptional
stackActionsbooleanfalseoptional
closeLabelstring"Close"optional
classNamestring—
optional
- Prop
open- Type
boolean- Default
- —
- Requirement
- required
- Prop
dismissable- Type
boolean- Default
true- Requirement
- optional
- Prop
onOpenChange- Type
(open: boolean) => void- Default
- —
- Requirement
- required
- Prop
size- Type
DialogSize- Default
- —
- Requirement
- optional
- Prop
divided- Type
boolean- Default
false- Requirement
- optional
- Prop
stackActions- Type
boolean- Default
false- Requirement
- optional
- Prop
closeLabel- Type
string- Default
"Close"- Requirement
- optional
- Prop
className- Type
string- Default
- —
- Requirement
- optional
Events
| Event | Payload |
|---|---|
onOpenChange | (open: boolean) => void |
Content regions
childrendescriptionfootertitle
Import
import { For, createMemo, createSignal } from "solid-js";
import { Button, Dialog } from "@grassroot/ui-solid";
Props
Prop
Type
Default
Requirement
openboolean—
required
onOpenChange(open: boolean) => void—
required
dismissablebooleantrueoptional
sizeDialogSize—
optional
dividedbooleanfalseoptional
stackActionsbooleanfalseoptional
closeLabelstring"Close"optional
classNamestring—
optional
- Prop
open- Type
boolean- Default
- —
- Requirement
- required
- Prop
onOpenChange- Type
(open: boolean) => void- Default
- —
- Requirement
- required
- Prop
dismissable- Type
boolean- Default
true- Requirement
- optional
- Prop
size- Type
DialogSize- Default
- —
- Requirement
- optional
- Prop
divided- Type
boolean- Default
false- Requirement
- optional
- Prop
stackActions- Type
boolean- Default
false- Requirement
- optional
- Prop
closeLabel- Type
string- Default
"Close"- Requirement
- optional
- Prop
className- Type
string- Default
- —
- Requirement
- optional
Events
| Event | Payload |
|---|---|
onOpenChange | (open: boolean) => void |
Content regions
childrendescriptionfootertitle