Claude Code Plugins

Community-maintained marketplace

Feedback

flow-actions-patterns

@Z1-Test/temp-rajan-s2
0
0

Complete user flow patterns for actions, forms, navigation, and state transitions

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

title Flow Actions & Patterns
description Complete user flow patterns for actions, forms, navigation, and state transitions
tags frontend, flows, actions, user-journey, interactions
name flow-actions-patterns

Flow Actions & Patterns Skill

What is it?

This skill defines complete user flow patterns—from initial action trigger through completion. It ensures all interactions are logically connected, properly confirmed, and appropriately provide feedback.

Why use it?

  • Completeness: No action left half-implemented
  • Consistency: Same flow patterns everywhere
  • UX Quality: Users always know what happened
  • Error Prevention: Proper validation and confirmation
  • Accessibility: Clear action feedback for all users

Action Flow States

Complete Action Lifecycle

┌─────────────────────────────────────────────────────────────────┐
│                       ACTION LIFECYCLE                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  [Idle] → [Pending] → [Processing] → [Success/Error] → [Idle]  │
│    ↑                                        │                   │
│    └────────────────────────────────────────┘                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Each action MUST handle all states:

interface ActionState {
  status: 'idle' | 'pending' | 'processing' | 'success' | 'error';
  error?: string;
  data?: any;
}

function ActionButton({ onAction }: { onAction: () => Promise<void> }) {
  const [state, setState] = useState<ActionState>({ status: 'idle' });
  
  const handleClick = async () => {
    setState({ status: 'processing' });
    try {
      await onAction();
      setState({ status: 'success' });
      toast.success('Action completed!');
    } catch (error) {
      setState({ status: 'error', error: error.message });
      toast.error('Action failed');
    } finally {
      setTimeout(() => setState({ status: 'idle' }), 2000);
    }
  };
  
  return (
    <Button 
      onClick={handleClick} 
      disabled={state.status === 'processing'}
    >
      {state.status === 'processing' && <Loader2 className="mr-2 size-4 animate-spin" />}
      {state.status === 'success' && <Check className="mr-2 size-4" />}
      {state.status === 'error' && <X className="mr-2 size-4" />}
      {state.status === 'idle' ? 'Save' : 
       state.status === 'processing' ? 'Saving...' :
       state.status === 'success' ? 'Saved!' : 'Retry'}
    </Button>
  );
}

Form Submission Flows

Standard Form Flow

┌──────────────────────────────────────────────────────────────────┐
│ FORM SUBMISSION FLOW                                             │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│ 1. [Pristine Form] - All fields empty, submit disabled          │
│         ↓                                                        │
│ 2. [Dirty Form] - User entered data, validation pending         │
│         ↓                                                        │
│ 3. [Validated] - All validations pass, submit enabled           │
│         ↓                                                        │
│ 4. [Submitting] - Form locked, loading indicator                │
│         ↓                                                        │
│ 5. [Success] - Redirect or success message                      │
│    OR                                                            │
│ 5. [Error] - Show errors, unlock form for correction            │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘
function CreateUserForm() {
  const [formState, setFormState] = useState<'pristine' | 'dirty' | 'submitting' | 'success' | 'error'>('pristine');
  const [errors, setErrors] = useState<Record<string, string>>({});
  
  const form = useForm({
    defaultValues: { name: '', email: '', role: '' },
    onChange: () => setFormState('dirty'),
  });
  
  const onSubmit = async (data: FormData) => {
    setFormState('submitting');
    setErrors({});
    
    try {
      // Client-side validation
      const validationErrors = validate(data);
      if (Object.keys(validationErrors).length > 0) {
        setErrors(validationErrors);
        setFormState('error');
        return;
      }
      
      // Server submission
      await createUser(data);
      setFormState('success');
      toast.success('User created successfully!');
      router.push('/users');
      
    } catch (error) {
      if (error.status === 422) {
        // Server validation errors
        setErrors(error.fieldErrors);
      } else {
        // General error
        toast.error('Failed to create user');
      }
      setFormState('error');
    }
  };
  
  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <fieldset disabled={formState === 'submitting'}>
        {/* Form fields */}
        <FormField name="name" error={errors.name} />
        <FormField name="email" error={errors.email} />
        <FormField name="role" error={errors.role} />
        
        {/* Actions */}
        <div className="flex justify-end gap-3 mt-6">
          <Button type="button" variant="outline" onClick={() => router.back()}>
            Cancel
          </Button>
          <Button type="submit" disabled={formState === 'pristine'}>
            {formState === 'submitting' && <Loader2 className="mr-2 size-4 animate-spin" />}
            Create User
          </Button>
        </div>
      </fieldset>
    </form>
  );
}

