Skip to content

Design system

Documentation

DesignPatterns / CRUD

Patterns / CRUD

Beta

CRUD

A CRUD pattern keeps list, create, edit, and delete actions legible as one loop, with confirmation and feedback at the point of change.

On this page

Preview

CRUD pattern

Keep list, edit, and delete flows connected: every destructive action is confirmed, and each result is announced near the list.

Projects

Manage the workspaces your team is shipping.

Atlas onboarding

Mai Nguyen · Updated 8 min ago

Active

Orbit billing

Phuc Tran · Updated Yesterday

Draft

Lumen research

Linh Do · Updated 3 days ago

Paused

Project list with search, edit dialog, and destructive delete confirmation

When to use

Use CRUD when a person can create or change records and needs to return to a list to compare the result. Start with local or fixture data while the interaction contract is being designed; do not add a service request just to make the documentation look complete.

The loop

  1. List. Show the identifying fields, status, owner, and last update needed to decide what to open.
  2. Create or edit. Use a focused form with a title that names the action and preserve the list context behind it.
  3. Confirm. Before an irreversible action, name the exact record and explain that the action cannot be undone.
  4. Report. Announce the result near the list and keep the updated record visible when possible.
crud-example.tsx
"use client";

import { useState } from "react";

import { Button } from "@phuctech/ui/components/button";
import { Card, CardContent, CardHeader, CardTitle } from "@phuctech/ui/components/card";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@phuctech/ui/components/dialog";

export function ProjectCrud() {
  const [deleteOpen, setDeleteOpen] = useState(false);

  return (
    <Card>
      <CardHeader><CardTitle>Projects</CardTitle></CardHeader>
      <CardContent>
        <Button type="button" onClick={() => setDeleteOpen(true)}>Delete selected</Button>
      </CardContent>
      <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Delete this project?</DialogTitle>
            <DialogDescription>This action cannot be undone.</DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <DialogClose render={<Button variant="outline">Cancel</Button>} />
            <Button variant="destructive" type="button" onClick={() => setDeleteOpen(false)}>Delete project</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </Card>
  );
}

Destructive actions

Delete is not a menu decoration. Use a shared Dialog, put Cancel first, and disable or close the dialog only after the operation has completed. For bulk deletion, include the count and the scope in the confirmation copy.

Confirm

“Delete Lumen research?” names the exact record and says the action cannot be undone.

Do not surprise

Never delete on row click, on menu open, or through an unlabelled icon with no confirmation step.

Accessibility

  • Give each row a stable accessible name and make icon-only edit/delete controls name the record they affect.
  • Use a real heading in the edit and delete dialogs, and return focus to the triggering action when the dialog closes.
  • Announce create, update, and delete results with a status message that remains readable after the list re-renders.
  • Keep the list usable at narrow widths; allow content to wrap or scroll rather than clipping actions.

Do and don’t

Do

Keep the record identity in the edit title, preserve the list context, and make the next action explicit.

Don’t

Put a full multi-step workflow in a small Dialog or use color as the only record status.

  • Dialog provides focus management and the confirmation surface.
  • Badge labels status with readable text, not color alone.
  • IconButton supports compact row actions with required accessible names.