Pattern gallery

Clean modal

No divider by default, a coloured footer action when one earns it, a sectioned body instead of hand-divided children.
See also

What this solves

Every modal shipped two dividers a consumer then removed, footer buttons had no way to carry colour or shade without a className override, and a body with more than one section was hand-divided per call site. Ruling 6 (doc 80) asks for defaults tuned to the common case instead of academically pure generics.
Use it when
  • A record view with more than one sectionModalBody + ModalSection, no nested Card
  • A form that saves something realSchemaForm inside ModalBody, next coloured to match the action
  • A destructive confirmConfirmModal preset="delete", now rhythmed by ModalBody
  • A multi-step flowModalFooter spread, an extra ghost action in children, divider opted back on

Rendered

Fixture data; check both themes.
patterns/clean-modal/example.tsx
Copy this into a fresh route and it renders as above.
"use client";
import {
Button,
ConfirmModal,
Modal,
ModalBody,
ModalFooter,
ModalSection,
ModalTitle,
SchemaForm,
Table,
useModal,
} from "@versable-git/ui";
import { useState } from "react";
const RECORD_ID = "clean-modal-record";
const FORM_ID = "clean-modal-form";
const CONFIRM_ID = "clean-modal-confirm";
const WIZARD_ID = "clean-modal-wizard";
const NAME_SCHEMA = {
type: "object",
properties: {
name: { type: "string", title: "Name" },
email: { type: "string", title: "Notify", format: "email" },
},
required: ["name"],
};
/**
* The four modal shapes the flipped defaults touch: a record view with a
* sectioned body, a form inside `ModalBody`, a destructive confirm now
* rhythmed by the same component, and a wizard footer opting `divider` on
* to show it is still available.
*/
export function CleanModal() {
const { openModal, closeModal } = useModal();
const [formValue, setFormValue] = useState<Record<string, unknown>>();
return (
<div className="flex flex-wrap gap-3">
<Button size="sm" content="Record modal" Icon="Copy" onClick={() => openModal(RECORD_ID)} />
<Button size="sm" content="Form modal" Icon="InputForm" onClick={() => openModal(FORM_ID)} />
<Button size="sm" content="Destructive confirm" Icon="Trash" color="error" variant="outline" onClick={() => openModal(CONFIRM_ID)} />
<Button size="sm" content="Wizard footer" Icon="Right" onClick={() => openModal(WIZARD_ID)} />
<Modal elementId={RECORD_ID} size="md">
<ModalTitle title="Disc Brake Rotor" subtitle="SKU RT-330" identifier="prt_9f21a" Icon="PartType" />
<ModalBody>
<ModalSection label="Details">
<dl className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<dt className="text-base-content/65">Brand</dt>
<dd>Brembo</dd>
<dt className="text-base-content/65">Weight</dt>
<dd>4.8 kg</dd>
</dl>
</ModalSection>
<ModalSection label="Fitment" count="2 vehicles">
<Table
density="compact"
columns={[
{ key: "make", header: "Make" },
{ key: "model", header: "Model" },
]}
rows={[
{ make: "Ford", model: "F-150" },
{ make: "Ford", model: "Ranger" },
]}
rowKey={(r) => `${r.make}-${r.model}`}
/>
</ModalSection>
</ModalBody>
<ModalFooter next={{ content: "Close", onClick: () => closeModal() }} />
</Modal>
<Modal elementId={FORM_ID} size="sm">
<ModalTitle title="Add a watcher" Icon="Team" />
<ModalBody>
<SchemaForm schema={NAME_SCHEMA} value={formValue} onChange={setFormValue} />
</ModalBody>
<ModalFooter
back={{ content: "Cancel", onClick: () => closeModal() }}
next={{ content: "Save", color: "success", onClick: () => closeModal() }}
/>
</Modal>
<ConfirmModal elementId={CONFIRM_ID} preset="delete" title="Delete this part?" message="This removes it from every fitment list. This cannot be undone." />
<Modal elementId={WIZARD_ID} size="sm">
<ModalTitle title="Import mapping" subtitle="Step 2 of 3" Icon="Workflow" />
<ModalBody>
<p className="text-base-content/70 text-sm">Match each source column to a kit field before continuing.</p>
</ModalBody>
<ModalFooter divider spread back={{ content: "Back" }} next={{ content: "Continue", color: "primary", onClick: () => closeModal() }}>
<Button size="sm" variant="ghost" content="Save draft" onClick={() => closeModal()} />
</ModalFooter>
</Modal>
</div>
);
}

Where it ships

  • packages/ui/src/modal/modal-elements.tsxModalTitle, ModalFooter, ModalBody, ModalSection
  • packages/ui/src/modal/confirm-modal.tsxConfirmModal's message now renders through ModalBody

App-specific: Real callers still own their own schemas, row data and confirm copy; this page fixes static content in place of a live record so the chrome is what's on display.

@versable-git/ui · composites proven in the apps