CRUD Action Flows

Create Flow

function CreateEntityFlow() {
  return (
    <>
      {/* 1. Trigger */}
      <Button onClick={() => setDialogOpen(true)}>
        <Plus className="mr-2 size-4" />
        Add New
      </Button>
      
      {/* 2. Form Dialog */}
      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Create New Item</DialogTitle>
            <DialogDescription>Fill out the form to create a new item.</DialogDescription>
          </DialogHeader>
          
          {/* 3. Form */}
          <form onSubmit={handleCreate}>
            {/* Fields... */}
            
            {/* 4. Actions */}
            <DialogFooter>
              <Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
              <Button type="submit" disabled={isSubmitting}>
                {isSubmitting ? <Loader2 className="animate-spin" /> : 'Create'}
              </Button>
            </DialogFooter>
          </form>
        </DialogContent>
      </Dialog>
      
      {/* 5. Success - Auto-close dialog, show toast, refresh list */}
    </>
  );
}

Update/Edit Flow

function EditEntityFlow({ entity }: { entity: Entity }) {
  const [isEditing, setIsEditing] = useState(false);
  
  // Inline edit mode
  if (isEditing) {
    return (
      <Card className="border-primary">
        <form onSubmit={handleUpdate}>
          {/* Editable fields */}
          <Input defaultValue={entity.name} name="name" />
          
          <div className="flex justify-end gap-2 mt-4">
            <Button variant="ghost" size="sm" onClick={() => setIsEditing(false)}>
              Cancel
            </Button>
            <Button size="sm" type="submit">
              Save Changes
            </Button>
          </div>
        </form>
      </Card>
    );
  }
  
  // View mode
  return (
    <Card>
      <div className="flex justify-between items-center">
        <div>
          <h3>{entity.name}</h3>
          <p className="text-muted-foreground">{entity.description}</p>
        </div>
        <Button variant="ghost" size="icon" onClick={() => setIsEditing(true)}>
          <Pencil className="size-4" />
        </Button>
      </div>
    </Card>
  );
}

Delete Flow (Destructive)

function DeleteEntityFlow({ entity, onDelete }: DeleteFlowProps) {
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [isDeleting, setIsDeleting] = useState(false);
  
  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      await deleteEntity(entity.id);
      toast.success(`${entity.name} deleted`);
      setConfirmOpen(false);
      onDelete();
    } catch (error) {
      toast.error('Failed to delete');
    } finally {
      setIsDeleting(false);
    }
  };
  
  return (
    <>
      {/* 1. Delete Trigger */}
      <Button variant="ghost" size="icon" onClick={() => setConfirmOpen(true)}>
        <Trash2 className="size-4 text-destructive" />
      </Button>
      
      {/* 2. Confirmation Dialog */}
      <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Delete {entity.name}?</AlertDialogTitle>
            <AlertDialogDescription>
              This action cannot be undone. This will permanently delete 
              <strong> {entity.name}</strong> and all associated data.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
            <AlertDialogAction
              onClick={handleDelete}
              disabled={isDeleting}
              className="bg-destructive hover:bg-destructive/90"
            >
              {isDeleting && <Loader2 className="mr-2 size-4 animate-spin" />}
              Delete
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}

Navigation Flows

Breadcrumb Navigation

// Always show user's location in hierarchy
<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/">Home</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink href="/users">Users</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>John Doe</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>

Tab Navigation with State

function TabbedContent() {
  const [activeTab, setActiveTab] = useState('overview');
  
  // Preserve tab state in URL for shareability
  const searchParams = useSearchParams();
  const router = useRouter();
  
  useEffect(() => {
    const tab = searchParams.get('tab') || 'overview';
    setActiveTab(tab);
  }, [searchParams]);
  
  const handleTabChange = (value: string) => {
    router.push(`?tab=${value}`, { scroll: false });
    setActiveTab(value);
  };
  
  return (
    <Tabs value={activeTab} onValueChange={handleTabChange}>
      <TabsList>
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="settings">Settings</TabsTrigger>
        <TabsTrigger value="activity">Activity</TabsTrigger>
      </TabsList>
      <TabsContent value="overview"><OverviewPanel /></TabsContent>
      <TabsContent value="settings"><SettingsPanel /></TabsContent>
      <TabsContent value="activity"><ActivityPanel /></TabsContent>
    </Tabs>
  );
}

