import { useState } from "react";
import { FormDialog } from "../kumo/form-dialog/form-dialog";
import { Button, Input } from "@cloudflare/kumo";
/** Basic add CIDR route dialog */
export function FormDialogBasicDemo() {
const [open, setOpen] = useState(false);
const [network, setNetwork] = useState("");
const [comment, setComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setNetwork("");
setComment("");
setOpen(false);
};
const handleClose = () => {
setNetwork("");
setComment("");
setOpen(false);
};
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Add CIDR Route
</Button>
<FormDialog
size="base"
open={open}
onOpenChange={(o) => !o && handleClose()}
title="Add CIDR Route"
description="Define a private network range accessible through your tunnel."
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
isSubmitDisabled={!network.trim()}
submitButtonText="Add Route"
>
<Input
label="Network CIDR"
labelTooltip="Use CIDR notation, e.g. 10.0.0.0/24 for a /24 block."
placeholder="10.0.0.0/24"
value={network}
onChange={(e) => setNetwork(e.target.value)}
/>
<Input
label="Comment"
placeholder="Office network"
value={comment}
onChange={(e) => setComment(e.target.value)}
required={false}
/>
</FormDialog>
</>
);
} Installation
FormDialog is a block - a CLI-installed component that you own and can customize. Unlike regular components, blocks are copied into your project so you have full control over the code.
1. Initialize Kumo config (first time only)
npx @cloudflare/kumo init2. Install the block
npx @cloudflare/kumo add form-dialog3. Import from your local path
// The path depends on your kumo.json blocksDir setting
// Default: src/components/kumo/
import { FormDialog } from "./components/kumo/form-dialog/form-dialog";Why blocks? Blocks give you full ownership of the code, allowing you to customize form layouts, validation behaviour, and reset logic to fit your specific needs.
Usage
import { useState } from "react";
import { Button, Input } from "@cloudflare/kumo";
import { FormDialog } from "./components/kumo/form-dialog/form-dialog";
export default function Example() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
await createZone(name);
setIsSubmitting(false);
setName("");
setOpen(false);
};
const handleClose = () => {
setName("");
setOpen(false);
};
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Add Zone
</Button>
<FormDialog
open={open}
onOpenChange={(o) => !o && handleClose()}
title="Add Zone"
description="Enter the domain name you want to add to Cloudflare."
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
isSubmitDisabled={!name.trim()}
submitButtonText="Add Zone"
>
<Input
label="Domain name"
placeholder="example.com"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</FormDialog>
</>
);
} Examples
Size
Use size=“sm” for single-field forms or text-only dialogs, and
size=“base” (default) for forms with two side-by-side inputs or
one long input.
import { useState } from "react";
import { FormDialog } from "../kumo/form-dialog/form-dialog";
import { Button, Input } from "@cloudflare/kumo";
/** Small dialog for a single short input — use when the form has one field or is text-only */
export function FormDialogSmDemo() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setName("");
setOpen(false);
};
const handleClose = () => {
setName("");
setOpen(false);
};
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Create a group
</Button>
<FormDialog
size="sm"
open={open}
onOpenChange={(o) => !o && handleClose()}
title="Create a group"
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
isSubmitDisabled={!name.trim()}
submitButtonText="Create"
>
<Input
label="Name"
placeholder="my-group"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</FormDialog>
</>
);
} Multiple sections
Use children to compose sections with their own headings and
descriptions.
import { useState } from "react";
import { FormDialog } from "../kumo/form-dialog/form-dialog";
import { Button, Text, Select, ClipboardText, Surface, Label } from "@cloudflare/kumo";
/** Create tunnel dialog — OS/architecture selectors with dynamic installation instructions */
export function FormDialogComplexDemo() {
const [open, setOpen] = useState(false);
const [os, setOs] = useState("linux");
const [arch, setArch] = useState("x86-64");
const [isSubmitting, setIsSubmitting] = useState(false);
const installSteps = INSTALL_STEPS[`${os}-${arch}`] ?? [];
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setOs("linux");
setArch("x86-64");
setOpen(false);
};
const handleClose = () => {
setOs("linux");
setArch("x86-64");
setOpen(false);
};
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Create Tunnel
</Button>
<FormDialog
size="base"
open={open}
onOpenChange={(o) => !o && handleClose()}
title="Create Tunnel"
description="Connect your infrastructure to Cloudflare's network."
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
submitButtonText="Create"
>
<div className="flex gap-3">
<div className="flex-1">
<Select
label="Operating system"
value={os}
onValueChange={(v) => {
if (v) {
setOs(v);
setArch("x86-64");
}
}}
className="w-full"
>
<Select.Option value="linux">Linux</Select.Option>
<Select.Option value="macos">macOS</Select.Option>
</Select>
</div>
<div className="flex-1">
<Select
label="Architecture"
value={arch}
onValueChange={(v) => {
if (v) setArch(v);
}}
className="w-full"
>
<Select.Option value="x86-64">x86-64</Select.Option>
<Select.Option value="arm64">ARM64</Select.Option>
</Select>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<Label>Installation</Label>
<Text variant="secondary" size="sm">
Run this command to install the connector on your machine.
</Text>
</div>
<Surface className="p-3 rounded-lg flex bg-kumo-elevated flex-col gap-3">
{installSteps.map((step) => (
<div key={step.title} className="flex flex-col gap-2">
<Text variant="secondary" size="xs">
{step.title}
</Text>
<ClipboardText text={step.command} />
</div>
))}
</Surface>
</div>
</FormDialog>
</>
);
} With Info Banner
Pass any ReactNode to the banner prop — the caller
decides whether it is an info notice, a warning, or an error.
import { useState } from "react";
import { FormDialog } from "../kumo/form-dialog/form-dialog";
import { Button, Input, Banner } from "@cloudflare/kumo";
import { InfoIcon } from "@phosphor-icons/react";
/** Form dialog with an info banner */
export function FormDialogWithBannerDemo() {
const [open, setOpen] = useState(false);
const [network, setNetwork] = useState("");
const [comment, setComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setNetwork("");
setComment("");
setOpen(false);
};
const handleClose = () => {
setNetwork("");
setComment("");
setOpen(false);
};
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Add CIDR
</Button>
<FormDialog
size="base"
open={open}
onOpenChange={(o) => !o && handleClose()}
title="Add Private Network"
description="Define a private network range accessible through your tunnel."
banner={
<Banner
variant="default"
icon={<InfoIcon weight="fill" />}
description="A Cloudflare Gateway or WARP client is required to route traffic to private networks."
/>
}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
isSubmitDisabled={!network.trim()}
submitButtonText="Add Network"
>
<Input
label="Network (CIDR)"
placeholder="10.0.0.0/24"
value={network}
onChange={(e) => setNetwork(e.target.value)}
/>
<Input
label="Comment"
placeholder="Office network"
value={comment}
onChange={(e) => setComment(e.target.value)}
required={false}
/>
</FormDialog>
</>
);
} Error State
Show a Banner variant=“error” when the server returns an error.
Pass undefined to hide it when there is no error.
import { useState } from "react";
import { FormDialog } from "../kumo/form-dialog/form-dialog";
import { Button, Input } from "@cloudflare/kumo";
/** Form dialog with an error message */
export function FormDialogErrorDemo() {
const [open, setOpen] = useState(false);
const [network, setNetwork] = useState("");
const [comment, setComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setIsSubmitting(true);
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setError("The CIDR route 10.0.0.0/24 overlaps with an existing route.");
};
const handleClose = () => {
setNetwork("");
setComment("");
setError("");
setOpen(false);
};
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Add CIDR Route
</Button>
<FormDialog
size="base"
open={open}
onOpenChange={(o) => !o && handleClose()}
title="Add CIDR Route"
description="Define a private network range accessible through your tunnel."
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
isSubmitDisabled={!network.trim()}
submitButtonText="Add Route"
errorMessage={error || undefined}
>
<Input
label="Network (CIDR)"
placeholder="10.0.0.0/24"
value={network}
onChange={(e) => setNetwork(e.target.value)}
/>
<Input
label="Comment"
placeholder="Office network"
value={comment}
onChange={(e) => setComment(e.target.value)}
required={false}
/>
</FormDialog>
</>
);
} API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "base" | "base" | - |
| open* | boolean | - | Whether the dialog is open |
| title* | string | - | Dialog title |
| description | string | - | Optional description rendered directly below the title |
| banner | ReactNode | - | Optional banner rendered above form fields (caller decides variant — info, warning, etc.) |
| errorMessage | string | - | Error message to display as an error banner above form fields |
| children | ReactNode | - | Form field content |
| isSubmitting | boolean | - | Whether the submit action is in progress |
| isSubmitDisabled | boolean | - | Whether the submit button should be disabled |
| submitButtonText | string | - | Submit button label (default: "Save") |
| cancelButtonText | string | - | Cancel button label (default: "Cancel") |
| className | string | - | Additional className for the dialog |