Multi-Step Flows (Wizards)

interface WizardStep {
  id: string;
  title: string;
  description: string;
  component: React.ComponentType<StepProps>;
  validation?: (data: WizardData) => boolean;
}

function MultiStepWizard({ steps }: { steps: WizardStep[] }) {
  const [currentStep, setCurrentStep] = useState(0);
  const [data, setData] = useState<WizardData>({});
  const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
  
  const isFirstStep = currentStep === 0;
  const isLastStep = currentStep === steps.length - 1;
  const currentStepConfig = steps[currentStep];
  
  const handleNext = () => {
    if (currentStepConfig.validation && !currentStepConfig.validation(data)) {
      return; // Validation failed
    }
    
    setCompletedSteps(prev => new Set(prev).add(currentStep));
    setCurrentStep(prev => Math.min(prev + 1, steps.length - 1));
  };
  
  const handleBack = () => {
    setCurrentStep(prev => Math.max(prev - 1, 0));
  };
  
  const handleComplete = async () => {
    try {
      await submitWizardData(data);
      toast.success('Wizard completed!');
      router.push('/success');
    } catch (error) {
      toast.error('Failed to complete');
    }
  };
  
  return (
    <div className="max-w-2xl mx-auto">
      {/* Progress indicator */}
      <nav className="mb-8">
        <ol className="flex items-center gap-4">
          {steps.map((step, index) => (
            <li key={step.id} className="flex items-center gap-2">
              <div className={cn(
                'size-8 rounded-full flex items-center justify-center border-2',
                index < currentStep && 'bg-primary border-primary text-primary-foreground',
                index === currentStep && 'border-primary text-primary',
                index > currentStep && 'border-muted text-muted-foreground'
              )}>
                {completedSteps.has(index) ? (
                  <Check className="size-4" />
                ) : (
                  <span className="text-sm font-medium">{index + 1}</span>
                )}
              </div>
              <span className={cn(
                'text-sm font-medium',
                index === currentStep ? 'text-foreground' : 'text-muted-foreground'
              )}>
                {step.title}
              </span>
              {index < steps.length - 1 && (
                <div className="w-8 h-px bg-border" />
              )}
            </li>
          ))}
        </ol>
      </nav>
      
      {/* Step content */}
      <Card className="p-6">
        <div className="mb-6">
          <h2 className="text-xl font-semibold">{currentStepConfig.title}</h2>
          <p className="text-muted-foreground">{currentStepConfig.description}</p>
        </div>
        
        <currentStepConfig.component 
          data={data} 
          onUpdate={(updates) => setData(prev => ({ ...prev, ...updates }))} 
        />
        
        {/* Navigation */}
        <div className="flex justify-between mt-8 pt-6 border-t">
          <Button 
            variant="outline" 
            onClick={handleBack}
            disabled={isFirstStep}
          >
            <ChevronLeft className="mr-2 size-4" />
            Back
          </Button>
          
          {isLastStep ? (
            <Button onClick={handleComplete}>
              Complete
              <Check className="ml-2 size-4" />
            </Button>
          ) : (
            <Button onClick={handleNext}>
              Next
              <ChevronRight className="ml-2 size-4" />
            </Button>
          )}
        </div>
      </Card>
    </div>
  );
}

Filtering & Search Flows

function FilterableList() {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState<Filters>({});
  const [sort, setSort] = useState<SortConfig>({ field: 'createdAt', direction: 'desc' });
  
  // Debounced search for performance
  const debouncedSearch = useDebounce(search, 300);
  
  // Data fetching with filters
  const { data, isLoading, error } = useQuery({
    queryKey: ['items', debouncedSearch, filters, sort],
    queryFn: () => fetchItems({ search: debouncedSearch, filters, sort }),
  });
  
  return (
    <div className="space-y-4">
      {/* Search bar */}
      <div className="flex gap-4">
        <div className="relative flex-1">
          <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
          <Input
            placeholder="Search..."
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="pl-10"
          />
          {search && (
            <Button
              variant="ghost"
              size="icon"
              className="absolute right-1 top-1/2 -translate-y-1/2 size-7"
              onClick={() => setSearch('')}
            >
              <X className="size-4" />
            </Button>
          )}
        </div>
        
        {/* Filter dropdown */}
        <Popover>
          <PopoverTrigger asChild>
            <Button variant="outline">
              <SlidersHorizontal className="mr-2 size-4" />
              Filters
              {Object.keys(filters).length > 0 && (
                <Badge className="ml-2">{Object.keys(filters).length}</Badge>
              )}
            </Button>
          </PopoverTrigger>
          <PopoverContent className="w-80">
            <FilterPanel filters={filters} onChange={setFilters} />
          </PopoverContent>
        </Popover>
        
        {/* Sort dropdown */}
        <Select value={`${sort.field}-${sort.direction}`} onValueChange={handleSortChange}>
          <SelectTrigger className="w-[180px]">
            <SelectValue placeholder="Sort by..." />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="createdAt-desc">Newest first</SelectItem>
            <SelectItem value="createdAt-asc">Oldest first</SelectItem>
            <SelectItem value="name-asc">Name A-Z</SelectItem>
            <SelectItem value="name-desc">Name Z-A</SelectItem>
          </SelectContent>
        </Select>
      </div>
      
      {/* Active filters */}
      {Object.keys(filters).length > 0 && (
        <div className="flex gap-2 flex-wrap">
          {Object.entries(filters).map(([key, value]) => (
            <Badge key={key} variant="secondary" className="gap-1">
              {key}: {value}
              <button onClick={() => removeFilter(key)}>
                <X className="size-3" />
              </button>
            </Badge>
          ))}
          <Button variant="ghost" size="sm" onClick={() => setFilters({})}>
            Clear all
          </Button>
        </div>
      )}
      
      {/* Results */}
      {isLoading ? (
        <LoadingSkeleton />
      ) : error ? (
        <ErrorState error={error} onRetry={refetch} />
      ) : data?.length === 0 ? (
        <EmptyState 
          icon={search ? Search : Inbox}
          title={search ? 'No results found' : 'No items'}
          description={search ? `No results for "${search}"` : 'Create your first item'}
        />
      ) : (
        <ItemList items={data} />
      )}
    </div>
  );
}

Confirmation Patterns

Inline Confirmation (Low Risk)

// For low-risk actions, use inline confirmation
<Button 
  variant="ghost" 
  onClick={() => setConfirmDelete(itemId)}
>
  {confirmDelete === itemId ? (
    <span className="text-destructive">Click again to delete</span>
  ) : (
    <Trash2 className="size-4" />
  )}
</Button>

Dialog Confirmation (High Risk)

// For high-risk actions, use dialog confirmation
<AlertDialog>
  <AlertDialogTrigger asChild>
    <Button variant="destructive">Delete Account</Button>
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone. This will permanently delete your account 
        and remove all data from our servers.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction className="bg-destructive">
        Yes, delete my account
      </AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

Type-to-Confirm (Critical Actions)

// For critical actions, require typing confirmation
function TypeToConfirmDialog({ entityName, onConfirm, onCancel }) {
  const [input, setInput] = useState('');
  const isMatch = input === entityName;
  
  return (
    <Dialog>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Delete {entityName}?</DialogTitle>
          <DialogDescription>
            This action is irreversible. Type <strong>{entityName}</strong> to confirm.
          </DialogDescription>
        </DialogHeader>
        
        <Input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder={`Type "${entityName}" to confirm`}
        />
        
        <DialogFooter>
          <Button variant="outline" onClick={onCancel}>Cancel</Button>
          <Button 
            variant="destructive" 
            disabled={!isMatch}
            onClick={onConfirm}
          >
            Delete Permanently
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Best Practices

DO:

  • Handle all action states (idle → processing → success/error)
  • Show loading indicators during async operations
  • Provide confirmation for destructive actions
  • Give clear success/error feedback
  • Preserve form state on validation errors
  • Make multi-step flows interruptible and resumable

DON'T:

  • Leave actions without feedback
  • Allow double-submission of forms
  • Delete without confirmation
  • Lose user data on errors
  • Break browser back button in flows
  • Hide progress in long operations